house/src/index.ts

326 lines
13 KiB
TypeScript
Raw Normal View History

2023-02-14 19:29:02 +00:00
import { is, fromJS, Dataflow, Dataspace, Double, Embedded, Ref, Schemas, Turn } from "@syndicate-lang/core";
2023-01-05 10:26:06 +00:00
import * as html from "@syndicate-lang/html";
2023-01-05 15:45:19 +00:00
import * as timer from "@syndicate-lang/timer";
2023-01-05 10:26:06 +00:00
import * as wsRelay from "@syndicate-lang/ws-relay";
import * as wakeDetector from './wake-detector.js';
2023-01-05 14:47:27 +00:00
import * as Shapes from './gen/shapes.js';
2023-01-06 14:09:46 +00:00
import * as SceneProtocol from './gen/scene.js';
2023-03-07 15:39:10 +00:00
import * as Tracking from './gen/tracking.js';
2023-01-06 13:01:47 +00:00
import { md5 } from './md5.js';
2023-01-10 14:09:19 +00:00
import { setupLog, log } from './log.js';
2023-02-11 21:09:44 +00:00
import G = Schemas.gatekeeper;
import T = Schemas.transportAddress;
2023-01-05 10:26:06 +00:00
import {
2023-01-06 14:09:46 +00:00
AbstractMesh,
Mesh,
2023-01-09 13:40:05 +00:00
Vector3,
} from '@babylonjs/core/Legacy/legacy';
2023-02-03 23:05:06 +00:00
import { activeFloorMeshes, activeTouchableMeshes, Environment, ShapeTree, buildSound, builder as B, u2, u3, u3v } from './shapes.js';
2023-01-09 13:40:05 +00:00
import { RunningEngine } from './engine.js';
2023-01-05 15:45:19 +00:00
import { uuid } from './uuid.js';
2023-01-05 14:47:27 +00:00
assertion type SceneHandle(ds: Embedded<Ref>);
function interpretScene(myId: string, runningEngine: RunningEngine, rootMesh: Mesh, sceneDs: Ref) {
2023-01-05 14:47:27 +00:00
at sceneDs {
2023-02-04 12:23:37 +00:00
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);
}
2023-02-03 23:05:06 +00:00
during Shapes.Sprite({ "name": $name: string, "formals": $formals }) => spawn named `sprite:${name}` {
2023-01-05 15:45:19 +00:00
if (name === myId) {
console.log('ignoring sprite', name);
} else {
console.log('+shape', name);
on stop console.log('-shape', name);
2023-02-03 23:05:06 +00:00
const env = new Environment(name, formals as symbol[], sceneDs);
spriteMain(env, runningEngine, rootMesh);
2023-01-05 15:45:19 +00:00
}
2023-01-05 14:47:27 +00:00
}
during SceneProtocol.AmbientSound({
"name": $name: string,
2023-02-02 20:58:53 +00:00
"spec": $spec: Shapes.SoundSpec
}) => spawn named `sound:${name}` {
const sound = buildSound(name, runningEngine.scene, spec, false);
on stop sound.dispose();
}
2023-01-05 14:47:27 +00:00
}
}
2023-02-03 23:05:06 +00:00
function spriteMain(env: Environment, runningEngine: RunningEngine, rootMesh: Mesh) {
at env.sceneDs {
let currentShape = ShapeTree.empty(env.spriteName, runningEngine.scene);
2023-01-12 14:22:03 +00:00
currentShape.rootnode.parent = rootMesh;
2023-01-05 14:47:27 +00:00
on stop currentShape.remove();
2023-02-03 23:05:06 +00:00
during Shapes.Sprite({ "name": env.spriteName, "shape": $shape: Shapes.Shape }) => {
currentShape = currentShape.reconcile(env, env.spriteName, shape);
2023-01-12 14:22:03 +00:00
currentShape.rootnode.parent = rootMesh;
2023-01-05 14:47:27 +00:00
}
}
}
2023-01-09 13:40:05 +00:00
async function enterScene(
2023-02-11 21:09:44 +00:00
routeField: Dataflow.Field<G.Route<Ref>>,
2023-01-09 13:40:05 +00:00
id: string,
runningEngine: RunningEngine,
ds: Ref,
sceneDs: Ref,
2023-01-09 15:10:22 +00:00
initialPosition: Vector3,
2023-01-09 13:40:05 +00:00
email: Dataflow.Field<string>,
) {
const currentSceneFacet = Turn.activeFacet;
console.log('enterScene', sceneDs);
const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene);
runningEngine.applyGravity = false;
2023-01-11 13:28:34 +00:00
runningEngine.camera.position = initialPosition.clone();
2023-01-09 13:40:05 +00:00
interpretScene(id, runningEngine, rootMesh, sceneDs);
2023-01-09 13:40:05 +00:00
let lastTouchTime = 0;
let lastTouchSpriteName = "";
2023-01-11 13:28:34 +00:00
runningEngine.onCollide = (other: AbstractMesh) => {
2023-01-09 13:40:05 +00:00
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;
}
}
};
2023-02-03 23:05:06 +00:00
const TAU = 2 * Math.PI;
const currentPosition = () => u3(runningEngine.position);
const currentRotation = () => u3(runningEngine.rotation, 1/TAU);
2023-01-09 13:40:05 +00:00
2023-02-03 23:05:06 +00:00
field position: Shapes.ImmediateVector3 = currentPosition();
field rotation: Shapes.ImmediateVector3 = currentRotation();
2023-01-09 13:40:05 +00:00
2023-02-14 19:29:02 +00:00
const refreshPeriod = Double(1 / 10);
2023-01-09 13:40:05 +00:00
at ds {
on message timer.PeriodicTick(refreshPeriod) => {
const newPosition = currentPosition();
const newRotation = currentRotation();
2023-02-03 23:05:06 +00:00
if (!is(Shapes.fromImmediateVector3(position.value), Shapes.fromImmediateVector3(newPosition))) {
2023-01-09 13:40:05 +00:00
position.value = newPosition;
}
2023-02-03 23:05:06 +00:00
if (!is(Shapes.fromImmediateVector3(rotation.value), Shapes.fromImmediateVector3(newRotation))) {
2023-01-09 13:40:05 +00:00
rotation.value = newRotation;
}
}
}
at sceneDs {
on message SceneProtocol.Touch({ "subject": id, "object": $o }) => {
react {
let needStop = true;
on asserted SceneProtocol.Portal({
"name": o,
2023-01-09 15:10:22 +00:00
"destination": $dest: SceneProtocol.PortalDestination,
2023-02-03 23:05:06 +00:00
"position": $targetPosition: Shapes.LiteralVector3,
2023-01-09 13:40:05 +00:00
}) => {
const newPos = new Vector3(targetPosition.x,
targetPosition.y,
targetPosition.z)
.add(runningEngine.options.initialPos);
2023-01-09 13:40:05 +00:00
needStop = false;
switch (dest._variant) {
case "local":
if (dest.value === sceneDs) {
2023-01-11 13:28:34 +00:00
runningEngine.camera.position = newPos;
} else {
runningEngine.scene.removeMesh(rootMesh, true);
Turn.active.stop(currentSceneFacet, () => {
react {
2023-01-31 16:23:42 +00:00
enterScene(routeField,
id,
runningEngine,
ds,
dest.value,
newPos,
email);
}
});
}
2023-01-09 13:40:05 +00:00
break;
default:
2023-01-31 16:23:42 +00:00
console.log('jumping to remote portal', dest);
routeField.value = dest.value;
2023-01-09 13:40:05 +00:00
break;
}
}
const checkFacet = Turn.activeFacet;
Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => {
if (needStop) {
stop {}
}
}));
}
}
2023-02-03 23:05:06 +00:00
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),
}) });
2023-02-04 12:23:45 +00:00
assert Shapes.Sprite({
2023-01-09 13:40:05 +00:00
name: id,
2023-02-03 23:05:06 +00:00
formals: [_POS, _HEAD, _BODY],
2023-01-09 13:40:05 +00:00
shape: B.nonphysical(
2023-02-03 23:05:06 +00:00
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([
2023-01-09 13:40:05 +00:00
B.box(),
2023-02-03 23:05:06 +00:00
B.move(u3v({x: 0, y: 0, z: 0.501}),
2023-01-09 13:40:05 +00:00
B.texture(
Shapes.TextureSpec.uvAlpha({
path: `https://www.gravatar.com/avatar/${md5(new TextEncoder().encode(email.value.trim()))}?s=256&d=wavatar`,
2023-02-03 23:05:06 +00:00
scale: Shapes.Vector2.immediate(u2({ x:1, y:1 })),
offset: Shapes.Vector2.immediate(u2({ x:0, y:0 })),
alpha: Shapes.DoubleValue.immediate(1),
2023-01-09 13:40:05 +00:00
}),
2023-02-03 23:05:06 +00:00
B.rotate(u3v({ x: 0, y: 0.5, z: 0 }),
2023-01-09 13:40:05 +00:00
B.plane()))),
]))),
]))),
2023-02-04 12:23:45 +00:00
});
2023-01-09 13:40:05 +00:00
}
}
2023-01-05 10:26:06 +00:00
function wsurl(): string {
const scheme = (document.location.protocol.toLowerCase() === 'https:') ? 'wss' : 'ws';
return `${scheme}://${document.location.host}/ws`;
}
function bootApp(ds: Ref, runningEngine: RunningEngine) {
2023-01-05 10:26:06 +00:00
spawn named 'app' {
at ds {
2023-01-06 14:09:46 +00:00
const id = uuid();
2023-01-05 10:26:06 +00:00
const url = wsurl();
2023-02-11 21:09:44 +00:00
const relayAddr = T.WebSocket(url);
2023-01-05 10:26:06 +00:00
2023-01-06 14:09:46 +00:00
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);
}));
2023-02-11 21:09:44 +00:00
field route: G.Route<Ref> = G.Route<Ref>({
2023-01-31 15:44:35 +00:00
"transports": [fromJS(relayAddr)],
2023-02-11 21:09:44 +00:00
"pathSteps": [],
2023-01-31 15:44:35 +00:00
});
2023-02-11 21:09:44 +00:00
during G.ResolvePath({
2023-01-31 16:23:42 +00:00
"route": route.value,
2023-02-11 21:09:44 +00:00
"control": $control_e: Embedded,
"resolved": G.Resolved.accepted($remoteDs_e: Embedded),
2023-01-31 16:23:42 +00:00
}) => {
2023-02-11 21:09:44 +00:00
const control = control_e.embeddedValue;
2023-01-05 10:26:06 +00:00
const remoteDs = remoteDs_e.embeddedValue;
2023-01-10 14:09:19 +00:00
setupLog(remoteDs, id, Symbol.for('vr-demo'));
log('connected');
2023-01-05 10:26:06 +00:00
on message wakeDetector.WakeEvent() => {
2023-02-11 21:09:44 +00:00
at control {
send message G.ForceDisconnect();
}
2023-01-05 10:26:06 +00:00
}
2023-01-05 14:47:27 +00:00
2023-01-09 13:40:05 +00:00
react {
at remoteDs {
stop on asserted SceneHandle($sceneDs_e: Embedded) => {
react {
2023-03-07 15:39:10 +00:00
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<Ref> = 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);
}
}
}
}
}
2023-01-31 16:23:42 +00:00
enterScene(route,
id,
2023-01-09 15:10:22 +00:00
runningEngine,
ds,
sceneDs_e.embeddedValue,
runningEngine.options.initialPos,
email);
2023-01-06 14:09:46 +00:00
}
2023-01-05 15:45:19 +00:00
}
2023-01-05 14:47:27 +00:00
}
}
2023-01-05 10:26:06 +00:00
}
}
}
}
window.addEventListener('load', async () => {
2023-01-09 13:40:05 +00:00
const runningEngine = await RunningEngine.start({
floorMeshes: () => activeFloorMeshes,
touchableMeshes: () => activeTouchableMeshes,
});
2023-02-03 20:43:05 +00:00
(window as any).E = runningEngine;
2023-01-05 10:26:06 +00:00
Dataspace.boot(ds => {
html.boot(ds);
2023-01-05 15:45:19 +00:00
timer.boot(ds);
2023-02-04 12:23:45 +00:00
wsRelay.boot(ds, false);
2023-01-05 10:26:06 +00:00
wakeDetector.boot(ds);
bootApp(ds, runningEngine);
2023-01-05 10:26:06 +00:00
});
});