378 lines
13 KiB
TypeScript
378 lines
13 KiB
TypeScript
import {
|
|
Mesh,
|
|
MeshBuilder,
|
|
Plane,
|
|
Quaternion,
|
|
Ray,
|
|
Scene,
|
|
Vector3,
|
|
VertexData,
|
|
} from '@babylonjs/core/Legacy/legacy';
|
|
|
|
import * as Cat from './cat.js';
|
|
import { earcut } from './earcut';
|
|
|
|
export class PenError extends Cat.RuntimeError {}
|
|
|
|
export class PenState {
|
|
templatePath: Vector3[] = [new Vector3()];
|
|
paths: Vector3[][] | null = null;
|
|
directions: Quaternion[] | null = null;
|
|
|
|
get isDown(): boolean {
|
|
return this.paths !== null;
|
|
}
|
|
|
|
clear() {
|
|
this.templatePath = [new Vector3()];
|
|
this.up();
|
|
}
|
|
|
|
set() {
|
|
if (!this.paths) throw new PenError("Cannot set pen with no paths");
|
|
this.templatePath = this.paths[0];
|
|
this.up();
|
|
}
|
|
|
|
down() {
|
|
this.paths = this.templatePath.map(_p => []);
|
|
this.directions = [];
|
|
}
|
|
|
|
up() {
|
|
this.paths = null;
|
|
this.directions = null;
|
|
}
|
|
|
|
push(pos: Vector3, q: Quaternion, miter: boolean) {
|
|
const directions = this.directions!;
|
|
const paths = this.paths!;
|
|
|
|
if (miter && directions.length > 0) {
|
|
const lastQ = directions[directions.length - 1];
|
|
const lastDir = new Vector3(0, 0, 1).applyRotationQuaternionInPlace(lastQ);
|
|
const thisDir = new Vector3(0, 0, 1).applyRotationQuaternionInPlace(q);
|
|
const miterDir = lastDir.add(thisDir).normalize();
|
|
const miterPlane = Plane.FromPositionAndNormal(pos, miterDir);
|
|
for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) {
|
|
const steps = paths[pathIndex];
|
|
const p = steps[steps.length - 1];
|
|
const d = pointDistance(p, lastDir, miterPlane);
|
|
if (d !== null) steps[steps.length - 1] = p.add(lastDir.scale(d));
|
|
}
|
|
this.templatePath.forEach((p, i) => {
|
|
const r = p.applyRotationQuaternion(q).addInPlace(pos);
|
|
const d = pointDistance(r, thisDir, miterPlane);
|
|
if (d !== null) r.addInPlace(thisDir.scale(d));
|
|
paths[i].push(r);
|
|
});
|
|
} else {
|
|
this.templatePath.forEach((p, i) => {
|
|
const r = p.applyRotationQuaternion(q).addInPlace(pos);
|
|
paths[i].push(r);
|
|
});
|
|
}
|
|
|
|
directions.push(q);
|
|
}
|
|
};
|
|
|
|
// Adapted from Ray.intersectsPlane
|
|
function pointDistance(origin: Vector3, direction: Vector3, plane: Plane): number | null {
|
|
const result1 = Vector3.Dot(plane.normal, direction);
|
|
if (Math.abs(result1) < 1e-6) return null; // direction parallel to plane
|
|
const result2 = Vector3.Dot(plane.normal, origin);
|
|
return (-plane.d - result2) / result1;
|
|
}
|
|
|
|
export enum CapType {
|
|
NONE,
|
|
START,
|
|
END,
|
|
BOTH,
|
|
};
|
|
|
|
export class TurtleVM extends Cat.VM<TurtleVM> {
|
|
container: Mesh;
|
|
meshes: Mesh[] = [];
|
|
wireframe = false;
|
|
cap: CapType = CapType.BOTH;
|
|
|
|
counter = 0;
|
|
sideOrientation = Mesh.DEFAULTSIDE;
|
|
|
|
pen = new PenState();
|
|
pos = new Vector3();
|
|
q = new Quaternion();
|
|
smooth = false;
|
|
miter = false;
|
|
|
|
get euler(): Vector3 {
|
|
return this.q.toEulerAngles();
|
|
}
|
|
|
|
constructor(
|
|
name: string,
|
|
scene: Scene | null,
|
|
program: Cat.Input,
|
|
) {
|
|
super(program, TurtlePrimitives);
|
|
this.container = new Mesh(name, scene);
|
|
}
|
|
|
|
forwardBy(dist: number) {
|
|
const v = new Vector3(0, 0, dist).applyRotationQuaternionInPlace(this.q);
|
|
this.pos.addInPlace(v);
|
|
if (this.pen.isDown) this.pen.push(this.pos, this.q, dist === 0.0 && this.miter);
|
|
}
|
|
|
|
rotate(y: number, x: number, z: number) {
|
|
const e = this.euler;
|
|
e.addInPlaceFromFloats(x * Cat.D2R, y * Cat.D2R, z * Cat.D2R);
|
|
this.q = e.toQuaternion();
|
|
}
|
|
|
|
relativeRotate(y: number, x: number, z: number) {
|
|
this.q = this.q.multiply(Quaternion.FromEulerAngles(x * Cat.D2R, y * Cat.D2R, z * Cat.D2R));
|
|
}
|
|
|
|
penDown() {
|
|
this.pen.down();
|
|
this.pen.push(this.pos, this.q, false);
|
|
}
|
|
|
|
penUp(close: boolean) {
|
|
if (!this.pen.isDown) return;
|
|
|
|
if (close) {
|
|
throw new Error('todo');
|
|
}
|
|
|
|
if (this.wireframe) {
|
|
this.emitWireframe();
|
|
} else {
|
|
this.emitSolid();
|
|
}
|
|
|
|
this.pen.up();
|
|
}
|
|
|
|
emitWireframe() {
|
|
const lines: Vector3[][] = [];
|
|
const paths = this.pen.paths!;
|
|
const pathCount = paths.length;
|
|
const stepCount = paths[0].length;
|
|
|
|
if (this.cap === CapType.START || this.cap === CapType.BOTH) {
|
|
lines.push(paths.map(p => p[0]));
|
|
}
|
|
|
|
for (let pathIndex = 1; pathIndex < pathCount; pathIndex++) {
|
|
for (let stepIndex = 1; stepIndex < stepCount; stepIndex++) {
|
|
lines.push([paths[pathIndex - 1][stepIndex - 1],
|
|
paths[pathIndex - 1][stepIndex],
|
|
paths[pathIndex][stepIndex],
|
|
paths[pathIndex][stepIndex - 1]]);
|
|
}
|
|
}
|
|
|
|
if (this.cap === CapType.END || this.cap === CapType.BOTH) {
|
|
lines.push(paths.map(p => p[p.length - 1]));
|
|
}
|
|
|
|
const meshName = this.container.name + this.counter++;
|
|
const ls = MeshBuilder.CreateLineSystem(meshName, { lines }, null);
|
|
ls.parent = this.container;
|
|
this.meshes.push(ls);
|
|
}
|
|
|
|
emitSolid() {
|
|
const positions: number[] = [];
|
|
const indices: number[] = [];
|
|
const normals: number[] = [];
|
|
const uvs: number[] = [];
|
|
|
|
const paths = this.pen.paths!;
|
|
const pathCount = paths.length;
|
|
const stepCount = paths[0].length;
|
|
|
|
const us: number[][] = [];
|
|
const vs: number[][] = [];
|
|
const uTotal: number[] = [];
|
|
const vTotal: number[] = [];
|
|
|
|
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
|
|
uTotal.push(0);
|
|
us.push([]);
|
|
let prev = paths[pathIndex][0];
|
|
for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
|
|
const curr = paths[pathIndex][stepIndex];
|
|
uTotal[pathIndex] += curr.subtract(prev).length();
|
|
us[pathIndex][stepIndex] = uTotal[pathIndex];
|
|
prev = curr;
|
|
}
|
|
}
|
|
|
|
for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
|
|
vTotal.push(0);
|
|
vs.push([]);
|
|
let prev = paths[0][stepIndex];
|
|
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
|
|
const curr = paths[pathIndex][stepIndex];
|
|
vTotal[stepIndex] += curr.subtract(prev).length();
|
|
vs[stepIndex][pathIndex] = vTotal[stepIndex];
|
|
prev = curr;
|
|
}
|
|
}
|
|
|
|
function pushPoint(pathIndex: number, stepIndex: number): number {
|
|
const pointIndex = positions.length / 3;
|
|
const p = paths[pathIndex][stepIndex];
|
|
positions.push(... p.asArray());
|
|
uvs.push((p.x - p.z), (p.y));
|
|
// uvs.push((p.x - p.z) / 1.2, (p.y) / 0.9);
|
|
// uvs.push(us[pathIndex][stepIndex] / uTotal[pathIndex],
|
|
// vs[stepIndex][pathIndex] / vTotal[stepIndex]);
|
|
return pointIndex;
|
|
}
|
|
|
|
const cachedIndices: { [key: string]: number } = {};
|
|
function cachedPoint(pathIndex: number, stepIndex: number): number {
|
|
const v = paths[pathIndex][stepIndex].asArray().map(n => n.toFixed(3)).toString();
|
|
if (!(v in cachedIndices)) cachedIndices[v] = pushPoint(pathIndex, stepIndex);
|
|
return cachedIndices[v];
|
|
}
|
|
|
|
const computePointIndex = this.smooth ? cachedPoint : pushPoint;
|
|
|
|
const capIndices = this.cap === CapType.NONE
|
|
? []
|
|
: earcut(this.pen.templatePath.flatMap(p => [p.x, p.y]), void 0, 2);
|
|
|
|
function cap(stepIndex: number, a: number, b: number, c: number) {
|
|
for (let i = 0; i < capIndices.length; i += 3) {
|
|
indices.push(computePointIndex(capIndices[i+a], stepIndex),
|
|
computePointIndex(capIndices[i+b], stepIndex),
|
|
computePointIndex(capIndices[i+c], stepIndex));
|
|
}
|
|
}
|
|
|
|
if (this.cap === CapType.START || this.cap === CapType.BOTH) {
|
|
cap(0, 0, 1, 2);
|
|
}
|
|
|
|
for (let pathIndex = 1; pathIndex < pathCount; pathIndex++) {
|
|
for (let stepIndex = 1; stepIndex < stepCount; stepIndex++) {
|
|
indices.push(computePointIndex(pathIndex - 1, stepIndex - 1),
|
|
computePointIndex(pathIndex - 1, stepIndex),
|
|
computePointIndex(pathIndex, stepIndex - 1));
|
|
|
|
indices.push(computePointIndex(pathIndex - 1, stepIndex),
|
|
computePointIndex(pathIndex, stepIndex),
|
|
computePointIndex(pathIndex, stepIndex - 1));
|
|
}
|
|
}
|
|
|
|
if (this.cap === CapType.END || this.cap === CapType.BOTH) {
|
|
cap(stepCount - 1, 2, 1, 0);
|
|
}
|
|
|
|
VertexData.ComputeNormals(positions, indices, normals);
|
|
VertexData._ComputeSides(this.sideOrientation, positions, indices, normals, uvs);
|
|
|
|
const vertexData = new VertexData();
|
|
vertexData.positions = new Float32Array(positions);
|
|
vertexData.indices = new Int32Array(indices);
|
|
vertexData.normals = new Float32Array(normals);
|
|
vertexData.uvs = new Float32Array(uvs);
|
|
|
|
const m = new Mesh(this.container.name + this.counter++);
|
|
vertexData.applyToMesh(m);
|
|
m.parent = this.container;
|
|
this.meshes.push(m);
|
|
}
|
|
}
|
|
|
|
export const TurtlePrimitives: Cat.Environment<TurtleVM> = Object.assign({}, Cat.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'(v) {
|
|
const [x, y, z] = v as number[];
|
|
this.pos.set(x as number, y as number, z as number);
|
|
return [];
|
|
},
|
|
|
|
'GetHeading'() { return [this.euler.asArray().map(v => v / Cat.D2R)]; },
|
|
'GetRX'() { return [this.euler.x / Cat.D2R]; },
|
|
'GetRY'() { return [this.euler.y / Cat.D2R]; },
|
|
'GetRZ'() { return [this.euler.z / Cat.D2R]; },
|
|
'SetRX'(v) { this.q.x = v as number * Cat.D2R; return []; },
|
|
'SetRY'(v) { this.q.y = v as number * Cat.D2R; return []; },
|
|
'SetRZ'(v) { this.q.z = v as number * Cat.D2R; return []; },
|
|
'SetHeading'(v) {
|
|
const [x, y, z] = v as number[];
|
|
this.q = Quaternion.FromEulerAngles(x as number * Cat.D2R, y as number * Cat.D2R, z as number * Cat.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 []; },
|
|
'DefinePen'() { this.pen.set(); return []; },
|
|
'GetPen'() { return [this.pen.templatePath.map(v => v.asArray())]; },
|
|
'SetPen'(p0) {
|
|
const p = p0 as [number, number, number][];
|
|
if (this.pen.isDown && this.pen.templatePath.length !== p.length) {
|
|
throw new PenError("Cannot set pen with different number of points when pen is down");
|
|
}
|
|
this.pen.templatePath = p.map(v => Vector3.FromArray(v));
|
|
return [];
|
|
},
|
|
|
|
'PenDown'() { this.penDown(); return []; },
|
|
'PenUp'() { this.penUp(false); return []; },
|
|
'Close'() { this.penUp(true); return []; },
|
|
|
|
'SetCap'(t) {
|
|
const n = t as keyof typeof CapType;
|
|
if (typeof t === 'string' && n in CapType) {
|
|
this.cap = CapType[n];
|
|
} else {
|
|
throw new Cat.TypeError("Bad cap type: " + t);
|
|
}
|
|
return [];
|
|
},
|
|
'SetWireframe'(b) { this.wireframe = b as boolean; return []; },
|
|
'SetSmooth'(b) { this.smooth = b as boolean; return []; },
|
|
'SetMiter'(b) { this.miter = b as boolean; return []; },
|
|
'SetSideOrientation'(s) {
|
|
switch (s) {
|
|
case "DEFAULT": this.sideOrientation = Mesh.DEFAULTSIDE; break;
|
|
case "FRONT": this.sideOrientation = Mesh.FRONTSIDE; break;
|
|
case "BACK": this.sideOrientation = Mesh.BACKSIDE; break;
|
|
case "DOUBLE": this.sideOrientation = Mesh.DOUBLESIDE; break;
|
|
default: throw new TypeError("Invalid SideOrientation: " + s);
|
|
}
|
|
return [];
|
|
},
|
|
} satisfies Cat.Environment<TurtleVM>);
|