import { Mesh, MeshBuilder, Plane, Quaternion, Scene, Vector3, VertexData, } from '@babylonjs/core/Legacy/legacy'; import { stringify } from '@preserves/core'; 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 { 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 = Object.assign( {}, Cat.Primitives, Cat.primitiveEnvironment({ '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: " + stringify(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: " + stringify(s)); } return []; }, }) satisfies Cat.Environment, );