house/src/shapes.ts

244 lines
7.7 KiB
TypeScript

import {
AbstractMesh,
Color3,
CSG,
HemisphericLight,
Mesh,
MeshBuilder,
Node,
PhotoDome,
Quaternion,
Scene,
StandardMaterial,
Texture,
TransformNode,
Vector2,
Vector3,
} from '@babylonjs/core/Legacy/legacy';
import { Value, is } from "@syndicate-lang/core";
import * as Shapes from './gen/shapes.js';
export const activeFloorMeshes: Array<Mesh> = [];
export function adjust(f: (... csgs: CSG[]) => CSG, ... ms: Mesh[]): Mesh {
const csgs = ms.map(m => CSG.FromMesh(m));
const c = f(... csgs);
if (ms.length > 0) {
ms.forEach(m => m.dispose());
const scene = ms[0].getScene();
const ans = c.toMesh(ms[0].name, null, scene, true);
ans.material = ms[0].material;
return ans;
} else {
return c.toMesh("adjusted");
}
}
export const subtractMany = (a: CSG, ... bs: CSG[]) => bs.reduce((a, b) => a.subtract(b), a);
export class PhotoSemiDome extends PhotoDome {
readonly _size: number;
constructor(... args: ConstructorParameters<typeof PhotoDome>) {
super(... args);
this._size = args[2].size!;
this._chopMesh();
}
_chopMesh() {
const d = this._size;
this._mesh.getChildMeshes()[0].dispose(); // remove the covering half-sphere
this._mesh = adjust(subtractMany,
this._mesh,
box(this._mesh.name + "_chop", d, d, d, 0, 0, -d/2));
this._mesh.parent = this;
}
}
export function box(name: string, width: number, height: number, depth: number, x?: number, y?: number, z?: number): Mesh {
const b = MeshBuilder.CreateBox(name, {});
b.scaling = new Vector3(width, height, depth);
b.position = new Vector3(x ?? 0, y ?? b.scaling.y/2, z ?? 0);
return b;
}
//---------------------------------------------------------------------------
export class ShapeTree<N extends Node = Node> {
shapePreserve: Value;
cleanups: Array<() => void> = [];
constructor (
public scene: Scene,
public shape: Shapes.Shape,
public node: N,
) {
this.shapePreserve = Shapes.fromShape(this.shape);
if (this.node instanceof AbstractMesh) {
this.node.checkCollisions = true;
}
}
reconcile(name: string, shape: Shapes.Shape): ShapeTree {
if (is(Shapes.fromShape(shape), this.shapePreserve)) {
return this;
} else {
this.remove();
return build(name, this.scene, shape, {});
}
}
remove() {
this.node?.dispose(false, true);
}
static empty(name: string, scene: Scene): ShapeTree {
return ShapeTree.transform(name, scene, Shapes.Shape.many([]));
}
static transform(name: string, scene: Scene, shape: Shapes.Shape): ShapeTree<TransformNode> {
return new ShapeTree(scene, shape, new TransformNode(name, scene));
}
}
export function v2(v: Shapes.Vector2): Vector2 {
return new Vector2(v.x, v.y);
}
export function v3(v: Shapes.Vector3): Vector3 {
return new Vector3(v.x, v.y, v.z);
}
export function q(q: Shapes.Quaternion): Quaternion {
return new Quaternion(q.a, q.b, q.c, q.d);
}
export type MeshCustomizer = { [key: string]: ((m: ShapeTree<Mesh>) => void) };
function applyCustomizer(m: ShapeTree<Mesh>, c: MeshCustomizer) {
Object.values(c).forEach(f => f(m));
}
export function build(name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree {
switch (shape._variant) {
case "Sphere": {
const t = new ShapeTree<Mesh>(scene, shape, MeshBuilder.CreateSphere(name, {}, scene));
applyCustomizer(t, customize);
return t;
}
case "Box": {
const t = new ShapeTree<Mesh>(scene, shape, MeshBuilder.CreateBox(name, {}, scene));
applyCustomizer(t, customize);
return t;
}
case "Light":
return new ShapeTree(
scene,
shape,
new HemisphericLight(name, v3(shape.value.v), scene));
case "Ground": {
const v = v2(shape.value.size);
const t = new ShapeTree(
scene,
shape,
MeshBuilder.CreateGround(name, { width: v.x, height: v.y }, scene));
applyCustomizer(t, customize);
return t;
}
case "Scale": {
const t = ShapeTree.transform(name, scene, shape);
t.node.scaling = v3(shape.value.v);
build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node;
return t;
}
case "Move": {
const t = ShapeTree.transform(name, scene, shape);
t.node.position = v3(shape.value.v);
build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node;
return t;
}
case "Rotate": {
const t = ShapeTree.transform(name, scene, shape);
switch (shape.value._variant) {
case "euler":
t.node.rotation = v3(shape.value.v);
break;
case "quaternion":
t.node.rotationQuaternion = q(shape.value.q);
break;
}
build(name + '.inner', scene, shape.value.shape, customize).node.parent = t.node;
return t;
}
case "many": {
const t = ShapeTree.transform(name, scene, shape);
shape.value.forEach((s, i) => {
build(name + '[' + i + ']', scene, s, customize).node.parent = t.node;
});
return t;
}
case "Texture": {
const mat = new StandardMaterial(name + '.texture', scene);
const tex = new Texture(shape.value.path, scene);
mat.diffuseTexture = tex;
switch (shape.value._variant) {
case "simple":
break;
case "uv": {
const scale = v2(shape.value.scale);
const offset = v2(shape.value.offset);
tex.uScale = 1 / scale.x;
tex.vScale = 1 / scale.y;
tex.uOffset = offset.x;
tex.vOffset = offset.y;
break;
}
}
return build(name + '.inner', scene, shape.value.shape, {
... customize,
material: m => m.node.material = mat,
});
}
case "Color": {
const mat = new StandardMaterial(name + '.texture', scene);
mat.diffuseColor = new Color3(shape.value.r, shape.value.g, shape.value.b);
if (shape.value._variant === "transparent") mat.alpha = shape.value.alpha;
return build(name + '.inner', scene, shape.value.shape, {
... customize,
material: m => m.node.material = mat,
});
}
case "Name":
return build(name + '.' + shape.value.base, scene, shape.value.shape, customize);
case "Floor":
return build(name, scene, shape.value.shape, {
... customize,
floor: m => {
activeFloorMeshes.push(m.node);
m.cleanups.push(() => {
const i = activeFloorMeshes.indexOf(m.node);
if (i !== -1) activeFloorMeshes.splice(i, 1);
});
},
});
default:
((_shape: never) => {
console.error('Unsupported shape variant', shape);
throw new Error("Unsupported shape variant");
})(shape);
}
}