house/src/shapes.ts

555 lines
20 KiB
TypeScript
Raw Normal View History

import {
2023-01-12 14:22:03 +00:00
AbstractMesh,
2023-01-05 14:47:27 +00:00
Color3,
2023-01-10 15:36:03 +00:00
CubeTexture,
CSG,
2023-01-05 14:47:27 +00:00
HemisphericLight,
ISoundOptions,
2023-01-06 11:16:42 +00:00
Material,
2023-01-10 21:04:38 +00:00
Matrix,
Mesh,
MeshBuilder,
2023-01-05 14:47:27 +00:00
Node,
Quaternion,
Scene,
SceneLoader,
2023-01-12 15:05:59 +00:00
Sound,
2023-01-05 14:47:27 +00:00
StandardMaterial,
Texture,
TransformNode,
Vector2,
Vector3,
} from '@babylonjs/core/Legacy/legacy';
2023-02-13 16:39:13 +00:00
import { Dataflow, IdentityMap, KeyedDictionary, Ref, Value, is, fromJS } from "@syndicate-lang/core";
2023-01-05 14:47:27 +00:00
import * as Shapes from './gen/shapes.js';
2023-02-13 16:39:13 +00:00
import { TurtleVM } from './turtle.js';
2023-01-05 14:47:27 +00:00
2023-02-03 23:05:06 +00:00
export class Environment {
fields = new IdentityMap<symbol, Dataflow.Field<Value<Ref>>>();
constructor(
public spriteName: string,
public formals: symbol[],
public sceneDs: Ref,
) {
formals.forEach(f => {
field v: Value<Ref> = false;
at this.sceneDs {
on asserted Shapes.Variable({
"spriteName": this.spriteName,
"variable": f,
"value": $newValue,
}) => {
v.value = newValue;
}
}
this.fields.set(f, v);
});
}
lookup(f: symbol, k: (v: Value<Ref>) => void) {
dataflow {
const v = this.fields.get(f);
if (v === void 0) throw new Error(`Lookup of ${f.description} in ${this.spriteName} failed`);
const w = v.value;
if (w !== false) k(w);
}
}
}
export type ValueK = (v: Value<Ref>) => void;
2023-01-12 14:22:03 +00:00
export const activeFloorMeshes: Array<AbstractMesh> = [];
export const activeTouchableMeshes: Array<AbstractMesh> = [];
2023-01-05 14:47:27 +00:00
export class ShapeTree<N extends Node = Node> {
shapePreserve: Value;
cleanups: Array<() => void> = [];
2023-01-12 14:22:03 +00:00
subnodes: Promise<N[]>;
subtrees: ShapeTree[] = [];
2023-01-05 14:47:27 +00:00
constructor (
public scene: Scene,
public shape: Shapes.Shape,
2023-01-12 14:22:03 +00:00
public rootnode: N,
subnodes?: Promise<N[]>,
2023-01-05 14:47:27 +00:00
) {
this.shapePreserve = Shapes.fromShape(this.shape);
2023-01-12 14:22:03 +00:00
const metadata = {};
this.rootnode.metadata = metadata;
this.subnodes = subnodes ?? Promise.resolve([]);
this.subnodes.then(ns => ns.forEach(n => n.metadata = metadata));
}
get allnodes(): Promise<N[]> {
return this.subnodes.then(ns => [this.rootnode, ... ns]);
2023-01-05 14:47:27 +00:00
}
2023-02-03 23:05:06 +00:00
reconcile(env: Environment, name: string, shape: Shapes.Shape): ShapeTree {
2023-01-05 14:47:27 +00:00
if (is(Shapes.fromShape(shape), this.shapePreserve)) {
return this;
} else {
this.remove();
2023-02-03 23:05:06 +00:00
return build(env, name, this.scene, shape, {
spriteName: async m => m.rootnode.metadata.spriteName = env.spriteName,
2023-01-12 14:22:03 +00:00
collisions: async m => (await m.allnodes).forEach(n => n.checkCollisions = true),
2023-01-05 15:45:19 +00:00
});
2023-01-05 14:47:27 +00:00
}
}
set parent(t: ShapeTree) {
t.subtrees.push(this);
this.rootnode.parent = t.rootnode;
}
2023-01-05 14:47:27 +00:00
remove() {
this.subtrees.forEach(t => t.remove());
2023-01-12 14:22:03 +00:00
this.allnodes.then(ns => ns.forEach(n => n.dispose()));
this.cleanups.forEach(c => c());
2023-01-05 14:47:27 +00:00
}
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));
}
}
2023-02-03 23:05:06 +00:00
export function dv(env: Environment, v: Shapes.DoubleValue, k: (v: number) => void) {
switch (v._variant) {
case "immediate": k(v.value); break;
case "reference": env.lookup(v.value, k as ValueK); break;
}
}
export function u2(v: {x: number, y: number}): Shapes.ImmediateVector2 {
return Shapes.ImmediateVector2({
x: Shapes.DoubleValue.immediate(v.x),
y: Shapes.DoubleValue.immediate(v.y),
});
}
export function lv2(env: Environment, v: Shapes.ImmediateVector2, k: (v: Vector2) => void) {
dv(env, v.x, x => dv(env, v.y, y => k(new Vector2(x, y))));
}
export function v2(env: Environment, v: Shapes.Vector2, k: (v: Vector2) => void) {
switch (v._variant) {
case "immediate": lv2(env, v.value, k); break;
case "reference": env.lookup(v.value, v => lv2(env, Shapes.asImmediateVector2(v), k)); break;
}
}
export function u3(v: {x: number, y: number, z: number}, scale = 1): Shapes.ImmediateVector3 {
return Shapes.ImmediateVector3({
x: Shapes.DoubleValue.immediate(v.x * scale),
y: Shapes.DoubleValue.immediate(v.y * scale),
z: Shapes.DoubleValue.immediate(v.z * scale),
});
2023-01-05 14:47:27 +00:00
}
2023-02-03 23:05:06 +00:00
export function u3v(v: {x: number, y: number, z: number}, scale = 1): Shapes.Vector3 {
return Shapes.Vector3.immediate(u3(v, scale));
2023-01-05 14:47:27 +00:00
}
2023-02-03 23:05:06 +00:00
export function lv3(env: Environment, v: Shapes.ImmediateVector3, k: (v: Vector3) => void) {
dv(env, v.x, x => dv(env, v.y, y => dv(env, v.z, z => k(new Vector3(x, y, z)))));
}
2023-02-03 23:05:06 +00:00
export function v3(env: Environment, v: Shapes.Vector3, k: (v: Vector3) => void) {
switch (v._variant) {
case "immediate": lv3(env, v.value, k); break;
case "reference": env.lookup(v.value, v => lv3(env, Shapes.asImmediateVector3(v), k)); break;
}
}
export function lq(env: Environment, q: Shapes.ImmediateQuaternion, k: (v: Quaternion) => void) {
dv(env, q.a, a => dv(env, q.b, b => dv(env, q.c, c => dv(env, q.d, d =>
k(new Quaternion(a, b, c, d))))));
}
export function q(env: Environment, q: Shapes.Quaternion, k: (v: Quaternion) => void) {
switch (q._variant) {
case "immediate": lq(env, q.value, k); break;
case "reference": env.lookup(q.value, q => lq(env, Shapes.asImmediateQuaternion(q), k)); break;
}
2023-01-05 14:47:27 +00:00
}
2023-01-12 14:22:03 +00:00
export type MeshCustomizer = { [key: string]: ((m: ShapeTree<AbstractMesh>) => void) };
2023-01-05 14:47:27 +00:00
2023-01-12 14:22:03 +00:00
function applyCustomizer(m: ShapeTree<AbstractMesh>, c: MeshCustomizer) {
2023-01-05 14:47:27 +00:00
Object.values(c).forEach(f => f(m));
}
2023-01-06 13:01:47 +00:00
type CachedTexture = {
key: Value,
referenceCount: number,
material: Material,
};
const textureCache = new KeyedDictionary<Value, CachedTexture>();
2023-02-03 23:05:06 +00:00
function buildTexture(env: Environment, name: string, scene: Scene, spec: Shapes.TextureSpec): CachedTexture {
2023-01-06 13:01:47 +00:00
const cacheKey = Shapes.fromTextureSpec(spec);
const entry = textureCache.get(cacheKey);
if (entry !== void 0) {
entry.referenceCount++;
return entry;
}
2023-01-06 11:16:42 +00:00
const mat = new StandardMaterial(name, scene);
const tex = new Texture(spec.path, scene);
mat.diffuseTexture = tex;
switch (spec._variant) {
case "simple":
break;
case "uvAlpha":
2023-02-03 23:05:06 +00:00
mat.alpha = 0;
dv(env, spec.alpha, a => mat.alpha = a);
2023-01-06 12:12:59 +00:00
tex.hasAlpha = true;
2023-01-06 11:16:42 +00:00
/* FALL THROUGH */
2023-02-03 23:05:06 +00:00
case "uv":
v2(env, spec.scale, scale => v2(env, spec.offset, offset => {
tex.uScale = scale.x;
tex.vScale = scale.y;
tex.uOffset = offset.x;
tex.vOffset = offset.y;
}));
2023-01-06 11:16:42 +00:00
break;
}
2023-01-06 13:01:47 +00:00
const newEntry = {
key: cacheKey,
referenceCount: 1,
material: mat,
};
textureCache.set(cacheKey, newEntry);
return newEntry;
}
function releaseTexture(entry: CachedTexture) {
if (--entry.referenceCount === 0) {
textureCache.delete(entry.key);
entry.material.dispose();
}
2023-01-06 11:16:42 +00:00
}
2023-01-12 14:22:03 +00:00
export type BuiltMesh = {
rootnode: AbstractMesh,
subnodes?: Promise<AbstractMesh[]>,
};
export function buildMesh(
name: string,
scene: Scene | null,
meshSpec: Shapes.Mesh,
2023-01-12 14:22:03 +00:00
): BuiltMesh {
2023-01-10 21:04:38 +00:00
switch (meshSpec._variant) {
2023-01-12 14:22:03 +00:00
case "Sphere": return { rootnode: MeshBuilder.CreateSphere(name, {}, scene) };
case "Box": return { rootnode: MeshBuilder.CreateBox(name, {}, scene) };
2023-02-03 23:05:06 +00:00
case "Ground": return { rootnode: MeshBuilder.CreateGround(name, {}, scene ?? void 0) };
2023-01-12 14:22:03 +00:00
case "Plane": return { rootnode: MeshBuilder.CreatePlane(name, {}, scene) };
case "External": {
2023-01-12 14:22:03 +00:00
const rootnode = new Mesh(name, scene);
return {
rootnode,
subnodes: new Promise(async resolve => {
const r =
await SceneLoader.ImportMeshAsync("", meshSpec.value.path, void 0, scene);
r.meshes.forEach(m => m.parent = rootnode);
resolve(r.meshes);
}),
};
}
2023-02-13 16:39:13 +00:00
case "turtle": {
const t = new TurtleVM(name, scene, meshSpec.value.program.map(fromJS));
2023-02-14 11:16:52 +00:00
try {
t.exec();
} catch (e) {
console.error(e);
}
2023-02-14 08:27:25 +00:00
return {
rootnode: t.container,
subnodes: Promise.resolve(t.meshes),
};
2023-02-13 16:39:13 +00:00
}
2023-01-10 21:04:38 +00:00
}
}
2023-02-02 20:58:53 +00:00
export function buildSound(name: string, scene: Scene, spec: Shapes.SoundSpec, spatial: boolean): Sound {
const options: ISoundOptions = {
loop: true,
autoplay: true,
skipCodecCheck: true,
};
if (spatial) {
options.distanceModel = "inverse";
options.rolloffFactor = 0.25;
}
switch (spec._variant) {
case "stream":
options.streaming = true;
break;
case "loop":
break;
}
return new Sound(name, spec.url, scene, null, options);
}
2023-02-03 23:05:06 +00:00
export function build(env: Environment, name: string, scene: Scene, shape: Shapes.Shape, customize: MeshCustomizer): ShapeTree {
2023-01-05 14:47:27 +00:00
switch (shape._variant) {
2023-01-05 21:03:35 +00:00
case "Mesh": {
2023-01-12 14:22:03 +00:00
const m = buildMesh(name, scene, shape.value);
const t = new ShapeTree<AbstractMesh>(scene, shape, m.rootnode, m.subnodes);
2023-01-06 11:16:42 +00:00
applyCustomizer(t, customize);
return t;
2023-01-05 14:47:27 +00:00
}
2023-02-03 23:05:06 +00:00
case "Light": {
const light = new HemisphericLight(name, new Vector3(0, 1, 0), scene);
v3(env, shape.value.v, v => light.direction = v);
return new ShapeTree(scene, shape, light);
}
2023-01-05 14:47:27 +00:00
case "Scale": {
const t = ShapeTree.transform(name, scene, shape);
2023-02-03 23:05:06 +00:00
v3(env, shape.value.v, v => t.rootnode.scaling = v);
build(env, name + '.inner', scene, shape.value.shape, customize).parent = t;
2023-01-05 14:47:27 +00:00
return t;
}
case "Move": {
const t = ShapeTree.transform(name, scene, shape);
2023-02-03 23:05:06 +00:00
v3(env, shape.value.v, v => t.rootnode.position = v);
build(env, name + '.inner', scene, shape.value.shape, customize).parent = t;
2023-01-05 14:47:27 +00:00
return t;
}
case "Rotate": {
const t = ShapeTree.transform(name, scene, shape);
switch (shape.value._variant) {
case "euler":
2023-02-03 23:05:06 +00:00
v3(env, shape.value.v, v => {
t.rootnode.rotation = v;
t.rootnode.rotation.scaleInPlace(2 * Math.PI);
});
2023-01-05 14:47:27 +00:00
break;
case "quaternion":
2023-02-03 23:05:06 +00:00
q(env, shape.value.q, q => t.rootnode.rotationQuaternion = q);
2023-01-05 14:47:27 +00:00
break;
}
2023-02-03 23:05:06 +00:00
build(env, name + '.inner', scene, shape.value.shape, customize).parent = t;
2023-01-05 14:47:27 +00:00
return t;
}
case "many": {
const t = ShapeTree.transform(name, scene, shape);
shape.value.forEach((s, i) => {
2023-02-03 23:05:06 +00:00
build(env, name + '[' + i + ']', scene, s, customize).parent = t;
2023-01-05 14:47:27 +00:00
});
return t;
}
case "Texture": {
2023-02-03 23:05:06 +00:00
const entry = buildTexture(env, name + '.texture', scene, shape.value.spec);
const t = build(env, name + '.inner', scene, shape.value.shape, {
2023-01-05 14:47:27 +00:00
... customize,
2023-01-12 14:22:03 +00:00
material: async m => (await m.allnodes).forEach(n => n.material = entry.material),
2023-01-05 14:47:27 +00:00
});
2023-01-06 13:01:47 +00:00
t.cleanups.push(() => releaseTexture(entry));
return t;
2023-01-05 14:47:27 +00:00
}
case "Color": {
const mat = new StandardMaterial(name + '.texture', scene);
2023-02-03 23:05:06 +00:00
dv(env, shape.value.r, r =>
dv(env, shape.value.g, g =>
dv(env, shape.value.b, b => mat.diffuseColor = new Color3(r, g, b))));
if (shape.value._variant === "transparent") {
dv(env, shape.value.alpha, a => mat.alpha = a);
}
const t = build(env, name + '.inner', scene, shape.value.shape, {
2023-01-05 14:47:27 +00:00
... customize,
2023-01-12 14:22:03 +00:00
material: async m => (await m.allnodes).forEach(n => n.material = mat),
2023-01-05 14:47:27 +00:00
});
2023-02-03 20:42:19 +00:00
t.cleanups.push(() => mat.dispose());
return t;
2023-01-05 14:47:27 +00:00
}
2023-01-12 15:05:59 +00:00
case "Sound": {
const sound = buildSound(name + ".sound", scene, shape.value.spec, true);
2023-02-03 23:05:06 +00:00
const t = build(env, name + ".inner", scene, shape.value.shape, {
2023-01-12 15:05:59 +00:00
... customize,
sound: async m => sound.attachToMesh(m.rootnode),
});
t.cleanups.push(() => sound.dispose());
return t;
2023-01-12 15:05:59 +00:00
}
2023-01-05 14:47:27 +00:00
case "Name":
2023-02-03 23:05:06 +00:00
return build(env, name + '.' + shape.value.base, scene, shape.value.shape, customize);
2023-01-05 14:47:27 +00:00
case "Floor":
2023-02-03 23:05:06 +00:00
return build(env, name, scene, shape.value.shape, {
2023-01-05 14:47:27 +00:00
... customize,
2023-01-12 14:22:03 +00:00
floor: async m => {
const nodes = await m.allnodes;
activeFloorMeshes.push(... nodes);
2023-01-05 14:47:27 +00:00
m.cleanups.push(() => {
2023-01-12 14:22:03 +00:00
const i = activeFloorMeshes.indexOf(nodes[0]);
if (i !== -1) activeFloorMeshes.splice(i, nodes.length);
2023-01-05 14:47:27 +00:00
});
},
});
2023-01-05 15:45:19 +00:00
case "Nonphysical":
2023-02-03 23:05:06 +00:00
return build(env, name, scene, shape.value.shape, {
2023-01-05 15:45:19 +00:00
... customize,
2023-01-12 14:22:03 +00:00
collisions: async m => (await m.allnodes).forEach(n => n.checkCollisions = false),
2023-01-05 15:45:19 +00:00
});
2023-01-06 14:09:46 +00:00
case "Touchable":
2023-02-03 23:05:06 +00:00
return build(env, name, scene, shape.value.shape, {
2023-01-06 14:09:46 +00:00
... customize,
2023-01-12 14:22:03 +00:00
touchable: async m => {
const nodes = await m.allnodes;
m.rootnode.metadata.touchable = true;
activeTouchableMeshes.push(... nodes);
2023-01-06 14:09:46 +00:00
m.cleanups.push(() => {
2023-01-12 14:22:03 +00:00
const i = activeTouchableMeshes.indexOf(nodes[0]);
if (i !== -1) activeTouchableMeshes.splice(i, nodes.length);
2023-01-06 14:09:46 +00:00
});
}
});
2023-01-05 21:03:35 +00:00
case "CSG": {
2023-01-12 14:22:03 +00:00
const m = buildCSG(name, scene, shape.value.expr);
const t = new ShapeTree<AbstractMesh>(scene, shape, m.rootnode, m.subnodes);
2023-01-10 21:04:38 +00:00
applyCustomizer(t, customize);
return t;
2023-01-05 21:03:35 +00:00
}
2023-01-10 15:36:03 +00:00
case "Skybox": {
2023-01-12 14:22:03 +00:00
const t = new ShapeTree<AbstractMesh>(scene, shape, MeshBuilder.CreateBox(name, { size: 2000 }, scene));
2023-01-10 15:36:03 +00:00
const mat = new StandardMaterial(name, scene);
mat.backFaceCulling = false;
mat.reflectionTexture = new CubeTexture(shape.value.path, scene);
mat.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE;
mat.diffuseColor = new Color3(0, 0, 0);
mat.specularColor = new Color3(0, 0, 0);
2023-01-12 14:22:03 +00:00
t.rootnode.material = mat;
2023-01-10 15:36:03 +00:00
applyCustomizer(t, customize);
return t;
}
2023-01-05 14:47:27 +00:00
default:
((_shape: never) => {
console.error('Unsupported shape variant', shape);
throw new Error("Unsupported shape variant");
})(shape);
}
}
2023-01-06 12:13:14 +00:00
2023-01-12 14:22:03 +00:00
export function buildCSG(name: string, scene: Scene, expr: Shapes.CSGExpr): BuiltMesh {
async function walk(expr: Shapes.CSGExpr, matrix: Matrix): Promise<CSG> {
2023-01-10 21:04:38 +00:00
switch (expr._variant) {
case "mesh": {
2023-01-12 14:22:03 +00:00
const m = buildMesh("", null, expr.shape);
const nodes: Mesh[] =
([m.rootnode, ... (m.subnodes ? await m.subnodes : [])]
.filter(n => n instanceof Mesh)) as Mesh[];
nodes.forEach(n => {
n.freezeWorldMatrix(matrix, true);
n.unfreezeWorldMatrix();
});
const cs = nodes.flatMap(n => {
try {
return [CSG.FromMesh(n)];
} catch (_e) {
return [];
}
});
const c = cs[0];
cs.slice(1).forEach(d => c.unionInPlace(d));
2023-01-12 14:22:03 +00:00
nodes.forEach(n => n.dispose());
2023-01-10 21:04:38 +00:00
return c;
}
case "scale":
return walk(expr.shape, matrix.multiply(
Matrix.Scaling(expr.v.x, expr.v.y, expr.v.z)));
case "move":
return walk(expr.shape, matrix.multiply(
Matrix.Translation(expr.v.x, expr.v.y, expr.v.z)));
case "rotate":
return walk(expr.shape, matrix.multiply(
Matrix.RotationYawPitchRoll(expr.v.y * 2 * Math.PI,
expr.v.x * 2 * Math.PI,
expr.v.z * 2 * Math.PI)));
case "subtract": {
2023-01-12 14:22:03 +00:00
const c = await walk(expr.base, matrix);
for (const d of expr.more) {
c.subtractInPlace(await walk(d, matrix));
}
2023-01-10 21:04:38 +00:00
return c;
}
case "union": {
2023-01-12 14:22:03 +00:00
const c = await walk(expr.base, matrix);
for (const d of expr.more) {
c.unionInPlace(await walk(d, matrix));
}
2023-01-10 21:04:38 +00:00
return c;
}
case "intersect": {
2023-01-12 14:22:03 +00:00
const c = await walk(expr.base, matrix);
for (const d of expr.more) {
c.intersectInPlace(await walk(d, matrix));
}
2023-01-10 21:04:38 +00:00
return c;
}
case "invert": {
2023-01-12 14:22:03 +00:00
const c = await walk(expr.shape, matrix);
2023-01-10 21:04:38 +00:00
c.inverseInPlace();
return c;
}
}
}
2023-01-12 14:22:03 +00:00
const rootnode = new Mesh(name, scene);
return {
rootnode,
subnodes: new Promise(async resolve => {
const c = await walk(expr, Matrix.Identity());
const m = c.toMesh(name + ".csg", null, scene);
m.parent = rootnode;
resolve([m]);
}),
};
2023-01-10 21:04:38 +00:00
}
2023-02-03 23:05:06 +00:00
export const builder = {
2023-01-06 12:13:14 +00:00
sphere: () => Shapes.Shape.Mesh(Shapes.Mesh.Sphere(Shapes.Sphere())),
box: () => Shapes.Shape.Mesh(Shapes.Mesh.Box(Shapes.Box())),
plane: () => Shapes.Shape.Mesh(Shapes.Mesh.Plane(Shapes.Plane())),
2023-02-03 23:05:06 +00:00
ground: () => Shapes.Shape.Mesh(Shapes.Mesh.Ground(Shapes.Ground())),
light: (v: Shapes.Vector3) => Shapes.Shape.Light(Shapes.Light(v)),
scale: (v: Shapes.Vector3, shape: Shapes.Shape) => Shapes.Shape.Scale(Shapes.Scale({ v, shape })),
move: (v: Shapes.Vector3, shape: Shapes.Shape) => Shapes.Shape.Move(Shapes.Move({ v, shape })),
rotate: (v: Shapes.Vector3, shape: Shapes.Shape) => Shapes.Shape.Rotate(Shapes.Rotate.euler({ v, shape })),
2023-01-06 12:13:14 +00:00
many: (shapes: Shapes.Shape[]) => Shapes.Shape.many(shapes),
texture: (spec: Shapes.TextureSpec, shape: Shapes.Shape) => Shapes.Shape.Texture(Shapes.Texture({ spec, shape })),
2023-02-03 23:05:06 +00:00
color: (r0: number | symbol, g0: number | symbol, b0: number | symbol, shape: Shapes.Shape, alpha0: number | symbol = 1.0) => {
function vd(x: number | symbol): Shapes.DoubleValue {
return typeof x === 'number' ? Shapes.DoubleValue.immediate(x) : Shapes.DoubleValue.reference(x);
}
const r = vd(r0);
const g = vd(g0);
const b = vd(b0);
const alpha = vd(alpha0);
return Shapes.Shape.Color((alpha0 === 1.0)
2023-01-06 12:13:14 +00:00
? Shapes.Color.opaque({ r, g, b, shape })
: Shapes.Color.transparent({ r, g, b, alpha, shape }));
},
name: (base: string, shape: Shapes.Shape) => Shapes.Shape.Name(Shapes.Name({ base, shape })),
floor: (shape: Shapes.Shape) => Shapes.Shape.Floor(Shapes.Floor(shape)),
nonphysical: (shape: Shapes.Shape) => Shapes.Shape.Nonphysical(Shapes.Nonphysical(shape)),
2023-02-03 23:05:06 +00:00
} satisfies { [key: string]: (... args: any[]) => Shapes.Shape };