ArtifactGraph reThink (PART 3) (#3140)

* adjust engine connection to opt out of webRTC connection

* refactor start and test setup

* add env to unit test

* spell config update

* fix beforeAll order bug

* initial integration of new artifact map with tests passing

* remove old artifact map and clean up

* graph artifact map

* have graph commited

* have graph commited

* remove bad file

* install playwright

* fmt

* commit permissions

* typo

* flesh out tests more

* Look at this (photo)Graph *in the voice of Nickelback*

* multi highlight

* redo image logic

* add in solid 2d data into artifactMap

* fix snapshots

* stabiles graph images

* Look at this (photo)Graph *in the voice of Nickelback*

* tweak tests

* rename blend to edgeCut

* Look at this (photo)Graph *in the voice of Nickelback*

* fix playw tests

* start of artifact map rename to graph

* rename file

* rename test

* rename clearup

* comments

* docs

* docs proof read

* few tweaks here and there

* typos

* delete get parent logic

* nit, combine if statements

* remove unused param

* fix silly test bug

* rename surfId to sufaceId

* rename types

* update comments

* add comment

* add extra check

* Look at this (photo)Graph *in the voice of Nickelback*

* pull out merge artifact function

* update comments

* fix test

* fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kurt Hutten
2024-08-03 18:08:51 +10:00
committed by GitHub
parent 7bf6bc3048
commit e5bec2140e
37 changed files with 2285 additions and 587 deletions

View File

@ -6,12 +6,12 @@ import { deferExecution, uuidv4 } from 'lib/utils'
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
ArtifactMap,
ArtifactGraph,
EngineCommand,
OrderedCommand,
ResponseMap,
createArtifactMap,
} from 'lang/std/artifactMap'
createArtifactGraph,
} from 'lang/std/artifactGraph'
import { useModelingContext } from 'hooks/useModelingContext'
// TODO(paultag): This ought to be tweakable.
@ -286,8 +286,6 @@ class EngineConnection extends EventTarget {
)
}
private failedConnTimeout: IsomorphicTimeout | null
readonly url: string
private readonly token?: string
@ -312,7 +310,6 @@ class EngineConnection extends EventTarget {
this.engineCommandManager = engineCommandManager
this.url = url
this.token = token
this.failedConnTimeout = null
this.pingPongSpan = { ping: undefined, pong: undefined }
@ -451,9 +448,11 @@ class EngineConnection extends EventTarget {
}
const createPeerConnection = () => {
this.pc = new RTCPeerConnection({
bundlePolicy: 'max-bundle',
})
if (!this.engineCommandManager.disableWebRTC) {
this.pc = new RTCPeerConnection({
bundlePolicy: 'max-bundle',
})
}
// Other parts of the application expect pc to be initialized when firing.
this.dispatchEvent(
@ -465,7 +464,7 @@ class EngineConnection extends EventTarget {
// 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.pc?.createDataChannel?.(DATACHANNEL_NAME_UMC)
this.state = {
type: EngineConnectionStateType.Connecting,
@ -498,7 +497,7 @@ class EngineConnection extends EventTarget {
},
})
}
this.pc.addEventListener('icecandidate', this.onIceCandidate)
this.pc?.addEventListener?.('icecandidate', this.onIceCandidate)
this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
@ -506,7 +505,7 @@ class EngineConnection extends EventTarget {
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
}
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
this.pc?.addEventListener?.('icecandidateerror', this.onIceCandidateError)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type...
@ -540,7 +539,7 @@ class EngineConnection extends EventTarget {
break
}
}
this.pc.addEventListener(
this.pc?.addEventListener?.(
'connectionstatechange',
this.onConnectionStateChange
)
@ -630,7 +629,7 @@ class EngineConnection extends EventTarget {
this.mediaStream = mediaStream
}
this.pc.addEventListener('track', this.onTrack)
this.pc?.addEventListener?.('track', this.onTrack)
this.onDataChannel = (event) => {
this.unreliableDataChannel = event.channel
@ -721,7 +720,7 @@ class EngineConnection extends EventTarget {
this.onDataChannelMessage
)
}
this.pc.addEventListener('datachannel', this.onDataChannel)
this.pc?.addEventListener?.('datachannel', this.onDataChannel)
}
const createWebSocketConnection = () => {
@ -756,6 +755,11 @@ class EngineConnection extends EventTarget {
// Send an initial ping
this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date()
if (this.engineCommandManager.disableWebRTC) {
this.engineCommandManager
.initPlanes()
.then(() => this.engineCommandManager.resolveReady())
}
}
this.websocket.addEventListener('open', this.onWebSocketOpen)
@ -803,7 +807,7 @@ class EngineConnection extends EventTarget {
.join('\n')
if (message.request_id) {
const artifactThatFailed =
this.engineCommandManager.artifactMap[message.request_id]
this.engineCommandManager.artifactGraph.get(message.request_id)
console.error(
`Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.type}`
@ -1089,8 +1093,10 @@ export enum EngineCommandManagerEvents {
* of those commands. It also sets up and tears down the connection to the Engine
* through the {@link EngineConnection} class.
*
* It also maintains an {@link artifactMap} that keeps track of the state of each
* command, and the artifacts that have been generated by those commands.
* As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response.
*
* Also all commands that are sent are kept track of in {@link orderedCommands} and their responses are kept in {@link responseMap}
* Both of these data structures are used to process the {@link artifactGraph}.
*/
interface PendingMessage {
@ -1103,17 +1109,10 @@ interface PendingMessage {
}
export class EngineCommandManager extends EventTarget {
/**
* The artifactMap is a client-side representation of the commands that have been sent
* to the server-side geometry engine, and the state of their resulting artifacts.
*
* It is used to keep track of the state of each command, which can fail, succeed, or be
* pending.
*
* It is also used to keep track of our client's understanding of what is in the engine scene
* so that we can map to and from KCL code. Each artifact maintains a source range to the part
* of the KCL code that generated it.
* The artifactGraph is a client-side representation of the commands that have been sent
* see: src/lang/std/artifactGraph-README.md for a full explanation.
*/
artifactMap: ArtifactMap = {}
artifactGraph: ArtifactGraph = new Map()
/**
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
*/
@ -1122,21 +1121,14 @@ export class EngineCommandManager extends EventTarget {
} = {}
/**
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
* list of the individual commands, this is used to process all the commands into the artifactMap
* list of the individual commands, this is used to process all the commands into the artifactGraph
*/
orderedCommands: Array<OrderedCommand> = []
/**
* A map of the responses to the @this.orderedCommands, when processing the commands into the artifactMap, this response map allow
* A map of the responses to the {@link orderedCommands}, when processing the commands into the artifactGraph, this response map allow
* us to look up the response by command id
*/
responseMap: ResponseMap = {}
/**
* The client-side representation of the scene command artifacts that have been sent to the server;
* that is, the *non-modeling* commands and corresponding artifacts.
*
* For modeling commands, see {@link artifactMap}.
*/
sceneCommandArtifacts: ArtifactMap = {}
/**
* A counter that is incremented with each command sent over the *unreliable* channel to the engine.
* This is compared to the latest received {@link inSequence} number to determine if we should ignore
@ -1158,7 +1150,7 @@ export class EngineCommandManager extends EventTarget {
reject: (reason: any) => void
}
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
private resolveReady = () => {}
resolveReady = () => {}
/** Folks should realize that wait for ready does not get called _everytime_
* the connection resets and restarts, it only gets called the first time.
*
@ -1205,11 +1197,12 @@ export class EngineCommandManager extends EventTarget {
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
disableWebRTC = false
modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any
start({
restart,
disableWebRTC = false,
setMediaStream,
setIsStreamReady,
width,
@ -1225,7 +1218,7 @@ export class EngineCommandManager extends EventTarget {
showScaleGrid: false,
},
}: {
restart?: boolean
disableWebRTC?: boolean
setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void
width: number
@ -1242,6 +1235,7 @@ export class EngineCommandManager extends EventTarget {
}
}) {
this.makeDefaultPlanes = makeDefaultPlanes
this.disableWebRTC = disableWebRTC
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) {
return
@ -1720,15 +1714,11 @@ export class EngineCommandManager extends EventTarget {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
if (!this.engineConnection?.isReady()) {
if (!this.engineConnection?.isReady() && !this.disableWebRTC)
return Promise.resolve()
}
if (id === undefined) {
return Promise.reject(new Error('id is undefined'))
}
if (rangeStr === undefined) {
if (id === undefined) return Promise.reject(new Error('id is undefined'))
if (rangeStr === undefined)
return Promise.reject(new Error('rangeStr is undefined'))
}
if (commandStr === undefined) {
return Promise.reject(new Error('commandStr is undefined'))
}
@ -1800,18 +1790,18 @@ export class EngineCommandManager extends EventTarget {
*/
async waitForAllCommands() {
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise))
this.artifactMap = createArtifactMap({
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,
ast: this.getAst(),
})
if (Object.values(this.artifactMap).length) {
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
}
private async initPlanes() {
async initPlanes() {
if (this.planesInitialized()) return
const planes = await this.makeDefaultPlanes()
this.defaultPlanes = planes
@ -1851,7 +1841,7 @@ export class EngineCommandManager extends EventTarget {
range: SourceRange,
commandTypeToTarget: string
): string | undefined {
const values = Object.entries(this.artifactMap)
const values = Object.entries(this.artifactGraph)
for (const [id, data] of values) {
// // Our range selection seems to just select the cursor position, so either
// // of these can be right...