import { is, fromJS, Dataflow, Dataspace, Double, Embedded, Ref, Schemas, Turn } from "@syndicate-lang/core"; import * as html from "@syndicate-lang/html"; import * as timer from "@syndicate-lang/timer"; import * as wsRelay from "@syndicate-lang/ws-relay"; import * as wakeDetector from './wake-detector.js'; import * as Shapes from './gen/shapes.js'; import * as SceneProtocol from './gen/scene.js'; import * as Tracking from './gen/tracking.js'; import { md5 } from './md5.js'; import { setupLog, log } from './log.js'; import G = Schemas.gatekeeper; import T = Schemas.transportAddress; import { AbstractMesh, Mesh, Vector3, } from '@babylonjs/core/Legacy/legacy'; import { activeFloorMeshes, activeTouchableMeshes, Environment, ShapeTree, buildSound, builder as B, u2, u3, u3v } from './shapes.js'; import { RunningEngine } from './engine.js'; import { uuid } from './uuid.js'; assertion type SceneHandle(ds: Embedded); function interpretScene(myId: string, runningEngine: RunningEngine, rootMesh: Mesh, sceneDs: Ref) { at sceneDs { during SceneProtocol.Gravity($direction: Shapes.LiteralVector3) => { Turn.active.sync(sceneDs).then(() => { console.log('enabling gravity'); runningEngine.applyGravity = true; }); on stop { console.log('disabling gravity'); runningEngine.applyGravity = false; } runningEngine.gravity = new Vector3(direction.x, direction.y, direction.z); } during Shapes.Sprite({ "name": $name: string, "formals": $formals }) => spawn named `sprite:${name}` { if (name === myId) { console.log('ignoring sprite', name); } else { console.log('+shape', name); on stop console.log('-shape', name); const env = new Environment(name, formals as symbol[], sceneDs); spriteMain(env, runningEngine, rootMesh); } } during SceneProtocol.AmbientSound({ "name": $name: string, "spec": $spec: Shapes.SoundSpec }) => spawn named `sound:${name}` { const sound = buildSound(name, runningEngine.scene, spec, false); on stop sound.dispose(); } } } function spriteMain(env: Environment, runningEngine: RunningEngine, rootMesh: Mesh) { at env.sceneDs { let currentShape = ShapeTree.empty(env.spriteName, runningEngine.scene); currentShape.rootnode.parent = rootMesh; on stop currentShape.remove(); during Shapes.Sprite({ "name": env.spriteName, "shape": $shape: Shapes.Shape }) => { currentShape = currentShape.reconcile(env, env.spriteName, shape); currentShape.rootnode.parent = rootMesh; } } } async function enterScene( routeField: Dataflow.Field>, id: string, runningEngine: RunningEngine, ds: Ref, sceneDs: Ref, initialPosition: Vector3, email: Dataflow.Field, ) { const currentSceneFacet = Turn.activeFacet; console.log('enterScene', sceneDs); const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene); runningEngine.applyGravity = false; runningEngine.camera.position = initialPosition.clone(); interpretScene(id, runningEngine, rootMesh, sceneDs); let lastTouchTime = 0; let lastTouchSpriteName = ""; runningEngine.onCollide = (other: AbstractMesh) => { if (other.metadata?.touchable) { const now = +new Date(); const touched = other.metadata?.spriteName ?? ""; if ((now - lastTouchTime > 500) || (lastTouchSpriteName !== touched)) { currentSceneFacet.turn(() => { at sceneDs { send message SceneProtocol.Touch({ subject: id, object: touched, }); } }); lastTouchTime = now; lastTouchSpriteName = touched; } } }; const TAU = 2 * Math.PI; const currentPosition = () => u3(runningEngine.position); const currentRotation = () => u3(runningEngine.rotation, 1/TAU); field position: Shapes.ImmediateVector3 = currentPosition(); field rotation: Shapes.ImmediateVector3 = currentRotation(); const refreshPeriod = Double(1 / 10); at ds { on message timer.PeriodicTick(refreshPeriod) => { const newPosition = currentPosition(); const newRotation = currentRotation(); if (!is(Shapes.fromImmediateVector3(position.value), Shapes.fromImmediateVector3(newPosition))) { position.value = newPosition; } if (!is(Shapes.fromImmediateVector3(rotation.value), Shapes.fromImmediateVector3(newRotation))) { rotation.value = newRotation; } } } at sceneDs { on message SceneProtocol.Touch({ "subject": id, "object": $o }) => { react { let needStop = true; on asserted SceneProtocol.Portal({ "name": o, "destination": $dest: SceneProtocol.PortalDestination, "position": $targetPosition: Shapes.LiteralVector3, }) => { const newPos = new Vector3(targetPosition.x, targetPosition.y, targetPosition.z) .add(runningEngine.options.initialPos); needStop = false; switch (dest._variant) { case "local": if (dest.value === sceneDs) { runningEngine.camera.position = newPos; } else { runningEngine.scene.removeMesh(rootMesh, true); Turn.active.stop(currentSceneFacet, () => { react { enterScene(routeField, id, runningEngine, ds, dest.value, newPos, email); } }); } break; default: console.log('jumping to remote portal', dest); routeField.value = dest.value; break; } } const checkFacet = Turn.activeFacet; Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => { if (needStop) { stop {} } })); } } const _POS = Symbol.for('pos'); const _HEAD = Symbol.for('head'); const _BODY = Symbol.for('body'); assert Shapes.Variable({ spriteName: id, variable: _POS, value: Shapes.fromImmediateVector3(position.value) }); assert Shapes.Variable({ spriteName: id, variable: _HEAD, value: Shapes.fromImmediateVector3(rotation.value) }); assert Shapes.Variable({ spriteName: id, variable: _BODY, value: Shapes.fromImmediateVector3({ x: Shapes.DoubleValue.immediate(0), y: rotation.value.y, z: Shapes.DoubleValue.immediate(0), }) }); assert Shapes.Sprite({ name: id, formals: [_POS, _HEAD, _BODY], shape: B.nonphysical( B.move(Shapes.Vector3.reference(_POS), B.many([ B.move(u3v({x: 0, y: -0.9, z: 0}), B.rotate(Shapes.Vector3.reference(_BODY), B.scale(u3v({x: 0.4, y: 1.4, z: 0.1}), B.box()))), B.rotate(Shapes.Vector3.reference(_HEAD), B.scale(u3v({x: 0.15, y: 0.23, z: 0.18}), B.many([ B.box(), B.move(u3v({x: 0, y: 0, z: 0.501}), B.texture( Shapes.TextureSpec.uvAlpha({ path: `https://www.gravatar.com/avatar/${md5(new TextEncoder().encode(email.value.trim()))}?s=256&d=wavatar`, scale: Shapes.Vector2.immediate(u2({ x:1, y:1 })), offset: Shapes.Vector2.immediate(u2({ x:0, y:0 })), alpha: Shapes.DoubleValue.immediate(1), }), B.rotate(u3v({ x: 0, y: 0.5, z: 0 }), B.plane()))), ]))), ]))), }); } } function wsurl(): string { const scheme = (document.location.protocol.toLowerCase() === 'https:') ? 'wss' : 'ws'; return `${scheme}://${document.location.host}/ws`; } function bootApp(ds: Ref, runningEngine: RunningEngine) { spawn named 'app' { at ds { const id = uuid(); const url = wsurl(); const relayAddr = T.WebSocket(url); field email: string = localStorage.getItem('userEmail') ?? id; const outerFacet = Turn.activeFacet; const emailInput = document.getElementById('emailInput') as HTMLInputElement; emailInput.value = email.value; emailInput.addEventListener('keyup', () => outerFacet.turn(() => { email.value = emailInput.value; localStorage.setItem('userEmail', emailInput.value); })); field route: G.Route = G.Route({ "transports": [fromJS(relayAddr)], "pathSteps": [], }); during G.ResolvePath({ "route": route.value, "control": $control_e: Embedded, "resolved": G.Resolved.accepted($remoteDs_e: Embedded), }) => { const control = control_e.embeddedValue; const remoteDs = remoteDs_e.embeddedValue; setupLog(remoteDs, id, Symbol.for('vr-demo')); log('connected'); on message wakeDetector.WakeEvent() => { at control { send message G.ForceDisconnect(); } } react { at remoteDs { stop on asserted SceneHandle($sceneDs_e: Embedded) => { react { at remoteDs { const ms: { [key: number]: true } = {}; on message $m0(Tracking.Marker({ "camera": "cam1", "id": _ })) => { const m = Tracking.asMarker(m0); if (!(m.id in ms)) { console.log('Spawning marker', m.id); ms[m.id] = true; spawn linked named ['marker', m.id] { field current: Tracking.Marker = m; on stop { delete ms[m.id]; } on message $m1(Tracking.Marker({ "camera": m.camera, "id": m.id, "rotation": _, })) => { current.value = Tracking.asMarker(m1); } at sceneDs_e.embeddedValue { assert fromJS(current.value); } } } } } enterScene(route, id, runningEngine, ds, sceneDs_e.embeddedValue, runningEngine.options.initialPos, email); } } } } } } } } window.addEventListener('load', async () => { const runningEngine = await RunningEngine.start({ floorMeshes: () => activeFloorMeshes, touchableMeshes: () => activeTouchableMeshes, }); (window as any).E = runningEngine; Dataspace.boot(ds => { html.boot(ds); timer.boot(ds); wsRelay.boot(ds, false); wakeDetector.boot(ds); bootApp(ds, runningEngine); }); });