2024-02-11 12:59:00 +11:00
|
|
|
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
2023-09-10 19:04:46 -04:00
|
|
|
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'
|
2024-02-11 12:59:00 +11:00
|
|
|
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
2024-02-14 08:03:20 +11:00
|
|
|
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-09-13 08:36:47 +10:00
|
|
|
let lastMessage = ''
|
|
|
|
|
2023-09-08 17:50:37 +10:00
|
|
|
interface CommandInfo {
|
|
|
|
commandType: CommandTypes
|
|
|
|
range: SourceRange
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode: PathToNode
|
2023-09-08 17:50:37 +10:00
|
|
|
parentId?: string
|
|
|
|
}
|
2023-09-17 21:57:43 -07:00
|
|
|
|
|
|
|
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
|
|
|
|
2023-09-08 17:50:37 +10:00
|
|
|
interface ResultCommand extends CommandInfo {
|
2023-06-22 16:43:33 +10:00
|
|
|
type: 'result'
|
|
|
|
data: any
|
2023-09-17 21:57:43 -07:00
|
|
|
raw: WebSocketResponse
|
2023-10-16 15:29:02 +11:00
|
|
|
headVertexId?: string
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-09-21 14:32:47 +10:00
|
|
|
interface FailedCommand extends CommandInfo {
|
|
|
|
type: 'failed'
|
|
|
|
errors: Models['FailureWebSocketResponse_type']['errors']
|
|
|
|
}
|
2023-09-08 17:50:37 +10:00
|
|
|
interface PendingCommand extends CommandInfo {
|
2023-06-22 16:43:33 +10:00
|
|
|
type: 'pending'
|
|
|
|
promise: Promise<any>
|
|
|
|
resolve: (val: any) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ArtifactMap {
|
2023-09-21 14:32:47 +10:00
|
|
|
[key: string]: ResultCommand | PendingCommand | FailedCommand
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
interface NewTrackArgs {
|
|
|
|
conn: EngineConnection
|
|
|
|
mediaStream: MediaStream
|
|
|
|
}
|
|
|
|
|
2023-09-21 12:07:47 -04:00
|
|
|
// This looks funny, I know. This is needed because node and the browser
|
|
|
|
// disagree as to the type. In a browser it's a number, but in node it's a
|
|
|
|
// "Timeout".
|
|
|
|
type Timeout = ReturnType<typeof setTimeout>
|
|
|
|
|
2023-09-10 19:04:46 -04:00
|
|
|
type ClientMetrics = Models['ClientMetrics_type']
|
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
type Value<T, U> = U extends undefined
|
|
|
|
? { type: T; value: U }
|
|
|
|
: U extends void
|
|
|
|
? { type: T }
|
|
|
|
: { type: T; value: U }
|
|
|
|
|
|
|
|
type State<T, U> = Value<T, U>
|
|
|
|
|
2024-02-12 16:00:31 -05:00
|
|
|
export enum EngineConnectionStateType {
|
2024-01-23 13:13:43 -05:00
|
|
|
Fresh = 'fresh',
|
|
|
|
Connecting = 'connecting',
|
|
|
|
ConnectionEstablished = 'connection-established',
|
2024-02-12 16:00:31 -05:00
|
|
|
Disconnecting = 'disconnecting',
|
2024-01-23 13:13:43 -05:00
|
|
|
Disconnected = 'disconnected',
|
|
|
|
}
|
|
|
|
|
2024-02-12 16:00:31 -05:00
|
|
|
export enum DisconnectingType {
|
2024-01-23 13:13:43 -05:00
|
|
|
Error = 'error',
|
|
|
|
Timeout = 'timeout',
|
|
|
|
Quit = 'quit',
|
|
|
|
}
|
|
|
|
|
2024-02-12 16:00:31 -05:00
|
|
|
export interface ErrorType {
|
|
|
|
// We may not necessary have an error to assign.
|
|
|
|
error?: Error
|
|
|
|
|
|
|
|
// We assign this in the state setter because we may have not failed at
|
|
|
|
// a Connecting state, which we check for there.
|
|
|
|
lastConnectingValue?: ConnectingValue
|
|
|
|
}
|
|
|
|
|
|
|
|
export type DisconnectingValue =
|
|
|
|
| State<DisconnectingType.Error, ErrorType>
|
|
|
|
| State<DisconnectingType.Timeout, void>
|
|
|
|
| State<DisconnectingType.Quit, void>
|
2024-01-23 13:13:43 -05:00
|
|
|
|
|
|
|
// These are ordered by the expected sequence.
|
2024-02-12 16:00:31 -05:00
|
|
|
export enum ConnectingType {
|
2024-01-23 13:13:43 -05:00
|
|
|
WebSocketConnecting = 'websocket-connecting',
|
|
|
|
WebSocketEstablished = 'websocket-established',
|
|
|
|
PeerConnectionCreated = 'peer-connection-created',
|
|
|
|
ICEServersSet = 'ice-servers-set',
|
|
|
|
SetLocalDescription = 'set-local-description',
|
|
|
|
OfferedSdp = 'offered-sdp',
|
|
|
|
ReceivedSdp = 'received-sdp',
|
|
|
|
SetRemoteDescription = 'set-remote-description',
|
|
|
|
WebRTCConnecting = 'webrtc-connecting',
|
|
|
|
ICECandidateReceived = 'ice-candidate-received',
|
|
|
|
TrackReceived = 'track-received',
|
|
|
|
DataChannelRequested = 'data-channel-requested',
|
|
|
|
DataChannelConnecting = 'data-channel-connecting',
|
|
|
|
DataChannelEstablished = 'data-channel-established',
|
|
|
|
}
|
|
|
|
|
2024-02-12 16:00:31 -05:00
|
|
|
export enum ConnectingTypeGroup {
|
|
|
|
WebSocket = 'WebSocket',
|
|
|
|
ICE = 'ICE',
|
|
|
|
WebRTC = 'WebRTC',
|
|
|
|
}
|
|
|
|
|
|
|
|
export const initialConnectingTypeGroupState: Record<
|
|
|
|
ConnectingTypeGroup,
|
|
|
|
[ConnectingType, boolean | undefined][]
|
|
|
|
> = {
|
|
|
|
[ConnectingTypeGroup.WebSocket]: [
|
|
|
|
[ConnectingType.WebSocketConnecting, undefined],
|
|
|
|
[ConnectingType.WebSocketEstablished, undefined],
|
|
|
|
],
|
|
|
|
[ConnectingTypeGroup.ICE]: [
|
|
|
|
[ConnectingType.PeerConnectionCreated, undefined],
|
|
|
|
[ConnectingType.ICEServersSet, undefined],
|
|
|
|
[ConnectingType.SetLocalDescription, undefined],
|
|
|
|
[ConnectingType.OfferedSdp, undefined],
|
|
|
|
[ConnectingType.ReceivedSdp, undefined],
|
|
|
|
[ConnectingType.SetRemoteDescription, undefined],
|
|
|
|
[ConnectingType.WebRTCConnecting, undefined],
|
|
|
|
[ConnectingType.ICECandidateReceived, undefined],
|
|
|
|
],
|
|
|
|
[ConnectingTypeGroup.WebRTC]: [
|
|
|
|
[ConnectingType.TrackReceived, undefined],
|
|
|
|
[ConnectingType.DataChannelRequested, undefined],
|
|
|
|
[ConnectingType.DataChannelConnecting, undefined],
|
|
|
|
[ConnectingType.DataChannelEstablished, undefined],
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
export type ConnectingValue =
|
2024-01-23 13:13:43 -05:00
|
|
|
| State<ConnectingType.WebSocketConnecting, void>
|
|
|
|
| State<ConnectingType.WebSocketEstablished, void>
|
|
|
|
| State<ConnectingType.PeerConnectionCreated, void>
|
|
|
|
| State<ConnectingType.ICEServersSet, void>
|
|
|
|
| State<ConnectingType.SetLocalDescription, void>
|
|
|
|
| State<ConnectingType.OfferedSdp, void>
|
|
|
|
| State<ConnectingType.ReceivedSdp, void>
|
|
|
|
| State<ConnectingType.SetRemoteDescription, void>
|
|
|
|
| State<ConnectingType.WebRTCConnecting, void>
|
|
|
|
| State<ConnectingType.TrackReceived, void>
|
|
|
|
| State<ConnectingType.ICECandidateReceived, void>
|
|
|
|
| State<ConnectingType.DataChannelRequested, string>
|
|
|
|
| State<ConnectingType.DataChannelConnecting, string>
|
|
|
|
| State<ConnectingType.DataChannelEstablished, void>
|
|
|
|
|
2024-02-12 16:00:31 -05:00
|
|
|
export type EngineConnectionState =
|
2024-01-23 13:13:43 -05:00
|
|
|
| State<EngineConnectionStateType.Fresh, void>
|
|
|
|
| State<EngineConnectionStateType.Connecting, ConnectingValue>
|
|
|
|
| State<EngineConnectionStateType.ConnectionEstablished, void>
|
2024-02-12 16:00:31 -05:00
|
|
|
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
|
|
|
|
| State<EngineConnectionStateType.Disconnected, void>
|
2024-01-23 13:13:43 -05: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-11-28 21:23:20 +11:00
|
|
|
class EngineConnection {
|
2023-08-18 16:16:16 -04:00
|
|
|
websocket?: WebSocket
|
2023-06-22 16:43:33 +10:00
|
|
|
pc?: RTCPeerConnection
|
2023-08-31 07:39:03 +10:00
|
|
|
unreliableDataChannel?: RTCDataChannel
|
2024-01-23 13:13:43 -05:00
|
|
|
mediaStream?: MediaStream
|
|
|
|
|
|
|
|
private _state: EngineConnectionState = {
|
|
|
|
type: EngineConnectionStateType.Fresh,
|
|
|
|
}
|
|
|
|
|
|
|
|
get state(): EngineConnectionState {
|
|
|
|
return this._state
|
|
|
|
}
|
|
|
|
|
|
|
|
set state(next: EngineConnectionState) {
|
|
|
|
console.log(`${JSON.stringify(this.state)} → ${JSON.stringify(next)}`)
|
2024-02-12 16:00:31 -05:00
|
|
|
|
|
|
|
if (next.type === EngineConnectionStateType.Disconnecting) {
|
2024-01-23 13:13:43 -05:00
|
|
|
console.trace()
|
|
|
|
const sub = next.value
|
2024-02-12 16:00:31 -05:00
|
|
|
if (sub.type === DisconnectingType.Error) {
|
|
|
|
// Record the last step we failed at.
|
|
|
|
// (Check the current state that we're about to override that
|
|
|
|
// it was a Connecting state.)
|
|
|
|
console.log(sub)
|
|
|
|
if (this._state.type === EngineConnectionStateType.Connecting) {
|
|
|
|
if (!sub.value) sub.value = {}
|
|
|
|
sub.value.lastConnectingValue = this._state.value
|
|
|
|
}
|
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
console.error(sub.value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._state = next
|
2024-02-12 16:00:31 -05:00
|
|
|
this.onConnectionStateChange(this._state)
|
2024-01-23 13:13:43 -05:00
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2023-09-21 12:07:47 -04:00
|
|
|
private failedConnTimeout: Timeout | null
|
2023-08-18 16:16:16 -04:00
|
|
|
|
|
|
|
readonly url: string
|
|
|
|
private readonly token?: string
|
2024-02-12 16:00:31 -05:00
|
|
|
|
|
|
|
// For now, this is only used by the NetworkHealthIndicator.
|
|
|
|
// We can eventually use it for more, but one step at a time.
|
|
|
|
private onConnectionStateChange: (state: EngineConnectionState) => void
|
|
|
|
|
|
|
|
// These are used for the EngineCommandManager and were created
|
|
|
|
// before onConnectionStateChange existed.
|
2023-08-24 23:46:45 +10:00
|
|
|
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-09-10 19:04:46 -04:00
|
|
|
// TODO: actual type is ClientMetrics
|
|
|
|
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
constructor({
|
|
|
|
url,
|
|
|
|
token,
|
2024-02-12 16:00:31 -05:00
|
|
|
onConnectionStateChange = () => {},
|
2023-08-24 23:46:45 +10:00
|
|
|
onNewTrack = () => {},
|
|
|
|
onEngineConnectionOpen = () => {},
|
|
|
|
onConnectionStarted = () => {},
|
|
|
|
onClose = () => {},
|
|
|
|
}: {
|
|
|
|
url: string
|
|
|
|
token?: string
|
2024-02-12 16:00:31 -05:00
|
|
|
onConnectionStateChange?: (state: EngineConnectionState) => void
|
2023-08-24 23:46:45 +10:00
|
|
|
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-09-21 12:07:47 -04:00
|
|
|
this.failedConnTimeout = null
|
2024-02-12 16:00:31 -05:00
|
|
|
this.onConnectionStateChange = onConnectionStateChange
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onEngineConnectionOpen = onEngineConnectionOpen
|
|
|
|
this.onConnectionStarted = onConnectionStarted
|
2024-01-23 13:13:43 -05:00
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
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
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
// Without an interval ping, our connection will timeout.
|
2023-09-21 12:07:47 -04:00
|
|
|
let pingInterval = setInterval(() => {
|
2024-01-23 13:13:43 -05:00
|
|
|
switch (this.state.type as EngineConnectionStateType) {
|
|
|
|
case EngineConnectionStateType.ConnectionEstablished:
|
|
|
|
this.send({ type: 'ping' })
|
|
|
|
break
|
2024-02-12 16:00:31 -05:00
|
|
|
case EngineConnectionStateType.Disconnecting:
|
2024-01-23 13:13:43 -05:00
|
|
|
case EngineConnectionStateType.Disconnected:
|
|
|
|
clearInterval(pingInterval)
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
break
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
}, pingIntervalMs)
|
2023-09-21 12:07:47 -04:00
|
|
|
|
|
|
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
2024-01-23 13:13:43 -05:00
|
|
|
let connectRetryInterval = setInterval(() => {
|
|
|
|
if (this.state.type !== EngineConnectionStateType.Disconnected) return
|
2024-02-12 16:00:31 -05:00
|
|
|
|
|
|
|
// Only try reconnecting when completely disconnected.
|
|
|
|
clearInterval(connectRetryInterval)
|
|
|
|
console.log('Trying to reconnect')
|
|
|
|
this.connect()
|
2023-09-21 12:07:47 -04:00
|
|
|
}, connectionTimeoutMs)
|
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
|
2023-09-21 12:07:47 -04:00
|
|
|
isConnecting() {
|
2024-01-23 13:13:43 -05:00
|
|
|
return this.state.type === EngineConnectionStateType.Connecting
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
isReady() {
|
2024-01-23 13:13:43 -05:00
|
|
|
return this.state.type === EngineConnectionStateType.ConnectionEstablished
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
|
2023-09-21 12:07:47 -04:00
|
|
|
tearDown() {
|
2024-01-23 13:13:43 -05:00
|
|
|
this.disconnectAll()
|
|
|
|
this.state = {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: EngineConnectionStateType.Disconnecting,
|
|
|
|
value: { type: DisconnectingType.Quit },
|
2024-01-23 13:13:43 -05:00
|
|
|
}
|
2023-09-21 12:07:47 -04:00
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
// 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-09-21 12:07:47 -04:00
|
|
|
if (this.isConnecting() || this.isReady()) {
|
|
|
|
return
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2023-08-30 10:34:14 -04:00
|
|
|
// Information on the connect transaction
|
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
const createPeerConnection = () => {
|
|
|
|
this.pc = new RTCPeerConnection()
|
|
|
|
|
|
|
|
// Data channels MUST BE specified before SDP offers because requesting
|
|
|
|
// them affects what our needs are!
|
|
|
|
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
|
|
|
|
this.pc.createDataChannel(DATACHANNEL_NAME_UMC)
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.DataChannelRequested,
|
|
|
|
value: DATACHANNEL_NAME_UMC,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pc.addEventListener('icecandidate', (event) => {
|
|
|
|
if (event.candidate === null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.ICECandidateReceived,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Request a candidate to use
|
|
|
|
this.send({
|
|
|
|
type: 'trickle_ice',
|
|
|
|
candidate: event.candidate.toJSON(),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.pc.addEventListener('icecandidateerror', (_event: Event) => {
|
|
|
|
const event = _event as RTCPeerConnectionIceErrorEvent
|
|
|
|
console.warn(
|
|
|
|
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
|
|
|
|
)
|
2023-08-30 13:14:52 -04:00
|
|
|
})
|
2024-01-23 13:13:43 -05:00
|
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
|
|
|
|
// Event type: generic Event type...
|
|
|
|
this.pc.addEventListener('connectionstatechange', (event: any) => {
|
|
|
|
console.log('connectionstatechange: ' + event.target?.connectionState)
|
|
|
|
switch (event.target?.connectionState) {
|
|
|
|
// From what I understand, only after have we done the ICE song and
|
|
|
|
// dance is it safest to connect the video tracks / stream
|
|
|
|
case 'connected':
|
|
|
|
// Let the browser attach to the video stream now
|
|
|
|
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
|
|
|
|
break
|
|
|
|
case 'failed':
|
|
|
|
this.disconnectAll()
|
|
|
|
this.state = {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: EngineConnectionStateType.Disconnecting,
|
2024-01-23 13:13:43 -05:00
|
|
|
value: {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: DisconnectingType.Error,
|
|
|
|
value: {
|
|
|
|
error: new Error(
|
|
|
|
'failed to negotiate ice connection; restarting'
|
|
|
|
),
|
|
|
|
},
|
2024-01-23 13:13:43 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.pc.addEventListener('track', (event) => {
|
|
|
|
const mediaStream = event.streams[0]
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.TrackReceived,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (mediaStream.getVideoTracks().length !== 1) {
|
|
|
|
reject(new Error('too many video tracks to report'))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let videoTrack = mediaStream.getVideoTracks()[0]
|
|
|
|
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
|
|
|
let client_metrics: ClientMetrics = {
|
|
|
|
rtc_frames_decoded: 0,
|
|
|
|
rtc_frames_dropped: 0,
|
|
|
|
rtc_frames_received: 0,
|
|
|
|
rtc_frames_per_second: 0,
|
|
|
|
rtc_freeze_count: 0,
|
|
|
|
rtc_jitter_sec: 0.0,
|
|
|
|
rtc_keyframes_decoded: 0,
|
|
|
|
rtc_total_freezes_duration_sec: 0.0,
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(paultag): Since we can technically have multiple WebRTC
|
|
|
|
// video tracks (even if the Server doesn't at the moment), we
|
|
|
|
// ought to send stats for every video track(?), and add the stream
|
|
|
|
// ID into it. This raises the cardinality of collected metrics
|
|
|
|
// when/if we do, but for now, just report the one stream.
|
|
|
|
|
|
|
|
videoTrackStats.forEach((videoTrackReport) => {
|
|
|
|
if (videoTrackReport.type === 'inbound-rtp') {
|
|
|
|
client_metrics.rtc_frames_decoded =
|
|
|
|
videoTrackReport.framesDecoded || 0
|
|
|
|
client_metrics.rtc_frames_dropped =
|
|
|
|
videoTrackReport.framesDropped || 0
|
|
|
|
client_metrics.rtc_frames_received =
|
|
|
|
videoTrackReport.framesReceived || 0
|
|
|
|
client_metrics.rtc_frames_per_second =
|
|
|
|
videoTrackReport.framesPerSecond || 0
|
|
|
|
client_metrics.rtc_freeze_count =
|
|
|
|
videoTrackReport.freezeCount || 0
|
|
|
|
client_metrics.rtc_jitter_sec = videoTrackReport.jitter || 0.0
|
|
|
|
client_metrics.rtc_keyframes_decoded =
|
|
|
|
videoTrackReport.keyFramesDecoded || 0
|
|
|
|
client_metrics.rtc_total_freezes_duration_sec =
|
|
|
|
videoTrackReport.totalFreezesDuration || 0
|
|
|
|
} else if (videoTrackReport.type === 'transport') {
|
|
|
|
// videoTrackReport.bytesReceived,
|
|
|
|
// videoTrackReport.bytesSent,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
resolve(client_metrics)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// The app is eager to use the MediaStream; as soon as onNewTrack is
|
|
|
|
// called, the following sequence happens:
|
|
|
|
// EngineConnection.onNewTrack -> StoreState.setMediaStream ->
|
|
|
|
// Stream.tsx reacts to mediaStream change, setting a video element.
|
|
|
|
// We wait until connectionstatechange changes to "connected"
|
|
|
|
// to pass it to the rest of the application.
|
|
|
|
|
|
|
|
this.mediaStream = mediaStream
|
|
|
|
})
|
|
|
|
|
|
|
|
this.pc.addEventListener('datachannel', (event) => {
|
|
|
|
this.unreliableDataChannel = event.channel
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.DataChannelConnecting,
|
|
|
|
value: event.channel.label,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
this.unreliableDataChannel.addEventListener('open', (event) => {
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.DataChannelEstablished,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Everything is now connected.
|
|
|
|
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
|
|
|
|
|
|
|
this.onEngineConnectionOpen(this)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.unreliableDataChannel.addEventListener('close', (event) => {
|
|
|
|
this.disconnectAll()
|
2024-02-12 16:00:31 -05:00
|
|
|
this.finalizeIfAllConnectionsClosed()
|
2024-01-23 13:13:43 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
this.unreliableDataChannel.addEventListener('error', (event) => {
|
|
|
|
this.disconnectAll()
|
|
|
|
|
|
|
|
this.state = {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: EngineConnectionStateType.Disconnecting,
|
2024-01-23 13:13:43 -05:00
|
|
|
value: {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: DisconnectingType.Error,
|
|
|
|
value: {
|
|
|
|
error: new Error(event.toString()),
|
|
|
|
},
|
2024-01-23 13:13:43 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.WebSocketConnecting,
|
|
|
|
},
|
2023-08-30 13:14:52 -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-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('open', (event) => {
|
2024-01-23 13:13:43 -05:00
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.WebSocketEstablished,
|
|
|
|
},
|
2023-07-11 20:34:09 +10:00
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
// This is required for when KCMA is running stand-alone / within Tauri.
|
|
|
|
// Otherwise when run in a browser, the token is sent implicitly via
|
|
|
|
// the Cookie header.
|
|
|
|
if (this.token) {
|
|
|
|
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
2023-11-13 11:13:15 -08:00
|
|
|
}
|
2023-08-21 16:53:31 -04:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('close', (event) => {
|
2024-01-23 13:13:43 -05:00
|
|
|
this.disconnectAll()
|
2024-02-12 16:00:31 -05:00
|
|
|
this.finalizeIfAllConnectionsClosed()
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('error', (event) => {
|
2024-01-23 13:13:43 -05:00
|
|
|
this.disconnectAll()
|
|
|
|
|
|
|
|
this.state = {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: EngineConnectionStateType.Disconnecting,
|
2024-01-23 13:13:43 -05:00
|
|
|
value: {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: DisconnectingType.Error,
|
|
|
|
value: {
|
|
|
|
error: new Error(event.toString()),
|
|
|
|
},
|
2024-01-23 13:13:43 -05:00
|
|
|
},
|
|
|
|
}
|
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) {
|
2023-10-11 13:36:54 +11:00
|
|
|
const errorsString = message?.errors
|
|
|
|
?.map((error) => {
|
|
|
|
return ` - ${error.error_code}: ${error.message}`
|
|
|
|
})
|
|
|
|
.join('\n')
|
2023-08-25 00:16:37 -04:00
|
|
|
if (message.request_id) {
|
2024-02-11 12:59:00 +11:00
|
|
|
const artifactThatFailed =
|
|
|
|
engineCommandManager.artifactMap[message.request_id] ||
|
|
|
|
engineCommandManager.lastArtifactMap[message.request_id]
|
2023-10-11 13:36:54 +11:00
|
|
|
console.error(
|
2024-02-11 12:59:00 +11:00
|
|
|
`Error in response to request ${message.request_id}:\n${errorsString}
|
|
|
|
failed cmd type was ${artifactThatFailed?.commandType}`
|
2023-10-11 13:36:54 +11:00
|
|
|
)
|
2024-02-11 12:59:00 +11:00
|
|
|
console.log(artifactThatFailed)
|
2023-08-25 00:16:37 -04:00
|
|
|
} else {
|
2023-10-11 13:36:54 +11:00
|
|
|
console.error(`Error from server:\n${errorsString}`)
|
2023-08-25 00:16:37 -04:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let resp = message.resp
|
2024-01-23 13:13:43 -05:00
|
|
|
|
|
|
|
// If there's no body to the response, we can bail here.
|
|
|
|
// !resp.type is usually "pong" response for our "ping"
|
|
|
|
if (!resp || !resp.type) {
|
2023-08-21 16:53:31 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
switch (resp.type) {
|
|
|
|
case 'ice_server_info':
|
|
|
|
let ice_servers = resp.data?.ice_servers
|
2023-08-30 10:34:14 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
// Now that we have some ICE servers it makes sense
|
|
|
|
// to start initializing the RTCPeerConnection. RTCPeerConnection
|
|
|
|
// will begin the ICE process.
|
|
|
|
createPeerConnection()
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.PeerConnectionCreated,
|
|
|
|
},
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
// No ICE servers can be valid in a local dev. env.
|
|
|
|
if (ice_servers?.length === 0) {
|
|
|
|
console.warn('No ICE servers')
|
|
|
|
this.pc?.setConfiguration({})
|
|
|
|
} else {
|
|
|
|
// 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({
|
|
|
|
iceServers: ice_servers,
|
|
|
|
iceTransportPolicy: 'relay',
|
2023-06-23 09:56:37 +10:00
|
|
|
})
|
2024-01-23 13:13:43 -05:00
|
|
|
}
|
2023-08-30 10:34:14 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.ICEServersSet,
|
|
|
|
},
|
2023-08-30 13:14:52 -04:00
|
|
|
}
|
2023-08-30 10:34:14 -04:00
|
|
|
|
2024-01-23 13:13:43 -05: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-09-10 19:04:46 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
// Add a transceiver to our SDP offer
|
|
|
|
this.pc?.addTransceiver('video', {
|
|
|
|
direction: 'recvonly',
|
|
|
|
})
|
|
|
|
|
|
|
|
// Create a session description offer based on our local environment
|
|
|
|
// that we will send to the remote end. The remote will send back
|
|
|
|
// what it supports via sdp_answer.
|
|
|
|
this.pc
|
|
|
|
?.createOffer()
|
|
|
|
.then((offer: RTCSessionDescriptionInit) => {
|
|
|
|
console.log(offer)
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.SetLocalDescription,
|
|
|
|
},
|
2023-09-10 19:04:46 -04:00
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
return this.pc?.setLocalDescription(offer).then(() => {
|
|
|
|
this.send({
|
|
|
|
type: 'sdp_offer',
|
|
|
|
offer,
|
|
|
|
})
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.OfferedSdp,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
2023-08-30 10:34:14 -04:00
|
|
|
})
|
2024-01-23 13:13:43 -05:00
|
|
|
.catch((error: Error) => {
|
|
|
|
console.error(error)
|
|
|
|
// The local description is invalid, so there's no point continuing.
|
|
|
|
this.disconnectAll()
|
|
|
|
this.state = {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: EngineConnectionStateType.Disconnecting,
|
2024-01-23 13:13:43 -05:00
|
|
|
value: {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: DisconnectingType.Error,
|
|
|
|
value: {
|
|
|
|
error,
|
|
|
|
},
|
2024-01-23 13:13:43 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
|
|
|
break
|
2023-08-30 10:34:14 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
case 'sdp_answer':
|
|
|
|
let answer = resp.data?.answer
|
|
|
|
if (!answer || answer.type === 'unspecified') {
|
|
|
|
return
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.ReceivedSdp,
|
|
|
|
},
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
// As soon as this is set, RTCPeerConnection tries to
|
|
|
|
// establish a connection.
|
|
|
|
// @ts-ignore
|
|
|
|
// Have to ignore because dom.ts doesn't have the right type
|
|
|
|
void this.pc?.setRemoteDescription(answer)
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.SetRemoteDescription,
|
|
|
|
},
|
|
|
|
}
|
2023-08-21 16:53:31 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
this.state = {
|
|
|
|
type: EngineConnectionStateType.Connecting,
|
|
|
|
value: {
|
|
|
|
type: ConnectingType.WebRTCConnecting,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
break
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
case 'trickle_ice':
|
|
|
|
let candidate = resp.data?.candidate
|
|
|
|
console.log('trickle_ice: using this candidate: ', candidate)
|
|
|
|
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
|
|
|
break
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2024-01-23 13:13:43 -05:00
|
|
|
case 'metrics_request':
|
|
|
|
if (this.webrtcStatsCollector === undefined) {
|
|
|
|
// TODO: Error message here?
|
|
|
|
return
|
|
|
|
}
|
|
|
|
void this.webrtcStatsCollector().then((client_metrics) => {
|
|
|
|
this.send({
|
|
|
|
type: 'metrics_response',
|
|
|
|
metrics: client_metrics,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
break
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
|
|
|
|
2023-09-21 12:07:47 -04:00
|
|
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
|
|
|
if (this.failedConnTimeout) {
|
|
|
|
clearTimeout(this.failedConnTimeout)
|
|
|
|
this.failedConnTimeout = null
|
|
|
|
}
|
|
|
|
this.failedConnTimeout = setTimeout(() => {
|
|
|
|
if (this.isReady()) {
|
|
|
|
return
|
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
this.failedConnTimeout = null
|
|
|
|
this.state = {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: EngineConnectionStateType.Disconnecting,
|
2024-01-23 13:13:43 -05:00
|
|
|
value: {
|
2024-02-12 16:00:31 -05:00
|
|
|
type: DisconnectingType.Timeout,
|
2024-01-23 13:13:43 -05:00
|
|
|
},
|
|
|
|
}
|
2024-02-12 16:00:31 -05:00
|
|
|
this.disconnectAll()
|
|
|
|
this.finalizeIfAllConnectionsClosed()
|
2023-09-21 12:07:47 -04:00
|
|
|
}, connectionTimeoutMs)
|
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onConnectionStarted(this)
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
2023-09-15 17:14:58 -04:00
|
|
|
unreliableSend(message: object | string) {
|
|
|
|
// TODO(paultag): Add in logic to determine the connection state and
|
|
|
|
// take actions if needed?
|
|
|
|
this.unreliableDataChannel?.send(
|
|
|
|
typeof message === 'string' ? message : JSON.stringify(message)
|
|
|
|
)
|
|
|
|
}
|
2023-08-31 14:44:22 +10:00
|
|
|
send(message: object | string) {
|
2023-08-21 16:53:31 -04:00
|
|
|
// TODO(paultag): Add in logic to determine the connection state and
|
|
|
|
// take actions if needed?
|
2023-08-31 14:44:22 +10:00
|
|
|
this.websocket?.send(
|
|
|
|
typeof message === 'string' ? message : JSON.stringify(message)
|
|
|
|
)
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
2024-01-23 13:13:43 -05:00
|
|
|
disconnectAll() {
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket?.close()
|
2023-08-31 07:39:03 +10:00
|
|
|
this.unreliableDataChannel?.close()
|
2024-01-23 13:13:43 -05:00
|
|
|
this.pc?.close()
|
2024-02-12 16:00:31 -05:00
|
|
|
|
2023-09-10 19:04:46 -04:00
|
|
|
this.webrtcStatsCollector = undefined
|
2024-01-23 13:13:43 -05:00
|
|
|
}
|
2024-02-12 16:00:31 -05:00
|
|
|
finalizeIfAllConnectionsClosed() {
|
2024-01-23 13:13:43 -05:00
|
|
|
console.log(this.websocket, this.pc, this.unreliableDataChannel)
|
2024-02-12 16:00:31 -05:00
|
|
|
const allClosed =
|
|
|
|
this.websocket?.readyState === 3 &&
|
|
|
|
this.pc?.connectionState === 'closed' &&
|
|
|
|
this.unreliableDataChannel?.readyState === 'closed'
|
|
|
|
if (allClosed) {
|
|
|
|
this.state = { type: EngineConnectionStateType.Disconnected }
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-25 00:16:37 -04:00
|
|
|
export type EngineCommand = Models['WebSocketRequest_type']
|
2023-08-31 05:19:37 +10:00
|
|
|
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
|
|
|
|
2023-09-08 17:50:37 +10:00
|
|
|
type CommandTypes = Models['ModelingCmd_type']['type']
|
|
|
|
|
2023-08-31 07:39:03 +10:00
|
|
|
type UnreliableResponses = Extract<
|
2023-08-31 05:19:37 +10:00
|
|
|
Models['OkModelingCmdResponse_type'],
|
|
|
|
{ type: 'highlight_set_entity' }
|
|
|
|
>
|
2023-08-31 07:39:03 +10:00
|
|
|
interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
2023-08-31 05:19:37 +10:00
|
|
|
event: T
|
2023-08-31 07:39:03 +10:00
|
|
|
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
2023-08-31 05:19:37 +10:00
|
|
|
}
|
|
|
|
|
2024-03-02 08:20:50 +11:00
|
|
|
export interface Subscription<T extends ModelTypes> {
|
2023-08-31 05:19:37 +10:00
|
|
|
event: T
|
|
|
|
callback: (
|
|
|
|
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
|
|
|
|
) => void
|
|
|
|
}
|
2023-08-25 00:16:37 -04:00
|
|
|
|
2023-11-24 08:59:24 +11:00
|
|
|
export type CommandLog =
|
|
|
|
| {
|
|
|
|
type: 'send-modeling'
|
|
|
|
data: EngineCommand
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
type: 'send-scene'
|
|
|
|
data: EngineCommand
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
type: 'receive-reliable'
|
|
|
|
data: WebSocketResponse
|
|
|
|
id: string
|
|
|
|
cmd_type?: string
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
type: 'execution-done'
|
|
|
|
data: null
|
|
|
|
}
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
export class EngineCommandManager {
|
|
|
|
artifactMap: ArtifactMap = {}
|
2023-11-28 21:23:20 +11:00
|
|
|
lastArtifactMap: ArtifactMap = {}
|
2024-02-13 18:47:37 +11:00
|
|
|
sceneCommandArtifacts: ArtifactMap = {}
|
2024-02-11 12:59:00 +11:00
|
|
|
private getAst: () => Program = () => ({ start: 0, end: 0, body: [] } as any)
|
2023-08-18 16:16:16 -04:00
|
|
|
outSequence = 1
|
|
|
|
inSequence = 1
|
|
|
|
engineConnection?: EngineConnection
|
2024-02-17 07:04:24 +11:00
|
|
|
defaultPlanes: { xy: string; yz: string; xz: string } = {
|
|
|
|
xy: '',
|
|
|
|
yz: '',
|
|
|
|
xz: '',
|
|
|
|
}
|
2023-11-24 08:59:24 +11:00
|
|
|
_commandLogs: CommandLog[] = []
|
|
|
|
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
2023-09-29 12:41:58 -07:00
|
|
|
// Folks should realize that wait for ready does not get called _everytime_
|
|
|
|
// the connection resets and restarts, it only gets called the first time.
|
|
|
|
// Be careful what you put here.
|
2023-08-18 16:16:16 -04:00
|
|
|
private resolveReady = () => {}
|
2023-10-11 13:36:54 +11:00
|
|
|
waitForReady: Promise<void> = new Promise((resolve) => {
|
|
|
|
this.resolveReady = resolve
|
|
|
|
})
|
2023-08-31 05:19:37 +10:00
|
|
|
|
|
|
|
subscriptions: {
|
|
|
|
[event: string]: {
|
|
|
|
[localUnsubscribeId: string]: (a: any) => void
|
|
|
|
}
|
|
|
|
} = {} as any
|
2023-08-31 07:39:03 +10:00
|
|
|
unreliableSubscriptions: {
|
2023-08-31 05:19:37 +10:00
|
|
|
[event: string]: {
|
|
|
|
[localUnsubscribeId: string]: (a: any) => void
|
|
|
|
}
|
|
|
|
} = {} as any
|
2023-09-25 19:49:53 -07:00
|
|
|
|
2024-02-12 16:00:31 -05:00
|
|
|
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
|
|
|
[]
|
|
|
|
|
2023-09-25 19:49:53 -07:00
|
|
|
constructor() {
|
|
|
|
this.engineConnection = undefined
|
2024-02-11 12:59:00 +11:00
|
|
|
;(async () => {
|
|
|
|
// circular dependency needs one to be lazy loaded
|
|
|
|
const { kclManager } = await import('lang/KclSingleton')
|
|
|
|
this.getAst = () => kclManager.ast
|
|
|
|
})()
|
2023-09-25 19:49:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
start({
|
2023-08-18 16:16:16 -04:00
|
|
|
setMediaStream,
|
|
|
|
setIsStreamReady,
|
|
|
|
width,
|
|
|
|
height,
|
2023-09-29 12:41:58 -07:00
|
|
|
executeCode,
|
2023-08-18 16:16:16 -04:00
|
|
|
token,
|
|
|
|
}: {
|
|
|
|
setMediaStream: (stream: MediaStream) => void
|
|
|
|
setIsStreamReady: (isStreamReady: boolean) => void
|
|
|
|
width: number
|
|
|
|
height: number
|
2023-09-29 12:41:58 -07:00
|
|
|
executeCode: (code?: string, force?: boolean) => void
|
2023-08-18 16:16:16 -04:00
|
|
|
token?: string
|
|
|
|
}) {
|
2023-09-25 19:49:53 -07:00
|
|
|
if (width === 0 || height === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we already have an engine connection, just need to resize the stream.
|
|
|
|
if (this.engineConnection) {
|
2023-11-13 19:32:07 -05:00
|
|
|
this.handleResize({
|
|
|
|
streamWidth: width,
|
|
|
|
streamHeight: height,
|
|
|
|
})
|
2023-09-25 19:49:53 -07:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
|
|
|
this.engineConnection = new EngineConnection({
|
|
|
|
url,
|
|
|
|
token,
|
2024-02-12 16:00:31 -05:00
|
|
|
onConnectionStateChange: (state: EngineConnectionState) => {
|
|
|
|
for (let cb of this.callbacksEngineStateConnection) {
|
|
|
|
cb(state)
|
|
|
|
}
|
|
|
|
},
|
2023-08-24 23:46:45 +10:00
|
|
|
onEngineConnectionOpen: () => {
|
2023-09-29 12:41:58 -07:00
|
|
|
// Make the axis gizmo.
|
|
|
|
// We do this after the connection opened to avoid a race condition.
|
|
|
|
// Connected opened is the last thing that happens when the stream
|
|
|
|
// is ready.
|
|
|
|
// We also do this here because we want to ensure we create the gizmo
|
|
|
|
// and execute the code everytime the stream is restarted.
|
|
|
|
const gizmoId = uuidv4()
|
2024-01-23 13:13:43 -05:00
|
|
|
void this.sendSceneCommand({
|
2023-09-29 12:41:58 -07:00
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: gizmoId,
|
|
|
|
cmd: {
|
|
|
|
type: 'make_axes_gizmo',
|
|
|
|
clobber: false,
|
|
|
|
// If true, axes gizmo will be placed in the corner of the screen.
|
|
|
|
// If false, it will be placed at the origin of the scene.
|
|
|
|
gizmo_mode: true,
|
|
|
|
},
|
|
|
|
})
|
2024-02-26 19:53:44 +11:00
|
|
|
sceneInfra.camControls.onCameraChange()
|
2024-03-02 08:20:50 +11:00
|
|
|
this.sendSceneCommand({
|
|
|
|
// CameraControls subscribes to default_camera_get_settings response events
|
|
|
|
// firing this at connection ensure the camera's are synced initially
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'default_camera_get_settings',
|
|
|
|
},
|
|
|
|
})
|
2023-09-29 12:41:58 -07:00
|
|
|
|
2024-02-17 07:04:24 +11:00
|
|
|
this.initPlanes().then(() => {
|
2024-02-26 21:02:33 +11:00
|
|
|
this.resolveReady()
|
|
|
|
setIsStreamReady(true)
|
2024-02-17 07:04:24 +11:00
|
|
|
executeCode(undefined, 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-31 07:39:03 +10:00
|
|
|
let unreliableDataChannel = event.channel
|
|
|
|
|
|
|
|
unreliableDataChannel.addEventListener('message', (event) => {
|
|
|
|
const result: UnreliableResponses = JSON.parse(event.data)
|
|
|
|
Object.values(
|
|
|
|
this.unreliableSubscriptions[result.type] || {}
|
|
|
|
).forEach(
|
|
|
|
// TODO: There is only one response that uses the unreliable channel atm,
|
2023-08-31 05:19:37 +10:00
|
|
|
// highlight_set_entity, if there are more it's likely they will all have the same
|
|
|
|
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
|
2023-08-31 07:39:03 +10:00
|
|
|
// per unreliable subscription.
|
2023-08-31 05:19:37 +10:00
|
|
|
(callback) => {
|
|
|
|
if (
|
|
|
|
result?.data?.sequence &&
|
|
|
|
result?.data.sequence > this.inSequence &&
|
|
|
|
result.type === 'highlight_set_entity'
|
|
|
|
) {
|
|
|
|
this.inSequence = result.data.sequence
|
|
|
|
callback(result)
|
|
|
|
}
|
|
|
|
}
|
2023-08-25 00:16:37 -04:00
|
|
|
)
|
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.
|
2024-01-23 13:13:43 -05:00
|
|
|
void exportSave(event.data)
|
2023-08-18 16:16:16 -04:00
|
|
|
} 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-10-05 00:18:50 -05:00
|
|
|
} else if (
|
|
|
|
!message.success &&
|
|
|
|
message.request_id &&
|
|
|
|
this.artifactMap[message.request_id]
|
|
|
|
) {
|
2023-09-21 14:32:47 +10:00
|
|
|
this.handleFailedModelingCommand(message)
|
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')
|
2023-08-31 22:57:58 -04:00
|
|
|
// this.engineConnection?.close()
|
|
|
|
// this.engineConnection?.connect()
|
2023-08-22 20:07:39 -04:00
|
|
|
})
|
|
|
|
|
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-09-25 19:49:53 -07:00
|
|
|
handleResize({
|
|
|
|
streamWidth,
|
|
|
|
streamHeight,
|
|
|
|
}: {
|
|
|
|
streamWidth: number
|
|
|
|
streamHeight: number
|
|
|
|
}) {
|
|
|
|
if (!this.engineConnection?.isReady()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const resizeCmd: EngineCommand = {
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'reconfigure_stream',
|
|
|
|
width: streamWidth,
|
|
|
|
height: streamHeight,
|
|
|
|
fps: 60,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
this.engineConnection?.send(resizeCmd)
|
|
|
|
}
|
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-11-28 21:23:20 +11:00
|
|
|
const command = this.artifactMap[id]
|
2024-02-13 18:47:37 +11:00
|
|
|
const sceneCommand = this.sceneCommandArtifacts[id]
|
2023-11-24 08:59:24 +11:00
|
|
|
this.addCommandLog({
|
|
|
|
type: 'receive-reliable',
|
|
|
|
data: message,
|
|
|
|
id,
|
2023-11-28 21:23:20 +11:00
|
|
|
cmd_type: command?.commandType || this.lastArtifactMap[id]?.commandType,
|
2023-11-24 08:59:24 +11:00
|
|
|
})
|
2023-08-31 05:19:37 +10:00
|
|
|
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
|
|
|
(callback) => callback(modelingResponse)
|
|
|
|
)
|
2023-08-21 16:53:31 -04:00
|
|
|
|
|
|
|
if (command && command.type === 'pending') {
|
|
|
|
const resolve = command.resolve
|
2024-02-11 15:08:54 -08:00
|
|
|
const artifact = {
|
2023-08-21 16:53:31 -04:00
|
|
|
type: 'result',
|
2023-09-08 17:50:37 +10:00
|
|
|
range: command.range,
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode: command.pathToNode,
|
2023-09-08 17:50:37 +10:00
|
|
|
commandType: command.commandType,
|
|
|
|
parentId: command.parentId ? command.parentId : undefined,
|
2023-08-25 00:16:37 -04:00
|
|
|
data: modelingResponse,
|
2023-09-17 21:57:43 -07:00
|
|
|
raw: message,
|
2024-02-11 15:08:54 -08:00
|
|
|
} as const
|
|
|
|
this.artifactMap[id] = artifact
|
2024-02-15 07:24:54 +11:00
|
|
|
if (
|
|
|
|
command.commandType === 'entity_linear_pattern' ||
|
|
|
|
command.commandType === 'entity_circular_pattern'
|
|
|
|
) {
|
2024-02-11 15:08:54 -08:00
|
|
|
const entities = (modelingResponse as any)?.data?.entity_ids
|
|
|
|
entities?.forEach((entity: string) => {
|
|
|
|
this.artifactMap[entity] = artifact
|
|
|
|
})
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
resolve({
|
|
|
|
id,
|
2023-09-08 17:50:37 +10:00
|
|
|
commandType: command.commandType,
|
|
|
|
range: command.range,
|
2023-08-31 05:19:37 +10:00
|
|
|
data: modelingResponse,
|
2023-09-17 21:57:43 -07:00
|
|
|
raw: message,
|
2023-08-21 16:53:31 -04:00
|
|
|
})
|
2024-02-13 18:47:37 +11:00
|
|
|
} 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: message,
|
|
|
|
} as const
|
|
|
|
this.sceneCommandArtifacts[id] = artifact
|
|
|
|
resolve({
|
|
|
|
id,
|
|
|
|
commandType: sceneCommand.commandType,
|
|
|
|
range: sceneCommand.range,
|
|
|
|
data: modelingResponse,
|
|
|
|
})
|
|
|
|
} else if (command) {
|
2023-08-21 16:53:31 -04:00
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'result',
|
2023-09-08 17:50:37 +10:00
|
|
|
commandType: command?.commandType,
|
|
|
|
range: command?.range,
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode: command?.pathToNode,
|
2023-08-25 00:16:37 -04:00
|
|
|
data: modelingResponse,
|
2023-09-17 21:57:43 -07:00
|
|
|
raw: message,
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
2024-02-13 18:47:37 +11:00
|
|
|
} else {
|
|
|
|
this.sceneCommandArtifacts[id] = {
|
|
|
|
type: 'result',
|
|
|
|
commandType: sceneCommand?.commandType,
|
|
|
|
range: sceneCommand?.range,
|
|
|
|
pathToNode: sceneCommand?.pathToNode,
|
|
|
|
data: modelingResponse,
|
|
|
|
raw: message,
|
|
|
|
}
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
}
|
2023-09-21 14:32:47 +10:00
|
|
|
handleFailedModelingCommand({
|
|
|
|
request_id,
|
|
|
|
errors,
|
|
|
|
}: Models['FailureWebSocketResponse_type']) {
|
|
|
|
const id = request_id
|
|
|
|
if (!id) return
|
|
|
|
const command = this.artifactMap[id]
|
|
|
|
if (command && command.type === 'pending') {
|
|
|
|
const resolve = command.resolve
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'failed',
|
|
|
|
range: command.range,
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode: command.pathToNode,
|
2023-09-21 14:32:47 +10:00
|
|
|
commandType: command.commandType,
|
|
|
|
parentId: command.parentId ? command.parentId : undefined,
|
|
|
|
errors,
|
|
|
|
}
|
|
|
|
resolve({
|
|
|
|
id,
|
|
|
|
commandType: command.commandType,
|
|
|
|
range: command.range,
|
|
|
|
errors,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'failed',
|
|
|
|
range: command.range,
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode: command.pathToNode,
|
2023-09-21 14:32:47 +10:00
|
|
|
commandType: command.commandType,
|
|
|
|
parentId: command.parentId ? command.parentId : undefined,
|
|
|
|
errors,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-23 09:56:37 +10:00
|
|
|
tearDown() {
|
2023-09-21 12:07:47 -04:00
|
|
|
this.engineConnection?.tearDown()
|
2023-06-23 09:56:37 +10:00
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
startNewSession() {
|
2023-11-28 21:23:20 +11:00
|
|
|
this.lastArtifactMap = this.artifactMap
|
2023-06-22 16:43:33 +10:00
|
|
|
this.artifactMap = {}
|
|
|
|
}
|
2023-08-31 05:19:37 +10:00
|
|
|
subscribeTo<T extends ModelTypes>({
|
|
|
|
event,
|
|
|
|
callback,
|
|
|
|
}: Subscription<T>): () => void {
|
|
|
|
const localUnsubscribeId = uuidv4()
|
|
|
|
const otherEventCallbacks = this.subscriptions[event]
|
|
|
|
if (otherEventCallbacks) {
|
|
|
|
otherEventCallbacks[localUnsubscribeId] = callback
|
|
|
|
} else {
|
|
|
|
this.subscriptions[event] = {
|
|
|
|
[localUnsubscribeId]: callback,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return () => this.unSubscribeTo(event, localUnsubscribeId)
|
|
|
|
}
|
|
|
|
private unSubscribeTo(event: ModelTypes, id: string) {
|
|
|
|
delete this.subscriptions[event][id]
|
|
|
|
}
|
2023-08-31 07:39:03 +10:00
|
|
|
subscribeToUnreliable<T extends UnreliableResponses['type']>({
|
2023-08-31 05:19:37 +10:00
|
|
|
event,
|
|
|
|
callback,
|
2023-08-31 07:39:03 +10:00
|
|
|
}: UnreliableSubscription<T>): () => void {
|
2023-08-31 05:19:37 +10:00
|
|
|
const localUnsubscribeId = uuidv4()
|
2023-08-31 07:39:03 +10:00
|
|
|
const otherEventCallbacks = this.unreliableSubscriptions[event]
|
2023-08-31 05:19:37 +10:00
|
|
|
if (otherEventCallbacks) {
|
|
|
|
otherEventCallbacks[localUnsubscribeId] = callback
|
|
|
|
} else {
|
2023-08-31 07:39:03 +10:00
|
|
|
this.unreliableSubscriptions[event] = {
|
2023-08-31 05:19:37 +10:00
|
|
|
[localUnsubscribeId]: callback,
|
|
|
|
}
|
|
|
|
}
|
2023-08-31 07:39:03 +10:00
|
|
|
return () => this.unSubscribeToUnreliable(event, localUnsubscribeId)
|
2023-08-31 05:19:37 +10:00
|
|
|
}
|
2023-08-31 07:39:03 +10:00
|
|
|
private unSubscribeToUnreliable(
|
|
|
|
event: UnreliableResponses['type'],
|
|
|
|
id: string
|
|
|
|
) {
|
|
|
|
delete this.unreliableSubscriptions[event][id]
|
2023-08-31 05:19:37 +10:00
|
|
|
}
|
2024-02-12 16:00:31 -05:00
|
|
|
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
|
|
|
|
this.callbacksEngineStateConnection.push(callback)
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
endSession() {
|
2023-09-08 17:50:37 +10:00
|
|
|
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
2023-11-01 17:34:54 -05:00
|
|
|
// we need to loop over them each individually because if the engine doesn't recognise a single
|
2023-09-08 17:50:37 +10:00
|
|
|
// id the whole command fails.
|
2023-11-06 11:49:13 +11:00
|
|
|
const artifactsToDelete: any = {}
|
2023-09-08 17:50:37 +10:00
|
|
|
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
|
|
|
|
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
|
|
|
|
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
|
|
|
|
// however all of the segments in the path are its children so there don't need to be deleted.
|
|
|
|
// this fact is very opaque in the api and docs (as to what should can be deleted).
|
|
|
|
// Using an array is the list is likely to grow.
|
|
|
|
'start_path',
|
2024-02-11 15:08:54 -08:00
|
|
|
'entity_linear_pattern',
|
2024-02-15 07:24:54 +11:00
|
|
|
'entity_circular_pattern',
|
2023-09-08 17:50:37 +10:00
|
|
|
]
|
2024-02-11 15:08:54 -08:00
|
|
|
if (artifactTypesToDelete.includes(artifact.commandType)) {
|
|
|
|
artifactsToDelete[id] = artifact
|
|
|
|
}
|
2024-02-15 07:24:54 +11:00
|
|
|
if (artifact.commandType === 'import_files') {
|
|
|
|
// TODO why is this handled differently from other artifacts, i.e. why does it not use the id from the
|
|
|
|
// modeling command? We're having to do special clean up for this one special object.
|
|
|
|
artifactsToDelete[(artifact as any)?.data?.data?.object_id] = artifact
|
|
|
|
}
|
2023-11-06 11:49:13 +11:00
|
|
|
})
|
|
|
|
Object.keys(artifactsToDelete).forEach((id) => {
|
2024-02-11 15:08:54 -08:00
|
|
|
const deleteCmd: EngineCommand = {
|
2023-09-08 17:50:37 +10:00
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'remove_scene_objects',
|
|
|
|
object_ids: [id],
|
|
|
|
},
|
|
|
|
}
|
2024-02-11 15:08:54 -08:00
|
|
|
this.engineConnection?.send(deleteCmd)
|
2023-09-08 17:50:37 +10:00
|
|
|
})
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-11-24 08:59:24 +11:00
|
|
|
addCommandLog(message: CommandLog) {
|
|
|
|
if (this._commandLogs.length > 500) {
|
|
|
|
this._commandLogs.shift()
|
|
|
|
}
|
|
|
|
this._commandLogs.push(message)
|
|
|
|
|
|
|
|
this._commandLogCallBack([...this._commandLogs])
|
|
|
|
}
|
|
|
|
clearCommandLogs() {
|
|
|
|
this._commandLogs = []
|
|
|
|
this._commandLogCallBack(this._commandLogs)
|
|
|
|
}
|
|
|
|
registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
|
|
|
|
this._commandLogCallBack = callback
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
sendSceneCommand(
|
|
|
|
command: EngineCommand,
|
|
|
|
forceWebsocket = false
|
|
|
|
): Promise<any> {
|
2023-09-25 19:49:53 -07:00
|
|
|
if (this.engineConnection === undefined) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
2023-09-29 12:41:58 -07:00
|
|
|
|
|
|
|
if (!this.engineConnection?.isReady()) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
2023-11-24 08:59:24 +11:00
|
|
|
if (
|
|
|
|
!(
|
|
|
|
command.type === 'modeling_cmd_req' &&
|
|
|
|
(command.cmd.type === 'highlight_set_entity' ||
|
2023-12-18 05:10:31 +11:00
|
|
|
command.cmd.type === 'mouse_move' ||
|
2024-02-11 12:59:00 +11:00
|
|
|
command.cmd.type === 'camera_drag_move' ||
|
|
|
|
command.cmd.type === 'default_camera_look_at' ||
|
|
|
|
command.cmd.type === ('default_camera_perspective_settings' as any))
|
2023-11-24 08:59:24 +11:00
|
|
|
)
|
|
|
|
) {
|
2023-12-18 05:10:31 +11:00
|
|
|
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
2023-11-24 08:59:24 +11:00
|
|
|
this.addCommandLog({
|
|
|
|
type: 'send-scene',
|
|
|
|
data: command,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-13 08:36:47 +10:00
|
|
|
if (
|
|
|
|
command.type === 'modeling_cmd_req' &&
|
|
|
|
command.cmd.type !== lastMessage
|
|
|
|
) {
|
|
|
|
console.log('sending command', command.cmd.type)
|
|
|
|
lastMessage = command.cmd.type
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
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()
|
|
|
|
}
|
2023-08-31 05:19:37 +10:00
|
|
|
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
2023-08-02 15:41:59 +10:00
|
|
|
const cmd = command.cmd
|
2023-08-18 16:16:16 -04:00
|
|
|
if (
|
2023-09-13 17:42:42 +10:00
|
|
|
(cmd.type === 'camera_drag_move' ||
|
2024-02-11 12:59:00 +11:00
|
|
|
cmd.type === 'handle_mouse_drag_move' ||
|
|
|
|
cmd.type === 'default_camera_look_at' ||
|
|
|
|
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
|
|
|
this.engineConnection?.unreliableDataChannel &&
|
|
|
|
!forceWebsocket
|
2023-08-18 16:16:16 -04:00
|
|
|
) {
|
2024-02-11 12:59:00 +11:00
|
|
|
;(cmd as any).sequence = this.outSequence
|
2023-08-09 20:49:10 +10:00
|
|
|
this.outSequence++
|
2023-09-15 17:14:58 -04:00
|
|
|
this.engineConnection?.unreliableSend(command)
|
2023-08-31 05:19:37 +10:00
|
|
|
return Promise.resolve()
|
2023-08-18 16:16:16 -04:00
|
|
|
} else if (
|
|
|
|
cmd.type === 'highlight_set_entity' &&
|
2023-08-31 07:39:03 +10:00
|
|
|
this.engineConnection?.unreliableDataChannel
|
2023-08-18 16:16:16 -04:00
|
|
|
) {
|
2023-08-09 20:49:10 +10:00
|
|
|
cmd.sequence = this.outSequence
|
|
|
|
this.outSequence++
|
2023-09-15 17:14:58 -04:00
|
|
|
this.engineConnection?.unreliableSend(command)
|
2023-08-31 05:19:37 +10:00
|
|
|
return Promise.resolve()
|
2023-09-08 17:50:37 +10:00
|
|
|
} else if (
|
|
|
|
cmd.type === 'mouse_move' &&
|
|
|
|
this.engineConnection.unreliableDataChannel
|
|
|
|
) {
|
|
|
|
cmd.sequence = this.outSequence
|
|
|
|
this.outSequence++
|
2023-09-15 17:14:58 -04:00
|
|
|
this.engineConnection?.unreliableSend(command)
|
2023-09-08 17:50:37 +10:00
|
|
|
return Promise.resolve()
|
2023-06-23 09:56:37 +10:00
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
if (
|
|
|
|
command.cmd.type === 'default_camera_look_at' ||
|
|
|
|
command.cmd.type === ('default_camera_perspective_settings' as any)
|
|
|
|
) {
|
|
|
|
;(cmd as any).sequence = this.outSequence++
|
|
|
|
}
|
2023-08-31 05:19:37 +10:00
|
|
|
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
2023-08-21 16:53:31 -04:00
|
|
|
this.engineConnection?.send(command)
|
2024-02-13 18:47:37 +11:00
|
|
|
return this.handlePendingSceneCommand(command.cmd_id, command.cmd)
|
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,
|
2024-02-11 12:59:00 +11:00
|
|
|
ast,
|
2023-06-22 16:43:33 +10:00
|
|
|
}: {
|
|
|
|
id: string
|
|
|
|
range: SourceRange
|
2023-08-31 14:44:22 +10:00
|
|
|
command: EngineCommand | string
|
2024-02-11 12:59:00 +11:00
|
|
|
ast: Program
|
2023-06-22 16:43:33 +10:00
|
|
|
}): Promise<any> {
|
2023-09-25 19:49:53 -07:00
|
|
|
if (this.engineConnection === undefined) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
if (!this.engineConnection?.isReady()) {
|
2023-08-31 05:19:37 +10:00
|
|
|
return Promise.resolve()
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-11-24 08:59:24 +11:00
|
|
|
if (typeof command !== 'string') {
|
|
|
|
this.addCommandLog({
|
|
|
|
type: 'send-modeling',
|
|
|
|
data: command,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.addCommandLog({
|
|
|
|
type: 'send-modeling',
|
|
|
|
data: JSON.parse(command),
|
|
|
|
})
|
|
|
|
}
|
2023-08-21 16:53:31 -04:00
|
|
|
this.engineConnection?.send(command)
|
2023-09-08 17:50:37 +10:00
|
|
|
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
2024-02-11 12:59:00 +11:00
|
|
|
return this.handlePendingCommand(id, command?.cmd, ast, range)
|
2023-09-08 17:50:37 +10:00
|
|
|
} else if (typeof command === 'string') {
|
|
|
|
const parseCommand: EngineCommand = JSON.parse(command)
|
|
|
|
if (parseCommand.type === 'modeling_cmd_req')
|
2024-02-11 12:59:00 +11:00
|
|
|
return this.handlePendingCommand(id, parseCommand?.cmd, ast, range)
|
2023-09-08 17:50:37 +10:00
|
|
|
}
|
2023-11-01 07:39:31 -04:00
|
|
|
throw Error('shouldnt reach here')
|
2023-08-31 05:19:37 +10:00
|
|
|
}
|
2024-02-13 18:47:37 +11:00
|
|
|
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 getParentId = (): string | undefined => {
|
|
|
|
if (command.type === 'extend_path') {
|
|
|
|
return command.path
|
|
|
|
}
|
|
|
|
// TODO handle other commands that have a parent
|
|
|
|
}
|
|
|
|
const pathToNode = ast
|
|
|
|
? getNodePathFromSourceRange(ast, range || [0, 0])
|
|
|
|
: []
|
|
|
|
this.sceneCommandArtifacts[id] = {
|
|
|
|
range: range || [0, 0],
|
|
|
|
pathToNode,
|
|
|
|
type: 'pending',
|
|
|
|
commandType: command.type,
|
|
|
|
parentId: getParentId(),
|
|
|
|
promise,
|
|
|
|
resolve,
|
|
|
|
}
|
|
|
|
return promise
|
|
|
|
}
|
2023-09-08 17:50:37 +10:00
|
|
|
handlePendingCommand(
|
|
|
|
id: string,
|
|
|
|
command: Models['ModelingCmd_type'],
|
2024-02-11 12:59:00 +11:00
|
|
|
ast?: Program,
|
2023-09-08 17:50:37 +10:00
|
|
|
range?: SourceRange
|
|
|
|
) {
|
2023-06-22 16:43:33 +10:00
|
|
|
let resolve: (val: any) => void = () => {}
|
|
|
|
const promise = new Promise((_resolve, reject) => {
|
|
|
|
resolve = _resolve
|
|
|
|
})
|
2023-09-08 17:50:37 +10:00
|
|
|
const getParentId = (): string | undefined => {
|
|
|
|
if (command.type === 'extend_path') {
|
|
|
|
return command.path
|
|
|
|
}
|
|
|
|
// TODO handle other commands that have a parent
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
const pathToNode = ast
|
|
|
|
? getNodePathFromSourceRange(ast, range || [0, 0])
|
|
|
|
: []
|
2023-06-22 16:43:33 +10:00
|
|
|
this.artifactMap[id] = {
|
2023-09-08 17:50:37 +10:00
|
|
|
range: range || [0, 0],
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode,
|
2023-06-22 16:43:33 +10:00
|
|
|
type: 'pending',
|
2023-09-08 17:50:37 +10:00
|
|
|
commandType: command.type,
|
|
|
|
parentId: getParentId(),
|
2023-06-22 16:43:33 +10:00
|
|
|
promise,
|
|
|
|
resolve,
|
|
|
|
}
|
|
|
|
return promise
|
|
|
|
}
|
2023-08-24 15:34:51 -07:00
|
|
|
sendModelingCommandFromWasm(
|
|
|
|
id: string,
|
|
|
|
rangeStr: string,
|
|
|
|
commandStr: string
|
|
|
|
): Promise<any> {
|
2023-09-25 19:49:53 -07:00
|
|
|
if (this.engineConnection === undefined) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
2024-03-11 17:50:31 -07:00
|
|
|
if (!this.engineConnection?.isReady()) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
2023-08-24 15:34:51 -07:00
|
|
|
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 range: SourceRange = JSON.parse(rangeStr)
|
|
|
|
|
2023-09-17 21:57:43 -07:00
|
|
|
// We only care about the modeling command response.
|
2024-02-11 12:59:00 +11:00
|
|
|
return this.sendModelingCommand({
|
|
|
|
id,
|
|
|
|
range,
|
|
|
|
command: commandStr,
|
|
|
|
ast: this.getAst(),
|
|
|
|
}).then(({ raw }) => JSON.stringify(raw))
|
2023-08-24 15:34:51 -07:00
|
|
|
}
|
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
|
2023-09-21 14:32:47 +10:00
|
|
|
} else if (command.type === 'failed') {
|
|
|
|
return Promise.resolve(command.errors)
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
|
|
|
return command.promise
|
|
|
|
}
|
2023-10-14 03:47:46 +11:00
|
|
|
async waitForAllCommands(): Promise<{
|
2023-06-22 16:43:33 +10:00
|
|
|
artifactMap: ArtifactMap
|
|
|
|
}> {
|
|
|
|
const pendingCommands = Object.values(this.artifactMap).filter(
|
|
|
|
({ type }) => type === 'pending'
|
|
|
|
) as PendingCommand[]
|
|
|
|
const proms = pendingCommands.map(({ promise }) => promise)
|
|
|
|
await Promise.all(proms)
|
2023-09-14 13:49:59 +10:00
|
|
|
|
2023-06-22 16:43:33 +10:00
|
|
|
return {
|
|
|
|
artifactMap: this.artifactMap,
|
|
|
|
}
|
|
|
|
}
|
2024-02-17 07:04:24 +11:00
|
|
|
private async initPlanes() {
|
|
|
|
const [xy, yz, xz] = [
|
|
|
|
await this.createPlane({
|
|
|
|
x_axis: { x: 1, y: 0, z: 0 },
|
|
|
|
y_axis: { x: 0, y: 1, z: 0 },
|
|
|
|
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
|
|
|
}),
|
|
|
|
await this.createPlane({
|
|
|
|
x_axis: { x: 0, y: 1, z: 0 },
|
|
|
|
y_axis: { x: 0, y: 0, z: 1 },
|
|
|
|
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
|
|
|
}),
|
|
|
|
await this.createPlane({
|
|
|
|
x_axis: { x: 1, y: 0, z: 0 },
|
|
|
|
y_axis: { x: 0, y: 0, z: 1 },
|
|
|
|
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
this.defaultPlanes = { xy, yz, xz }
|
|
|
|
|
|
|
|
this.subscribeTo({
|
|
|
|
event: 'select_with_point',
|
|
|
|
callback: ({ data }) => {
|
|
|
|
if (!data?.entity_id) return
|
|
|
|
if (
|
|
|
|
![
|
|
|
|
this.defaultPlanes.xy,
|
|
|
|
this.defaultPlanes.yz,
|
|
|
|
this.defaultPlanes.xz,
|
|
|
|
].includes(data.entity_id)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
this.onPlaneSelectCallback(data.entity_id)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
planesInitialized(): boolean {
|
|
|
|
return (
|
|
|
|
this.defaultPlanes.xy !== '' &&
|
|
|
|
this.defaultPlanes.yz !== '' &&
|
|
|
|
this.defaultPlanes.xz !== ''
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
onPlaneSelectCallback = (id: string) => {}
|
|
|
|
onPlaneSelected(callback: (id: string) => void) {
|
|
|
|
this.onPlaneSelectCallback = callback
|
|
|
|
}
|
|
|
|
|
|
|
|
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
|
|
|
|
return await this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'object_visible',
|
|
|
|
object_id: id,
|
|
|
|
hidden: hidden,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private async createPlane({
|
|
|
|
x_axis,
|
|
|
|
y_axis,
|
|
|
|
color,
|
|
|
|
}: {
|
|
|
|
x_axis: Models['Point3d_type']
|
|
|
|
y_axis: Models['Point3d_type']
|
|
|
|
color: Models['Color_type']
|
|
|
|
}): Promise<string> {
|
|
|
|
const planeId = uuidv4()
|
|
|
|
await this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd: {
|
|
|
|
type: 'make_plane',
|
|
|
|
size: 100,
|
|
|
|
origin: { x: 0, y: 0, z: 0 },
|
|
|
|
x_axis,
|
|
|
|
y_axis,
|
|
|
|
clobber: false,
|
|
|
|
hide: true,
|
|
|
|
},
|
|
|
|
cmd_id: planeId,
|
|
|
|
})
|
|
|
|
await this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd: {
|
|
|
|
type: 'plane_set_color',
|
|
|
|
plane_id: planeId,
|
|
|
|
color,
|
|
|
|
},
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
})
|
|
|
|
await this.setPlaneHidden(planeId, true)
|
|
|
|
return planeId
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-09-25 19:49:53 -07:00
|
|
|
|
|
|
|
export const engineCommandManager = new EngineCommandManager()
|