2023-02-13 16:38:43 +00:00
|
|
|
import {
|
|
|
|
Mesh,
|
|
|
|
MeshBuilder,
|
2023-02-14 08:27:25 +00:00
|
|
|
Plane,
|
2023-02-13 16:38:43 +00:00
|
|
|
Quaternion,
|
2023-02-14 08:27:25 +00:00
|
|
|
Ray,
|
2023-02-13 16:38:43 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
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();
|
2023-02-14 11:16:40 +00:00
|
|
|
// p.multiplyToRef(this.templateScale, r);
|
|
|
|
// r.rotateByQuaternionToRef(q, r);
|
|
|
|
p.rotateByQuaternionToRef(q, r);
|
2023-02-13 16:38:43 +00:00
|
|
|
r.addInPlace(pos);
|
|
|
|
this.paths![i].push(r);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const D2R = Math.PI / 180;
|
|
|
|
|
|
|
|
export class TurtleVM extends BaseVM<TurtleVM> {
|
|
|
|
container: Mesh;
|
2023-02-14 08:27:25 +00:00
|
|
|
meshes: Mesh[] = [];
|
|
|
|
|
2023-02-13 16:38:43 +00:00
|
|
|
counter = 0;
|
|
|
|
sideOrientation = Mesh.BACKSIDE; // TODO: see SideOrientation primitive below
|
|
|
|
|
|
|
|
pen = new PenState();
|
|
|
|
pos = new Vector3();
|
|
|
|
q = new Quaternion();
|
2023-02-14 08:27:25 +00:00
|
|
|
prevQ = this.q;
|
2023-02-13 16:38:43 +00:00
|
|
|
|
|
|
|
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) {
|
2023-02-14 08:27:25 +00:00
|
|
|
this.q = newQ;
|
|
|
|
}
|
|
|
|
|
|
|
|
resetQ(newQ: Quaternion) {
|
|
|
|
this.q = this.prevQ = newQ;
|
2023-02-13 16:38:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2023-02-14 08:27:25 +00:00
|
|
|
this.meshes.push(m);
|
2023-02-13 16:38:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const TurtlePrimitives: Environment<TurtleVM> = Object.assign({}, Primitives, {
|
2023-02-14 08:27:25 +00:00
|
|
|
'Home'() { this.pos = new Vector3(); this.resetQ(new Quaternion()); return []; },
|
2023-02-13 16:38:43 +00:00
|
|
|
|
|
|
|
'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) {
|
2023-02-14 08:27:25 +00:00
|
|
|
this.resetQ(Quaternion.FromEulerAngles(x as number * D2R, y as number * D2R, z as number * D2R));
|
2023-02-13 16:38:43 +00:00
|
|
|
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 []; },
|
|
|
|
|
|
|
|
'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 [];
|
|
|
|
},
|
|
|
|
} satisfies Environment<TurtleVM>);
|
2023-02-14 08:27:25 +00:00
|
|
|
|
|
|
|
function mitredExtrude(
|
|
|
|
name: string,
|
|
|
|
options: { shape: Vector3[], path: Vector3[], close?: boolean },
|
|
|
|
scene: Scene,
|
|
|
|
): Mesh {
|
|
|
|
const shape = options.shape;
|
|
|
|
const path = options.path;
|
|
|
|
const closed = options.close ?? false;
|
|
|
|
|
|
|
|
function miterNormal(v1: Vector3, v2: Vector3): Vector3 {
|
|
|
|
return Vector3.Cross(Vector3.Cross(v2, v1), v2.subtract(v1));
|
|
|
|
}
|
|
|
|
|
|
|
|
var allPaths = [];
|
|
|
|
|
|
|
|
for (var s = 0; s < shape.length; s++) {
|
|
|
|
let axisZ = path[1].subtract(path[0]).normalize();
|
|
|
|
const axisX = Vector3.Cross(scene.activeCamera!.position, axisZ).normalize();
|
|
|
|
const axisY = Vector3.Cross(axisZ, axisX);
|
|
|
|
let startPoint = path[0].add(axisX.scale(shape[s].x)).add(axisY.scale(shape[s].y));
|
|
|
|
const ribbonPath = [startPoint];
|
|
|
|
for (var p = 0; p < path.length - 2; p++) {
|
|
|
|
const nextAxisZ = path[p + 2].subtract(path[p + 1]).normalize();
|
|
|
|
|
|
|
|
startPoint = startPoint.add(axisZ.scale(new Ray(startPoint, axisZ).intersectsPlane(Plane.FromPositionAndNormal(path[p + 1], miterNormal(axisZ, nextAxisZ)))!));
|
|
|
|
|
|
|
|
ribbonPath.push(startPoint);
|
|
|
|
axisZ = nextAxisZ;
|
|
|
|
}
|
|
|
|
// Last Point
|
|
|
|
if (closed) {
|
|
|
|
let nextAxisZ = path[0].subtract(path[path.length - 1]).normalize();
|
|
|
|
startPoint = startPoint.add(axisZ.scale(new Ray(startPoint, axisZ).intersectsPlane(Plane.FromPositionAndNormal(path[path.length - 1], miterNormal(axisZ, nextAxisZ)))!));
|
|
|
|
|
|
|
|
ribbonPath.push(startPoint);
|
|
|
|
axisZ = nextAxisZ;
|
|
|
|
|
|
|
|
nextAxisZ = path[1].subtract(path[0]).normalize();
|
|
|
|
startPoint = startPoint.add(axisZ.scale(new Ray(startPoint, axisZ).intersectsPlane(Plane.FromPositionAndNormal(path[0], miterNormal(axisZ, nextAxisZ)))!));
|
|
|
|
|
|
|
|
ribbonPath.shift();
|
|
|
|
ribbonPath.unshift(startPoint);
|
|
|
|
} else {
|
|
|
|
startPoint = startPoint.add(axisZ.scale(new Ray(startPoint, axisZ).intersectsPlane(Plane.FromPositionAndNormal(path[path.length - 1], axisZ))!));
|
|
|
|
|
|
|
|
ribbonPath.push(startPoint);
|
|
|
|
}
|
|
|
|
allPaths.push(ribbonPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
return MeshBuilder.CreateRibbon(name, {
|
|
|
|
pathArray: allPaths,
|
|
|
|
sideOrientation: Mesh.DOUBLESIDE,
|
|
|
|
closeArray: true,
|
|
|
|
closePath: closed,
|
|
|
|
}, scene);
|
|
|
|
}
|