I’ve been experimenting with trying to get more ‘type safety’ in scripts of my personal projects and found that you can intersect types in the JS Docs implementation of Monaco, our current Code Editor.
With that and some boiler plating use in JS Docs, it’s kind of doable.
See the following:
(function () {
var BoxPlacement = pc.createScript('boxPlacement');
BoxPlacement.attributes.add('boxEntity', { type: 'entity', description: 'The entity to be spawned after clicking.' });
BoxPlacement.attributes.add('cameraEntity', { type: 'entity', description: 'The main camera entity in the scene.' });
/**
* @typedef $BoxPlacement
* @property {pc.Entity} boxEntity
* @property {pc.Entity} cameraEntity
* @property {function(pc.Vec3)} spawnBox
* @property {function(Object.<number, number>)} fireRaycast
* @property {function(pc.MouseEvent)} onMouseDown
* @property {function(pc.TouchEvent)} onTouchDown
*/
/** @type {pc.ScriptType & $BoxPlacement} */
var prototype = BoxPlacement.prototype;
// initialize code called once per entity
prototype.initialize = function () {
if (this.app.touch) {
this.app.touch.on('touchstart', this.onTouchStart, this);
}
this.app.mouse.on('mousedown', this.onMouseDown, this);
this.on('destroy', function () {
if (this.app.touch) {
this.app.touch.off('touchstart', this.onTouchStart, this);
}
this.app.mouse.off('mousedown', this.onMouseDown, this);
}, this);
};
prototype.onMouseDown = function (e) {
this.fireRaycast(e);
};
prototype.onTouchStart = function (touchEvent) {
// For each new touch on screen, create a box
var changedTouches = touchEvent.changedTouches;
for (var i = 0; i < changedTouches.length; i++) {
this.fireRaycast(changedTouches[i]);
}
touchEvent.event.preventDefault();
};
prototype.fireRaycast = function (event) {
// Get the vec3 to start from
var from = this.cameraEntity.getPosition();
// Convert the 2D screen space coordinates to a 3D point for the end point
var to = this.cameraEntity.camera.screenToWorld(event.x, event.y, this.cameraEntity.camera.farClip);
// Cast a ray between the two points
var raycastResult = this.app.systems.rigidbody.raycastFirst(from, to);
// If there was a hit, spawn a box at the point
if (raycastResult) {
var hitPoint = raycastResult.point;
this.spawnBox(hitPoint);
}
};
prototype.spawnBox = function (point) {
// Clone the box entity and add it to the hierarchy
var newBox = this.boxEntity.clone();
this.boxEntity.parent.addChild(newBox);
// Adjust the position up so the box does not clip through the plane
point.y += 0.5;
// Move the box to the clicked position
newBox.rigidbody.teleport(point);
// Enable the new box
newBox.enabled = true;
};
})();
The important parts are:
- The whole script is function scoped because we are using some global variables that really should not be exposed
-
BoxPlacement.prototype
is currently a pc.ScriptType so we use a temp variable and the@type
property to give it some properties
/**
* @typedef $BoxPlacement
* @property {pc.Entity} boxEntity
* @property {pc.Entity} cameraEntity
* @property {function(pc.Vec3)} spawnBox
* @property {function(Object.<number, number>)} fireRaycast
* @property {function(pc.MouseEvent)} onMouseDown
* @property {function(pc.TouchEvent)} onTouchDown
*/
/** @type {pc.ScriptType & $BoxPlacement} */
var prototype = BoxPlacement.prototype;
// initialize code called once per entity
prototype.initialize = function () {
This gives us code completion and linting for the attributes and functions that we define in the typedef making it a bit easier to work with.
It’s not perfect as I can’t name params in the functions and the JSDocs for the functions are not near the the functions themselves.
Anyone got any advance on this?
I think this would be a lot easier with ES6 classes but we need to implement a babel transpiler in the publish step to convert to ES5 code for maximum browser compatibility.