Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active November 9, 2016 02:50
Show Gist options
  • Save mathdoodle/d8a63a69cb83a7259a0e871c495f5efc to your computer and use it in GitHub Desktop.
Save mathdoodle/d8a63a69cb83a7259a0e871c495f5efc to your computer and use it in GitHub Desktop.
Scalars, Vectors, and Units of Measure

Scalars, Vectors, and Units of Measure

Overview

The purpose of this learning unit is to show how units of measure can be integrated with scalars and vectors to describe physical measures.

The exercise involved building a simulation of a cart travelling on a track with attention to scaling and units.

A useful software artifact from this exercise will be abstractions for representing scalars and vectors with units.

For expediency, the Unit class from the UNITS library will be used to represent the S.I. unit of measure. Mentors should be prepared to explain how units relate to dimensional analysis and physcial standards.

Remarks

TODO: How to leave a trail using three.js?

const heightCone = 0.2
const radiusCone = 0.08
const radiusStem = 0.01
const heightStem = 0.8
const radiusSegments = 16
export default class Arrow extends THREE.Group {
private material: THREE.MeshLambertMaterial;
constructor() {
super()
this.material = new THREE.MeshLambertMaterial()
const cone = new THREE.Mesh(new THREE.CylinderGeometry(0, radiusCone, heightCone, radiusSegments), this.material)
cone.position.y = (heightCone / 2) + heightStem
const stem = new THREE.Mesh(new THREE.CylinderGeometry(radiusStem, radiusStem, heightStem, radiusSegments), this.material)
stem.position.y = heightStem / 2
this.add(cone)
this.add(stem)
}
get color() {
return this.material.color
}
set color(value) {
this.material.color.copy(value)
}
get axis(): THREE.Vector3 {
throw new Error("TODO")
}
set axis(value: THREE.Vector3) {
// The quaternion is the attitude.
const magnitude = value.length()
const direction = value.clone().normalize()
// The reference attitude is e2.
const vFrom = new THREE.Vector3(0, 1, 0)
const vTo = new THREE.Vector3(direction.x, direction.y, direction.z)
this.quaternion.setFromUnitVectors(vFrom, vTo)
this.scale.x = magnitude
this.scale.y = magnitude
this.scale.z = magnitude
}
}
function makeGeometry(size = 1): THREE.BufferGeometry {
const vertices = new Float32Array( [
0, 0, 0, size, 0, 0,
0, 0, 0, 0, size, 0,
0, 0, 0, 0, 0, size
] );
const colors = new Float32Array( [
1, 0, 0, 1, 0, 0,
0, 1, 0, 0, 1, 0,
0, 0, 1, 0, 0, 1
] );
const geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
return geometry;
}
function makeMaterial() {
return new THREE.LineBasicMaterial({vertexColors: THREE.VertexColors});
}
export default class AxisHelper extends THREE.LineSegments {
constructor(size = 1) {
super(makeGeometry(size), makeMaterial());
}
}
import Scalar from './Scalar'
import Vector from './Vector'
export default class Box {
public avatar: THREE.Mesh
constructor(public width: Scalar, public height: Scalar, public depth: Scalar) {
// TODO: Perform a conversion and verify that units are correct.
const geometry = new THREE.BoxGeometry(width.quantity, height.quantity, depth.quantity)
const material = new THREE.MeshLambertMaterial({color: 0xFFFFFF, wireframe: false})
this.avatar = new THREE.Mesh(geometry, material)
}
get position(): Vector {
// TODO: Perform a conversion and verify that units are correct.
const position = this.avatar.position
const x = position.x
const y = position.y
const z = position.z
return new Vector(x, y, z, UNITS.Unit.METER)
}
set position(value: Vector) {
// console.log(`position => ${JSON.stringify(value)}`)
// TODO: Perform a conversion and verify that units are correct.
const position = this.avatar.position
position.set(value.x, value.y, value.z)
}
}
const noopTimer = {
start: function() {},
elapsed: function() { return 0; }
};
export default class ConsoleReporter implements jasmine.Reporter {
private totalSpecDefined: number;
private timer = noopTimer;
constructor(options?) {
console.log("ConsoleReporter.constructor()")
}
jasmineStarted(options: {totalSpecsDefined: number}): void {
console.log(`jasmineStarted ${JSON.stringify(options, null, 2)}`);
this.totalSpecDefined = options.totalSpecsDefined || 0;
this.timer.start();
}
jasmineDone(options: {order: {random: boolean; seed: string}}): void {
console.log(`jasmineDone ${JSON.stringify(options, null, 2)}`);
}
suiteStarted(result: {id: string; description: string, fullName: string, failedExpectations: jasmine.Expectation[]}): void {
console.log(`suiteStarted ${JSON.stringify(result, null, 2)}`);
}
suiteDone(result: {id: string; description: string, fullName: string, failedExpectations: any[]; status: string}): void {
console.log(`suiteDone ${JSON.stringify(result, null, 2)}`);
}
specStarted(result: {id: string; description: string, fullName: string, failedExpectations: any[]; passedExpectations: any[]; pendingReason: string}): void {
console.log(`specStarted ${JSON.stringify(result, null, 2)}`);
}
specDone(result: {id: string; description: string, fullName: string, failedExpectations: any[]; passedExpectations: any[]; pendingReason: string}): void {
console.log(`specDone ${JSON.stringify(result, null, 2)}`);
}
}
export const color = {
red: new THREE.Color(1, 0, 0),
green: new THREE.Color(0, 1, 0),
blue: new THREE.Color(0, 0, 1),
cyan: new THREE.Color(0, 1, 1),
magenta: new THREE.Color(1, 0, 1),
yellow: new THREE.Color(1, 1, 0),
black: new THREE.Color(0, 0, 0),
white: new THREE.Color(1, 1, 1)
}
export function windowResize(renderer: THREE.Renderer, camera: THREE.PerspectiveCamera){
const callback = function() {
renderer.setSize( window.innerWidth, window.innerHeight );
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', callback, false);
callback();
return {
/**
*
*/
resize: function() {
callback();
},
/**
* Stop watching window resize
*/
stop : function() {
window.removeEventListener('resize', callback);
}
};
}
const noopTimer = {
start: function() {},
elapsed: function() { return 0; }
};
export default class HtmlReporter implements jasmine.Reporter {
private totalSpecDefined: number;
private timer = noopTimer;
constructor(options?) {
console.log("HtmlReporter.constructor()")
}
initialize(): void {
}
jasmineStarted(options: {totalSpecsDefined: number}): void {
console.log(`jasmineStarted ${JSON.stringify(options, null, 2)}`);
this.totalSpecDefined = options.totalSpecsDefined || 0;
this.timer.start();
}
jasmineDone(options: {order: {random: boolean; seed: string}}): void {
console.log(`jasmineDone ${JSON.stringify(options, null, 2)}`);
}
suiteStarted(result: {id: string; description: string, fullName: string, failedExpectations: jasmine.Expectation[]}): void {
console.log(`suiteStarted ${JSON.stringify(result, null, 2)}`);
}
suiteDone(result: {id: string; description: string, fullName: string, failedExpectations: any[]; status: string}): void {
console.log(`suiteDone ${JSON.stringify(result, null, 2)}`);
}
specStarted(result: {id: string; description: string, fullName: string, failedExpectations: any[]; passedExpectations: any[]; pendingReason: string}): void {
console.log(`specStarted ${JSON.stringify(result, null, 2)}`);
}
specDone(result: {id: string; description: string, fullName: string, failedExpectations: any[]; passedExpectations: any[]; pendingReason: string}): void {
console.log(`specDone ${JSON.stringify(result, null, 2)}`);
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>My first Three.js app</title>
<!-- STYLES-MARKER -->
<style>
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100% }
</style>
<!-- SCRIPTS-MARKER -->
<script src="https://jspm.io/system.js"></script>
</head>
<body>
<script>
// CODE-MARKER
</script>
<script>
System.import('./index.js')
</script>
</body>
</html>
import Arrow from './Arrow'
import AxisHelper from './AxisHelper'
import Box from './Box'
import OrbitControls from './OrbitControls'
import {color, windowResize} from './extras'
import Scalar from './Scalar'
import Vector from './Vector'
const unitI = new Vector(1, 0, 0, void 0)
const unitJ = new Vector(0, 1, 0, void 0)
const unitK = new Vector(0, 0, 1, void 0)
const meter = new Scalar(1, UNITS.Unit.METER)
const second = new Scalar(1, UNITS.Unit.SECOND)
const cm = new Scalar(0.01, UNITS.Unit.METER)
/**
* +1 if going from left to right, otherwise -1.
*/
const leftToRight = -1
let velocity = (leftToRight * 0.2 * unitI + 0.0 * unitJ) * meter / second
const renderer = new THREE.WebGLRenderer({antialias: true})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
const scene = new THREE.Scene()
const aspectRatio = window.innerWidth/window.innerHeight
const camera = new THREE.PerspectiveCamera(45, aspectRatio, 0.001, 1000)
const ambLight = new THREE.AmbientLight(0x444444)
scene.add(ambLight)
const dirLight = new THREE.DirectionalLight()
scene.add(dirLight)
const track = new Box(2 * meter, 5 * cm, 10 * cm)
scene.add(track.avatar)
const cart = new Box(10 * cm, 4 * cm, 6 * cm)
// TypeScript doesn't know how to compute the type of an inhomogeneous product.
// It assumes that the type is that of the left hand operand.
// So we order our product that way (Vector times Scalar).
cart.position = leftToRight * unitI * (0.5 * (cart.width - track.width)) + unitJ * (0.5 * track.height + 0.5 * cart.height + cm)
scene.add(cart.avatar)
// const arrow = new Arrow()
// arrow.color = color.yellow
// arrow.axis = unitI + unitJ + unitK
// arrow.position.copy(origin)
// scene.add(arrow)
const axes = new AxisHelper(2)
scene.add(axes)
// We've monkey-patched THREE.Vector3 to support scalar multiplication!
// camera.position.copy(5 * unitK + 2 * unitJ + unitI).normalize().multiplyScalar(2)
camera.position.z = 3
const controls = new OrbitControls(camera, renderer.domElement)
const resizer = windowResize(renderer, camera).resize()
const ΔT = 0.01 * second
// Using a function expression rather than a function declaration circumvents
// issue caused by function hoisting.
const render = function() {
requestAnimationFrame(render)
dirLight.position.copy(camera.position)
cart.position = cart.position + velocity * ΔT
controls.update()
renderer.render(scene, camera)
}
requestAnimationFrame(render)
/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
* @author David Geo Holmes (TypeScript migration)
*/
const EPS = 0.000001;
// 65 /*A*/, 83 /*S*/, 68 /*D*/
const keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 };
export default class OrbitControls {
private object: THREE.Object3D;
private domElement;
public enabled = true;
public center = new THREE.Vector3();
public userZoom = true;
public userZoomSpeed = 1.0;
public userRotate = true;
public userRotateSpeed = 1.0;
public userPan = true;
public userPanSpeed = 2.0;
public autoRotate = false;
public autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
public minPolarAngle = 0; // radians
public maxPolarAngle = Math.PI; // radians
public minDistance = 0;
public maxDistance = Infinity;
private phiDelta = 0;
private thetaDelta = 0;
private scale = 1;
private lastPosition = new THREE.Vector3();
constructor( object: THREE.Object3D , domElement: HTMLCanvasElement ) {
this.object = object;
this.domElement = ( domElement !== undefined ) ? domElement : document;
var PIXELS_PER_ROUND = 1800;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var zoomStart = new THREE.Vector2();
var zoomEnd = new THREE.Vector2();
var zoomDelta = new THREE.Vector2();
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 };
var state = STATE.NONE;
// events
var changeEvent = { type: 'change' };
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
const onMouseDown = (event: MouseEvent) => {
if (this.enabled === false ) return;
if (this.userRotate === false ) return;
event.preventDefault();
if ( state === STATE.NONE )
{
if ( event.button === 0 )
state = STATE.ROTATE;
if ( event.button === 1 )
state = STATE.ZOOM;
if ( event.button === 2 )
state = STATE.PAN;
}
if ( state === STATE.ROTATE ) {
//state = STATE.ROTATE;
rotateStart.set( event.clientX, event.clientY );
}
else if ( state === STATE.ZOOM ) {
//state = STATE.ZOOM;
zoomStart.set( event.clientX, event.clientY );
}
else if ( state === STATE.PAN ) {
//state = STATE.PAN;
}
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
}
this.domElement.addEventListener( 'mousedown', onMouseDown, false );
const onMouseMove = (event: MouseEvent) => {
if (this.enabled === false ) return;
event.preventDefault();
if ( state === STATE.ROTATE ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart );
this.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * this.userRotateSpeed );
this.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * this.userRotateSpeed );
rotateStart.copy( rotateEnd );
}
else if ( state === STATE.ZOOM ) {
zoomEnd.set( event.clientX, event.clientY );
zoomDelta.subVectors( zoomEnd, zoomStart );
if ( zoomDelta.y > 0 ) {
this.zoomIn();
}
else {
this.zoomOut();
}
zoomStart.copy( zoomEnd );
} else if ( state === STATE.PAN ) {
const movementX: number = event['movementX'] || event['mozMovementX'] || event['webkitMovementX'] || 0;
const movementY = event['movementY'] || event['mozMovementY'] || event['webkitMovementY'] || 0;
this.pan( new THREE.Vector3( - movementX, movementY, 0 ) );
}
}
const onMouseUp = (event: MouseEvent) => {
if (this.enabled === false) return;
if (this.userRotate === false) return;
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
state = STATE.NONE;
}
const onMouseWheel = (event: MouseWheelEvent) => {
if (this.enabled === false ) return;
if (this.userZoom === false ) return;
let delta = 0;
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9
delta = event.wheelDelta;
}
else if ( event.detail ) { // Firefox
delta = - event.detail;
}
if ( delta > 0 ) {
this.zoomOut();
}
else {
this.zoomIn();
}
}
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
const onKeyDown = (event: KeyboardEvent) => {
if (this.enabled === false) return;
if (this.userPan === false) return;
switch ( event.keyCode ) {
/*case scope.keys.UP:
scope.pan( new THREE.Vector3( 0, 1, 0 ) );
break;
case scope.keys.BOTTOM:
scope.pan( new THREE.Vector3( 0, - 1, 0 ) );
break;
case scope.keys.LEFT:
scope.pan( new THREE.Vector3( - 1, 0, 0 ) );
break;
case scope.keys.RIGHT:
scope.pan( new THREE.Vector3( 1, 0, 0 ) );
break;
*/
case keys.ROTATE:
state = STATE.ROTATE;
break;
case keys.ZOOM:
state = STATE.ZOOM;
break;
case keys.PAN:
state = STATE.PAN;
break;
}
}
const onKeyUp = (event: KeyboardEvent) => {
switch (event.keyCode) {
case keys.ROTATE:
case keys.ZOOM:
case keys.PAN:
state = STATE.NONE;
break;
}
}
window.addEventListener( 'keydown', onKeyDown, false );
window.addEventListener( 'keyup', onKeyUp, false );
}
private getAutoRotationAngle(): number {
return 2 * Math.PI / 60 / 60 * this.autoRotateSpeed;
}
private getZoomScale(): number {
return Math.pow( 0.95, this.userZoomSpeed );
}
rotateLeft(angle?: number): void {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.thetaDelta -= angle;
}
rotateRight(angle?: number): void {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.thetaDelta += angle;
}
rotateUp(angle?: number) {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.phiDelta -= angle;
}
rotateDown(angle?: number) {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.phiDelta += angle;
}
zoomIn(zoomScale?: number): void {
if ( zoomScale === undefined ) {
zoomScale = this.getZoomScale();
}
this.scale /= zoomScale;
}
zoomOut(zoomScale?: number): void {
if (zoomScale === undefined) {
zoomScale = this.getZoomScale();
}
this.scale *= zoomScale;
}
pan( distance: THREE.Vector3 ) {
distance.transformDirection( this.object.matrix );
distance.multiplyScalar( this.userPanSpeed );
this.object.position.add( distance );
this.center.add( distance );
}
update() {
const position = this.object.position;
const offset = position.clone().sub( this.center );
// angle from z-axis around y-axis
let theta = Math.atan2( offset.x, offset.z );
// angle from y-axis
let phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
if (this.autoRotate) {
this.rotateLeft(this.getAutoRotationAngle());
}
theta += this.thetaDelta;
phi += this.phiDelta;
// restrict phi to be between desired limits
phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
// restrict phi to be betwee EPS and PI-EPS
phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
var radius = offset.length() * this.scale;
// restrict radius to be between desired limits
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
offset.x = radius * Math.sin( phi ) * Math.sin( theta );
offset.y = radius * Math.cos( phi );
offset.z = radius * Math.sin( phi ) * Math.cos( theta );
position.copy( this.center ).add( offset );
this.object.lookAt( this.center );
this.thetaDelta = 0;
this.phiDelta = 0;
this.scale = 1;
if ( this.lastPosition.distanceTo( this.object.position ) > 0 ) {
// this.dispatchEvent( changeEvent );
this.lastPosition.copy( this.object.position );
}
}
}
{
"name": "scalars-vectors-units",
"version": "0.1.0",
"dependencies": {
"DomReady": "1.0.0",
"three.js": "0.82.0",
"davinci-units": "1.5.4",
"jasmine": "2.4.1"
},
"description": "Scalars, Vectors, and Units of Measure",
"keywords": [
"three.js",
"THREE"
],
"operatorOverloading": true,
"author": "David Geo Holmes"
}
import Scalar from './Scalar'
export default function() {
describe("constructor", function() {
it("should initialize parts", function() {
const s = new Scalar(2, UNITS.Unit.METER)
expect(s).toBeDefined()
expect(s.quantity).toBe(2)
expect(s.uom).toBeDefined()
expect(s.uom.multiplier).toBe(1)
expect(s.uom.dimensions.L.numer).toBe(1)
expect(s.uom.dimensions.L.denom).toBe(1)
})
it("should allow dimensionless measures", function() {
const s = new Scalar(2, void 0)
expect(s).toBeDefined()
expect(s.quantity).toBe(2)
expect(s.uom).toBeUndefined()
})
})
describe("__add__", function() {
const one = new Scalar(1, UNITS.Unit.METER)
const two = new Scalar(2, UNITS.Unit.METER)
const out = one.__add__(two)
describe("(1 m), (2 m)", function() {
})
it("quantity should be correct", function() {
expect(out.quantity).toBe(3)
})
it("uom should be correct", function() {
expect(out.uom.dimensions.M.numer).toBe(0)
expect(out.uom.dimensions.M.denom).toBe(1)
expect(out.uom.dimensions.L.numer).toBe(1)
expect(out.uom.dimensions.L.denom).toBe(1)
expect(out.uom.dimensions.M.numer).toBe(0)
expect(out.uom.dimensions.M.denom).toBe(1)
})
})
describe("__mul__", function() {
const one = new Scalar(1, UNITS.Unit.METER)
const two = new Scalar(2, UNITS.Unit.METER)
const out = one.__mul__(two)
describe("(1 m), (2 m)", function() {
})
it("quantity should be correct", function() {
expect(out.quantity).toBe(2)
})
it("uom should be correct", function() {
expect(out.uom.dimensions.M.numer).toBe(0)
expect(out.uom.dimensions.M.denom).toBe(1)
expect(out.uom.dimensions.L.numer).toBe(2)
expect(out.uom.dimensions.L.denom).toBe(1)
expect(out.uom.dimensions.M.numer).toBe(0)
expect(out.uom.dimensions.M.denom).toBe(1)
})
})
describe("__div__", function() {
const one = new Scalar(1, UNITS.Unit.METER)
const two = new Scalar(2, UNITS.Unit.METER)
const out = one.__div__(two)
describe("(1 m), (2 m)", function() {
})
it("quantity should be correct", function() {
expect(out.quantity).toBe(0.5)
})
it("uom should be correct", function() {
expect(out.uom.dimensions.M.numer).toBe(0)
expect(out.uom.dimensions.M.denom).toBe(1)
expect(out.uom.dimensions.L.numer).toBe(0)
expect(out.uom.dimensions.L.denom).toBe(1)
expect(out.uom.dimensions.M.numer).toBe(0)
expect(out.uom.dimensions.M.denom).toBe(1)
})
})
describe("something", function() {
it("should be OK", function() {
pending("Demonstration of how to mark a specification as pending.")
})
})
}
function isNumber(x: any): x is number {
return typeof x === 'number'
}
function isScalar(x: any): x is Scalar {
return x instanceof Scalar
}
export default class Scalar {
constructor(public quantity: number, public uom: UNITS.Unit) {
}
__add__(rhs: Scalar): Scalar {
return new Scalar(this.quantity + rhs.quantity, UNITS.Unit.compatible(this.uom, rhs.uom))
}
__sub__(rhs: Scalar): Scalar {
return new Scalar(this.quantity - rhs.quantity, UNITS.Unit.compatible(this.uom, rhs.uom))
}
__mul__(rhs: Scalar | number): Scalar {
if (isScalar(rhs)) {
return new Scalar(this.quantity * rhs.quantity, UNITS.Unit.mul(this.uom, rhs.uom))
}
else if (isNumber(rhs)) {
return new Scalar(this.quantity * rhs, this.uom)
}
else {
console.log("Scalar.__mul__ ignored.")
return void 0
}
}
__rmul__(lhs: Scalar | number): Scalar {
if (isScalar(lhs)) {
return new Scalar(lhs.quantity * this.quantity, UNITS.Unit.mul(this.uom, lhs.uom))
}
else if (isNumber(lhs)) {
return new Scalar(lhs * this.quantity, this.uom)
}
else {
console.log("Scalar.__rmul__ ignored.")
return void 0
}
}
__div__(rhs: Scalar): Scalar {
return new Scalar(this.quantity / rhs.quantity, UNITS.Unit.div(this.uom, rhs.uom))
}
__neg__(): Scalar {
return new Scalar(-this.quantity, this.uom)
}
toString(): string {
return `${this.quantity} * ${this.uom}`
}
}
<!DOCTYPE html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<script src='https://jspm.io/system.js'></script>
<!-- SCRIPTS-MARKER -->
</head>
<body>
<script>
// CODE-MARKER
</script>
<script>
System.import('./tests.js')
</script>
</body>
</html>
import ConsoleReporter from './ConsoleReporter';
import HtmlReporter from './HtmlReporter';
import Scalar from './Scalar.spec';
import Vector from './Vector.spec';
const jasmineCore: jasmine.Jasmine = jasmineRequire.core(jasmineRequire)
// We shouldn't need to do this...
jasmineRequire.html(jasmineCore)
const env = jasmineCore.getEnv()
// Build the collection of functions that we use in our specs and put them in the global namespace.
extend(window, jasmineRequire.interface(jasmineCore, env))
const reporter = new jasmine.HtmlReporter({
env: env,
getContainer: function() { return document.body },
createElement: function() { return document.createElement.apply(document, arguments) },
createTextNode: function() { return document.createTextNode.apply(document, arguments) },
timer: new jasmine.Timer()
})
env.addReporter(reporter)
// env.addReporter(new ConsoleReporter())
DomReady.ready(function() {
reporter.initialize()
describe("Scalar", Scalar)
describe("Vector", Vector)
env.execute()
})
/*
* Helper function for extending the properties on objects.
*/
export default function extend<T>(destination: T, source: any): T {
for (let property in source) {
destination[property] = source[property]
}
return destination
}
import Vector from './Vector'
export default function() {
describe("constructor", function() {
const unitI = new Vector(1, 0, 0, void 0)
const unitJ = new Vector(0, 1, 0, void 0)
const unitK = new Vector(0, 0, 1, void 0)
it("unitI should be [1, 0, 0] in the standard basis.", function() {
expect(unitI.x).toBe(1)
expect(unitI.y).toBe(0)
expect(unitI.z).toBe(0)
})
it("unitJ should be [0, 1, 0] in the standard basis.", function() {
expect(unitJ.x).toBe(0)
expect(unitJ.y).toBe(1)
expect(unitJ.z).toBe(0)
})
it("unitK should be [0, 0, 1] in the standard basis.", function() {
expect(unitK.x).toBe(0)
expect(unitK.y).toBe(0)
expect(unitK.z).toBe(1)
})
})
}
import Scalar from './Scalar'
function isNumber(x: any): x is number {
return typeof x === 'number'
}
function isScalar(x: any): x is Scalar {
return x instanceof Scalar
}
function isVector(x: any): x is Vector {
return x instanceof Vector
}
export default class Vector {
constructor(public x: number, public y: number, public z: number, public uom: UNITS.Unit) {
}
__add__(rhs): Vector {
if (isVector(rhs)) {
return new Vector(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z, UNITS.Unit.compatible(this.uom, rhs.uom))
}
else {
console.log("Vector.__add__ ignored.")
return void 0;
}
}
__mul__(rhs: Scalar | number): Vector {
if (isScalar(rhs)) {
const q = rhs.quantity
return new Vector(this.x * q, this.y * q, this.z * q, UNITS.Unit.mul(this.uom, rhs.uom))
}
else if (isNumber(rhs)) {
return new Vector(this.x * rhs, this.y * rhs, this.z * rhs, this.uom)
}
else {
console.log("Vector.__mul__ ignored.")
return void 0;
}
}
__rmul__(lhs: Scalar | number): Vector {
if (isScalar(lhs)) {
console.log("Vector.__rmul__ ignored (Scalar).")
}
else if (isNumber(lhs)) {
return new Vector(lhs * this.x, lhs * this.y, lhs * this.z, this.uom)
}
else {
return void 0;
}
}
__div__(rhs: Scalar | number): Vector {
if (isScalar(rhs)) {
const q = rhs.quantity
return new Vector(this.x / q, this.y / q, this.z / q, UNITS.Unit.div(this.uom, rhs.uom))
}
else if (isNumber(rhs)) {
return new Vector(this.x / rhs, this.y / rhs, this.z / rhs, this.uom)
}
else {
console.log("Vector.__mul__ ignored.")
return void 0;
}
}
__neg__(): Vector {
return new Vector(-this.x, -this.y, -this.z, this.uom)
}
toString(): string {
return `[${this.x}, ${this.y}, ${this.z}] ${this.uom}`
}
}
/**
* Constructs a new vector from 3D cartesian coordinates.
*/
export function vector(x: number, y: number, z: number): THREE.Vector3 {
return new THREE.Vector3(x, y, z)
}
/**
* Computes the magnitude of a vector.
*/
export function mag(v: THREE.Vector3): number {
const x = v.x;
const y = v.y;
const z = v.z;
// TODO: Student should complete
return Math.sqrt(x * x + y * y + z * z);
}
/**
* Computes the direction for a vector.
*/
export function norm(v: THREE.Vector3): THREE.Vector3 {
// TODO: Student should complete.
const m = mag(v);
return new THREE.Vector3(this.x / m, this.y / m, this.z / m);
}
/**
* The scalar or dot product of two vectors.
* This is the symmetrized product of vectors that yields a scalar.
*/
export function dot(a: THREE.Vector3, b: THREE.Vector3): number {
// TODO: Student should complete.
throw new Error("TODO: dot(a: Vector3, b: Vector3) => number")
}
/**
* The cross product.
* This is the antisymmetric product of vectors, dualized to yield a vector.
*/
export function cross(a: THREE.Vector3, b: THREE.Vector3): THREE.Vector3 {
// TODO: Student should complete.
throw new Error("TODO: cross(a: Vector3, b: Vector3) => Vector3")
}
//
// A side effect of importing and using anything from this module is to
// monkey-patch the THREE.Vector3 class by adding methods that support
// operator overloading in STEMCstudio. The full range of possible
// operators is described in the STEMCstudio Wiki.
//
// TODO: Student should complete.
// Vector addition
// Vector subtraction
// Unary plus
// Unary minus
//
//
//
THREE.Vector3.prototype['__add__'] = function(rhs: THREE.Vector3): THREE.Vector3 {
// TODO: Student should complete...
return new THREE.Vector3(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z);
}
//
// vector * scalar, where vector: THREE.Vector3 and scalar: number.
//
THREE.Vector3.prototype['__mul__'] = function(rhs: number): THREE.Vector3 {
// TODO: Student should complete...
return void 0
// return new THREE.Vector3(this.x * rhs, this.y * rhs, this.z * rhs);
}
//
// scalar * vector, where vector: THREE.Vector3 and scalar: number.
//
THREE.Vector3.prototype['__rmul__'] = function(lhs: number): THREE.Vector3 {
return new THREE.Vector3(lhs * this.x, lhs * this.y, lhs * this.z);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment