Skip to content

Latest commit

 

History

History
170 lines (141 loc) · 5.9 KB

DEVELOPMENT.md

File metadata and controls

170 lines (141 loc) · 5.9 KB

Development Process

The porting process is a learning process consisting of trial and error learning how AssemblyScript works and adapting the JavaScript code to work in AssemblyScript. Much of the code can remain mostly the same, but certain things that work in JavaScript, like object literals, don't work in AssemblyScript. In these cases we've needed to convert things like object literals to strictly typed instances constructed from classes that we defined and which had not previously existed in the Three.js code base.

For example, functions accepting options objects like

function someFunction(options) {
	// ...
}
someFunction({ option1: 'foo', option2: 'bar' })

have been converted to a strictly typed equivalent like so:

class SomeFunctionOptions {
	option1: string
	option2: string
}
function someFunction(options: SomeFunctionOptions) {
	// ...
}
someFunction({ option1: 'foo', option2: 'bar' } as SomeFunctionOptions)
// or
const options = new SomeFunctionOptions()
options.option1 = 'foo'
options.option2 = 'bar'
someFunction(options)

We've also needed to convert Three.js function-style classes to class syntax in the AS code. For example, a classes like

function Mesh() {
	Object3D.call(this)
	// ...
}

Mesh.prototype = Object.create(Object3D.prototype, {
	someMethod() {
		// ...
	},
})

have been be converted to classes of the form

class Mesh extends Object3D {
	constructor() {
		super()
		// ...
	}

	someMethod() {
		// ...
	}
}

The EventDispatcher class is a prime example that needed some refactoring to allow for strict typing to work within the confines of AssemblyScript. For example, in Three.js, listening to and dispatching an event looks like the following:

obj.addEventListener('didSomething', event => {
	log(event.target === obj) // true
	log(event.foo) // 123
	log(event.lorem) // 456
})
obj.dispatchEvent({ type: 'didSomething', foo: 123, bar: 456 })

As passing object literals like that doesn't work in AssemblyScript (because we do not know what properties someone would want to pass within an event), we changed the API to look like this:

class SomeThing extends Listener {
	// listeners must be objects with a handleEvent method.
	handleEvent(e: Event) {
		if (e.type == 'didSomething' && e.target === obj) {
			log(event.target === obj) // true

			// we need to use a conditional statement so that AssemblyScript can
			// narrow the type of event.attachment to that which we expect.
			const attachment = event.attachment
			if (attachment instanceof SomethingEventAttachment) {
				log(event) // 123
				log(event.lorem) // 456
			}
		}
	}
}

const listener = new SomeThing()

obj.addEventListener('didSomething', listener)

class SomethingEventAttachment {
	foo: f64
	bar: f64
}

const attachment: SomethingEventAttachment = { foo: 123, bar: 456 }

obj.dispatchEvent(new Event('didSomething', obj, attachment))

We will improve the Event API over time, but that's the initial draft. Once AssemblyScript releases the function closures feature, we can switch back to passing functions, instead of objects with handleEvent methods.

The process so far involves choosing a class from Three.js and sticking it into the src/as/ folder. The file structure in src/as/ matches with the same file structure as in the Three.js src/ folder.

The gist of the development process is:

  • choose a file from the Three.js repo
  • copy it into the same structure as the Three.js repo, with a .ts extension, and in src/as/ instead of just src/ (someone may have already pasted the Three.js file in our repo, check for that)
  • copy the test associated with the file from the Three.js repo, but with a .spec.ts extension, and place it as a sibling to the source file
  • port the code for the source and tests of the chosen file. You can take the type definitions from the Three.js .d.ts files which are sibling to the .js files, and use those to help create the merged TypeScript (AssemblyScript) code.
  • after porting source and test code, run npm test to test it

For example, when we ported the Object3D class from src/core/Object3D.js in the Three.js repo, we placed it in our repo as src/as/core/Object3D.ts, inside of src/as/ instead of just src/ in order to distinguish the AssemblyScript code from regular TypeScript code in src/ts/. We took src/core/Object3D.d.ts in the Three.js repo as a starting point for the types, merging the types into the same file.

Basically we merged both src/core/Object3D.js and src/core/Object3D.d.ts from the Three.js repo into a single src/as/core/Object3D.ts file in our repo.

Then we ported test/unit/src/core/Object3D.tests.js from the Three.js repo and made it a sibling file of our Object3D.ts file, at src/as/core/Object3D.spec.ts. We prefer to co-locate test files as siblings of the source files, making them easier to navigate.

As you can tell from comparing the test file from Three.js and the test file in glas, porting test code involves converting from QUnit-style tests to using as-pect-style describe, test, and expect functions. See as-pect documentation for more details on available APIs; basically as-pect's API similar to Mocha.js+Chai or Jasmine.