import Turtle from './Turtle.js' import { Object3D } from '../vendor/Object3D.js' import * as util from './utils.js' const { checkArg, checkArgs } = util /** * Class Turtle3D subclasses {@link Turtles}, adding 3D methods using * Three.js's Object3D module. * See [NetLogo](https://ccl.northwestern.edu/netlogo/docs/3d.html) * who's 3D semantics we follow. * * Just as with Turtle, you do not call `new Turtle3D()`, * instead class Turtles creates Turtle3D instances via * {@link Model} modifying the Turtles/Turtle3D initialization. * * Again, class Turtles is a factory for all of it's Turtle3D instances. * So *don't* do this: */ class Turtle3D extends Turtle { constructor() { super() // needed to avoid Turtle z setting. Remove after fixing z gatters this.obj3d = new Object3D() } newInstance(agentProto) { const insstance = super.newInstance(agentProto) insstance.obj3d = new Object3D() insstance.obj3d.rotation.order = 'ZYX' insstance.reset() return insstance } /** * Resets this turtle's position, rotation and heading all to 0's */ reset() { this.obj3d.position.set(0, 0, 0) this.obj3d.rotation.set(0, 0, 0) this.heading = 0 } /** * Set's this turtle's 3D, x y z, position * * @param {number} x float for x position * @param {number} y float for y position * @param {number} z float for z position */ setxyz(x, y, z) { // checkArgs(arguments) super.setxy(x, y, z) } getxyz() { return this.obj3d.position.toArray() } setRotation(x, y, z) { // checkArgs(arguments) this.obj3d.rotation.set(x, y, z) // super/this.theta = this.obj3d.rotation.z ???? } getRotation() { const { x, y, z } = this.obj3d.rotation // .reorder('ZYX') return [x, y, z] } getThetaPhiPsi() { return this.getRotation().reverse() } getHeadingPitchRoll() { const [psi, phi, theta] = this.getRotation() const heading = util.radToHeading(theta) const pitch = util.radToDeg(-phi) const roll = util.radToDeg(psi) return [heading, pitch, roll] // return [this.heading, this.pitch, this.roll] // ???? } getDxDyDz() { return [this.dx, this.dy, this.dz] } // REMIND: temporary. // handleEdge(x, y, z) { // super.handleEdge(x, y, z) // this.setxyz(this.x, this.y, this.z) // } get x() { return this.obj3d.position.x } set x(d) { // checkArg(d) this.obj3d.position.x = d } get y() { return this.obj3d.position.y } set y(d) { // checkArg(d) this.obj3d.position.y = d } get z() { return this.obj3d.position.z } set z(d) { // checkArg(d) // this.obj3d.position.z = d // This test is needed due to z getter called by super initialization. if (this.obj3d) this.obj3d.position.z = d } // Trap super's setting of theta get theta() { // util.warn('theta is deprecated, use heading instead') return this.obj3d.rotation.z } set theta(rad) { // checkArg(rad) // util.warn('theta is deprecated, use heading instead') if (this.obj3d) this.obj3d.rotation.z = rad } get heading() { return this.model.fromRads(this.obj3d.rotation.z) } set heading(angle) { // checkArg(angle) this.obj3d.rotation.z = this.model.toRads(angle) } get pitch() { // return -this.model.fromRads(this.obj3d.rotation.y) // return -this.model.fromAngleRads(this.obj3d.rotation.y) return -this.model.fromAngleRads(this.obj3d.rotation.y) } set pitch(angle) { // checkArg(angle) // this.obj3d.rotation.y = -this.model.toRads(angle) // this.obj3d.rotation.y = -this.model.toAngleRads(angle) this.obj3d.rotation.y = -this.model.toAngleRads(angle) } get roll() { // return this.model.fromRads(this.obj3d.rotation.x) // return this.model.fromAngleRads(this.obj3d.rotation.x) return this.model.fromAngleRads(this.obj3d.rotation.x) } set roll(angle) { // checkArg(angle) // this.obj3d.rotation.x = this.model.toRads(angle) // this.obj3d.rotation.x = this.model.toAngleRads(angle) this.obj3d.rotation.x = this.model.toAngleRads(angle) } // Move along the turtle's X axis forward(d) { // checkArg(d) const p0 = this.patch this.obj3d.translateX(d) super.checkXYZ(p0) // let [x, y, z] = this.getxyz() // super.setxy(x, y, z) } // Incremental rotation around given axis right(angle) { this.left(-angle) // this.obj3d.rotateZ(-this.model.toAngleRads(angle)) // this.theta = this.obj3d.rotation.z } left(angle) { // checkArg(angle) this.obj3d.rotateZ(this.model.toAngleRads(angle)) // this.right(-angle) } tiltUp(angle) { // this.obj3d.rotateY(-this.model.toAngleRads(angle)) this.tiltDown(-angle) } tiltDown(angle) { // checkArg(angle) this.obj3d.rotateY(this.model.toAngleRads(angle)) // this.tiltUp(-angle) } rollRight(angle) { // checkArg(angle) this.obj3d.rotateX(this.model.toAngleRads(angle)) } rollLeft(angle) { this.rollRight(-angle) } facexyz(x1, y1, z1) { // checkArgs(arguments) // const headingTowards = this.model.toRads(this.towardsXY(x1, y1)) // const pitchTowards = this.model.toRads(this.towardsPitchXYZ(x1, y1, z1) const headingTowards = this.towardsXY(x1, y1) const pitchTowards = this.towardsPitchXYZ(x1, y1, z1) // const roll = this.roll // this.obj3d.rotation.set(0, 0, 0) this.heading = headingTowards this.pitch = pitchTowards // this.roll = roll } face(agent) { // checkArg(agent, 'object') const { x, y, z } = agent this.facexyz(x, y, z) } towardsPitchXYZ(x1, y1, z1) { // checkArgs(arguments) const [x, y, z] = this.getxyz() const [dx, dy, dz] = [x1 - x, y1 - y, z1 - z] const xyhypot = Math.hypot(dx, dy) const pitchRads = Math.atan2(dz, xyhypot) return this.model.fromAngleRads(pitchRads) } towardsPitch(agent) { // checkArg(agent, 'object') const { x, y, z } = agent this.towardsPitchXYZ(x, y, z) } distance(agent) { // checkArg(agent, 'object') const { x, y, z } = agent return this.distanceXYZ(x, y, z) } distanceXYZ(x1, y1, z1) { // checkArgs(arguments) const { x, y, z } = this return util.distance3(x, y, z, x1, y1, z1) } // From https://ccl.northwestern.edu/netlogo/docs/ // Note: dx is simply the sine of the turtle's heading, and dy is simply the cosine. (If this is the reverse of what you expected, it's because in NetLogo a heading of 0 is north and 90 is east, which is the reverse of how angles are usually defined in geometry.) // Note: In earlier versions of NetLogo, these primitives were used in many situations where the new patch-ahead primitive is now more appropriate. // NOTE: dz is simply the sine of the turtle's pitch. Both dx and dy have changed in this case. So, dx = cos(pitch) * sin(heading) and dy = cos(pitch) * cos(heading). get dx() { const { y: pitch, z: heading } = this.obj3d.rotation return Math.cos(pitch) * Math.cos(heading) } get dy() { const { y: pitch, z: heading } = this.obj3d.rotation return Math.cos(pitch) * Math.sin(heading) } get dz() { const pitch = this.obj3d.rotation.y return Math.sin(pitch) } } export default Turtle3D