house/src/turtle.ts

184 lines
5.7 KiB
TypeScript

import {
Mesh,
MeshBuilder,
Quaternion,
Scene,
Vector3,
} from '@babylonjs/core/Legacy/legacy';
import { VM as BaseVM, Input, Primitives, Environment } from './cat.js';
export class PenState {
templatePath: Vector3[] = [new Vector3()];
paths: Vector3[][] | null = null;
templateScale = new Vector3(1, 1, 1);
get isDown(): boolean {
return this.paths !== null;
}
clear() {
this.templatePath = [new Vector3()];
this.paths = null;
}
set() {
if (!this.paths) throw new Error("Cannot set pen with no paths");
this.templatePath = this.paths[0];
this.paths = null;
}
down() {
this.paths = this.templatePath.map(_p => []);
}
push(pos: Vector3, q: Quaternion) {
this.templatePath.forEach((p, i) => {
const r = new Vector3();
p.multiplyToRef(this.templateScale, r);
r.rotateByQuaternionToRef(q, r);
r.addInPlace(pos);
this.paths![i].push(r);
});
}
};
const D2R = Math.PI / 180;
export class TurtleVM extends BaseVM<TurtleVM> {
container: Mesh;
counter = 0;
sideOrientation = Mesh.BACKSIDE; // TODO: see SideOrientation primitive below
pen = new PenState();
pos = new Vector3();
q = new Quaternion();
cornerDivisions = 0;
get euler(): Vector3 {
return this.q.toEulerAngles();
}
constructor(
name: string,
scene: Scene | null,
program: Input,
) {
super(program, TurtlePrimitives);
this.container = new Mesh(name, scene);
}
forwardBy(dist: number) {
const v = new Vector3(0, 0, dist);
v.rotateByQuaternionToRef(this.q, v);
this.pos.addInPlace(v);
if (this.pen.isDown) this.pen.push(this.pos, this.q);
}
setQ(newQ: Quaternion) {
if (this.cornerDivisions === 0) {
this.q = newQ;
} else {
this.q = newQ;
this.pen.push(this.pos, this.q);
}
}
rotate(y: number, x: number, z: number) {
const e = this.euler;
e.addInPlaceFromFloats(x * D2R, y * D2R, z * D2R);
this.setQ(e.toQuaternion());
}
relativeRotate(y: number, x: number, z: number) {
this.setQ(this.q.multiply(Quaternion.FromEulerAngles(x * D2R, y * D2R, z * D2R)));
}
penDown() {
this.pen.down();
this.pen.push(this.pos, this.q);
}
penUp(close: boolean) {
if (!this.pen.isDown) return;
if (close) {
throw new Error('todo');
}
const m = MeshBuilder.CreateRibbon(this.container.name + this.counter++, {
pathArray: this.pen.paths!,
sideOrientation: this.sideOrientation,
});
m.parent = this.container;
}
}
export const TurtlePrimitives: Environment<TurtleVM> = Object.assign({}, Primitives, {
'Home'() { this.pos = new Vector3(); this.q = new Quaternion(); return []; },
'GetPos'() { return [this.pos.asArray()]; },
'GetX'() { return [this.pos.x]; },
'GetY'() { return [this.pos.y]; },
'GetZ'() { return [this.pos.z]; },
'SetX'(v) { this.pos.x = v as number; return []; },
'SetY'(v) { this.pos.y = v as number; return []; },
'SetZ'(v) { this.pos.z = v as number; return []; },
'SetPos'(x, y, z) {
this.pos.set(x as number, y as number, z as number);
return [];
},
'GetHeading'() { return [this.euler.asArray().map(v => v / D2R)]; },
'GetRX'() { return [this.euler.x / D2R]; },
'GetRY'() { return [this.euler.y / D2R]; },
'GetRZ'() { return [this.euler.z / D2R]; },
'SetRX'(v) { this.q.x = v as number * D2R; return []; },
'SetRY'(v) { this.q.y = v as number * D2R; return []; },
'SetRZ'(v) { this.q.z = v as number * D2R; return []; },
'SetHeading'(x, y, z) {
this.q = Quaternion.FromEulerAngles(x as number * D2R, y as number * D2R, z as number * D2R);
return [];
},
'F'(dist) { this.forwardBy(dist as number); return []; },
'B'(dist) { this.forwardBy(-(dist as number)); return []; },
'RX'(degrees) { this.rotate(0, degrees as number, 0); return []; },
'RY'(degrees) { this.rotate(degrees as number, 0, 0); return []; },
'RZ'(degrees) { this.rotate(0, 0, degrees as number); return []; },
'U'(degrees) { this.relativeRotate(0, -(degrees as number), 0); return []; },
'D'(degrees) { this.relativeRotate(0, degrees as number, 0); return []; },
'L'(degrees) { this.relativeRotate(-(degrees as number), 0, 0); return []; },
'R'(degrees) { this.relativeRotate(degrees as number, 0, 0); return []; },
'CW'(degrees) { this.relativeRotate(0, 0, -(degrees as number)); return []; },
'CCW'(degrees) { this.relativeRotate(0, 0, degrees as number); return []; },
'ClearPen'() { this.pen.clear(); return []; },
'SetPen'() { this.pen.set(); return []; },
'PenScale'(sx, sy, sz) {
this.pen.templateScale = new Vector3(sx as number, sy as number, sz as number);
return [];
},
'PenDown'() { this.penDown(); return []; },
'PenUp'() { this.penUp(false); return []; },
'Close'() { this.penUp(true); return []; },
'SideOrientation'(s) {
// TODO: why is this back to front?? argh
switch (s) {
case "default": this.sideOrientation = Mesh.BACKSIDE; break;
case "front": this.sideOrientation = Mesh.BACKSIDE; break;
case "back": this.sideOrientation = Mesh.FRONTSIDE; break;
case "double": this.sideOrientation = Mesh.DOUBLESIDE; break;
default:
break;
}
return [];
},
'CornerDivisions'(n) { this.cornerDivisions = n as number; return []; },
} satisfies Environment<TurtleVM>);