three-codeeditor
Version:
codeeditor for three.js
363 lines (293 loc) • 14.4 kB
JavaScript
/*global THREE,ace*/
;(function() {
// "imports"
var aceHelper, rendering, canvas2d, domevents,
mouseevents, commands, raycasting;
// "imports" assigned here b/c they are first available after this module got defined
function imports() {
aceHelper = THREE.CodeEditor.aceHelper,
rendering = THREE.CodeEditor.rendering,
canvas2d = THREE.CodeEditor.canvas2d,
mouseevents = THREE.CodeEditor.mouseevents;
domevents = THREE.CodeEditor.domevents;
raycasting = THREE.CodeEditor.raycasting;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
if (!THREE.CodeEditor) {
THREE.CodeEditor = function CodeEditor() { this.initialize.apply(this, arguments); }
THREE.CodeEditor.prototype = Object.create(THREE.Mesh.prototype);
} else imports();
(function() {
this.isCodeEditor3D = true;
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// initialize-release
// -=-=-=-=-=-=-=-=-=-
this.initialize = function(options) {
imports();
if (!options) throw new Error("No settings specified for CodeEditor3D!");
if (!options.events && (!options.domElement || !options.domElement)) {
new Error("Settings do not specify an event interface nor a domElement / camera that is needed to set events up!")
}
this.vr = options.vr;
this.events = options.events || new THREEx.DomEvents(options.camera, options.domElement);
// supported events: resize
lively.lang.events.makeEmitter(this);
var width = 400, height = 400;
var editorGeo = plane(
new THREE.Vector3(-width/2, height/2,0),
new THREE.Vector3( width/2, height/2,0),
new THREE.Vector3( width/2,-height/2,0),
new THREE.Vector3(-width/2,-height/2,0));
// building the html canvas that will be used as a texture
var canvas = this.canvas2d = canvas2d.create(width, height),
texture = new THREE.Texture(canvas),
material= new THREE.MeshBasicMaterial({
color: "white", map: texture,
side: THREE.DoubleSide});
// var maxAnisotropy = renderer.getMaxAnisotropy();
// texture.anisotropy = 16;
THREE.Mesh.call(this, editorGeo, material);
editorGeo.computeBoundingBox();
this.position.copy(editorGeo.boundingBox.center());
// creating the ace editor instance that will work behind the scenes as our "model"
var aceEditor;
if (options.aceEditor) this.aceEditor = aceEditor = options.aceEditor;
else {
var offset = options.canvasOffset || {left: 0, top: 0};
aceEditor = this.aceEditor = aceHelper.createAceEditor(
offset.left, offset.top, width, height);
}
aceEditor.parent3d = this; // FIXME backlink for autocompleter
var self = this;
aceEditor.renderer.on("afterRender", function() { rendering.onAceEditorAfterRenderEvent(aceEditor, self); });
aceEditor.renderer.on("themeChange", function() { self.invalidateScrollbar(); });
aceEditor.renderer.on("resize", function() { self.invalidateScrollbar(); });
aceEditor.renderer.on("autosize", function() { self.invalidateScrollbar(); });
texture.needsUpdate = true;
this.addMouseEventListeners();
}
this.destroy = function() {
// FIXME remove mouse handler...
this.canvas2d.cleanup();
this.canvas2d = null;
this.aceEditor.cleanup();
this.aceEditor = null;
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// editor behavior
// -=-=-=-=-=-=-=-=-=-
this.setValue = function(text) {
this.aceEditor.setValue(text);
};
this.insert = function(text, noTransform) {
// insert text at cursor
this.aceEditor.insert(text, noTransform);
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// input events
// -=-=-=-=-=-=-
this.scrollSpeed = 1;
this.addMouseEventListeners = function() {
mouseevents.patchTHREExDOMEventInstance(this);
if (this._onMouseDown || this._onMouseMove || this._onMouseWheel) this.removeMouseEventListeners();
this._onMouseDown = function(evt) { return this.onMouseDown(evt); }.bind(this);
this._onMouseMove = function(evt) { return this.onMouseMove(evt); }.bind(this);
this._onMouseWheel = function(evt) { return this.onMouseWheel(evt); }.bind(this);
this._onMouseOver = function(evt) { return this.onMouseOver(evt); }.bind(this);
this._onMouseOut = function(evt) { return this.onMouseOut(evt); }.bind(this);
this.events.addEventListener('mousedown', this._onMouseDown, false);
this.events.addEventListener('mousemove', this._onMouseMove, false);
this.events.addEventListener('mousewheel', this._onMouseWheel, false);
this.events.addEventListener('mouseover', this._onMouseOver, false);
this.events.addEventListener('mouseout', this._onMouseOut, false);
}
this.removeMouseEventListeners = function() {
this._onMouseDown && this.events.removeEventListener('mousedown', this._onMouseDown, false);
this._onMouseMove && this.events.removeEventListener('mousemove', this._onMouseMove, false);
this._onMouseWheel && this.events.removeEventListener('mousewheel', this._onMouseWheel, false);
this._onMouseOver && this.events.removeEventListener("mouseover", this._onMouseOver, false);
this._onMouseOut && this.events.removeEventListener("mouseout", this._onMouseOut, false);
this._onMouseDown = null;
this._onMouseMove = null;
this._onMouseWheel = null;
this._onMouseOver = null;
this._onMouseOut = null;
}
this.onMouseDown = function(evt) {
// clicked on scrollbar?
if (mouseevents.processScrollbarMouseEvent(
this.events, this, this.clickState, evt)) return true;
var origDomEvent = evt.origDomEvent || evt;
var aceCoords = raycasting.raycastIntersectionToDomXY(evt.detail.intersection, this.aceEditor.container);
mouseevents.reemit3DMouseEvent(this.events, origDomEvent, this.clickState, this, aceCoords);
}
this.onMouseMove = function(evt) {
var aceCoords = raycasting.raycastIntersectionToDomXY(evt.detail.intersection, this.aceEditor.container);
mouseevents.reemit3DMouseEvent(this.events, evt.origDomEvent, this.clickState, this, aceCoords);
}
this.onMouseWheel = function(evt) {
var aceCoords = raycasting.raycastIntersectionToDomXY(evt.detail.intersection, this.aceEditor.container);
mouseevents.reemit3DMouseEvent(this.events, evt.origDomEvent, this.clickState, this, aceCoords);
}
this.onMouseOver = function(evt) {
return;
if (evt.target !== this) return;
var noMouse = !mouseevents.isLeftMouseButtonPressed(evt)
&& !mouseevents.isRightMouseButtonPressed(evt);
if (noMouse) this.aceEditor.focus();
if (noMouse) console.log("ficussed!!");
};
this.onMouseOut = function(evt) {
return;
console.log("blur!!");
this.aceEditor.blur();
};
this.getScrollbar = function() {
return this.scrollbar || (this.scrollbar = createScrollbar(this.aceEditor));
}
this.invalidateScrollbar = function() { this.scrollbar = null; }
this.clickState = {
lastClickTime: 0,
doubleClickTriggerTime: 500,
scrollbarClickPoint: null
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// geometry
// -=-=-=-=-
this.getWidth = function() { return this.getSize().x; };
this.getHeight = function() { return this.getSize().y; };
this.setHeight = function(height) { return this.setSize(this.getWidth(), height); };
this.setWidth = function(width) { return this.setSize(width, this.getHeight()); };
this.getSize = function() {
this.geometry.computeBoundingBox();
return this.geometry.boundingBox.size();
};
this.setSize = function(width, height) {
this.aceEditor.container.style.width = width + "px";
this.aceEditor.container.style.height = height + "px";
this.canvas2d.width = width;
this.canvas2d.height = height;
this.geometry.vertices = [
new THREE.Vector3(-width/2, height/2,0),
new THREE.Vector3( width/2, height/2,0),
new THREE.Vector3(-width/2,-height/2,0),
new THREE.Vector3( width/2,-height/2,0)]
this.material.map.needsUpdate = true;
this.aceEditor.resize(true);
this.geometry.verticesNeedUpdate = true;
// for events:
this.geometry.computeBoundingBox();
this.geometry.computeBoundingSphere();
this.emit("resize", {x: width, y: height});
};
this.getGlobalVertice = function(i) {
return this.geometry.vertices[i].clone().applyMatrix4(this.matrixWorld);
};
this.topLeft = function() { return this.getGlobalVertice(0); };
this.topRight = function() { return this.getGlobalVertice(1); };
this.bottomLeft = function() { return this.getGlobalVertice(2); };
this.bottomRight = function() { return this.getGlobalVertice(3); };
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// positioning
// -=-=-=-=-=-=-
this.alignWithCamera = function(leftRightOrCenter, camera) {
// offset: // -1 left, 0 center, 1 right
this.geometry.computeBoundingBox()
var margin = 200;
var size = this.geometry.boundingBox.size()
var dist = (size.y+margin) / 2 / Math.tan(Math.PI * camera.fov / 360);
var center = raycasting.pickingRay({x:0,y:0}, camera).ray.at(dist);
this.position.copy(center);
this.lookAt(camera.position);
if (leftRightOrCenter === "center") return;
// var projectionPoint = raycasting.pickingRay({x:-1,y:0}, camera).ray.at(dist);
// var delta = projectionPoint.clone().sub(center);
// align(this, this.topLeft(), projectionPoint)
// this.position.copy(projectionPoint.clone());
// this.lookAt(camera.position.clone().add(delta));
// this.position.add(this.topRight().clone().sub(this.topLeft()).multiplyScalar(.5));
// return;
// move the editor to the left side until it reaches the screen border...
// ugly! I should use fov and stuff to compute the coordinates but its soooo late already.... :P
camera.updateMatrix()
camera.updateMatrixWorld()
var delta = camera.up.clone().cross(this.position.clone().sub(camera.position)).normalize()
if (leftRightOrCenter === "right") delta.negate();
var frustum = new THREE.Frustum();
frustum.setFromMatrix( new THREE.Matrix4().multiply( camera.projectionMatrix, camera.matrixWorldInverse ) );
var bounds = new THREE.Box3().setFromObject(this);
do { // move left unti we "hit" the corner of the editor
this.position.add(delta);
this.updateMatrixWorld();
var pointsToCheck = leftRightOrCenter === "right" ?
[this.topRight(), this.bottomRight()] : [this.topLeft(), this.bottomLeft()]
} while (pointsToCheck.every(function(p) { return frustum.containsPoint(p); }));
}
this.autoAlignWithCamera = function(dir, camera) {
var cameraState, editorState, editor = this;;
this.stopAutoAlignWithCamera();
rememberState();
editor.alignWithCamera(dir, camera);
editor._autoAlignWithCameraInterval = setInterval(function() {
if (!hasCameraChanged() && !hasEditorChanged()) return;
rememberState();
lively.lang.fun.debounceNamed("autoAlignWithCamera", 200,
editor.alignWithCamera.bind(editor, dir, camera))();
}, 100);
function rememberState() {
editorState = {wasResized: false, position: editor.position.clone()};
editor.once("resize", function() { editorState.wasResized = true; });
cameraState = lively.lang.obj.extract(
["position", "rotation", "fov", "aspect", "zoom"], camera,
function(k, val) { return val && val.clone ? val.clone() : val; });
}
function hasCameraChanged() {
return Object.keys(cameraState).some(function(k) {
if (!cameraState[k]) return false;
if (cameraState[k].equals) return !cameraState[k].equals(camera[k]);
return cameraState[k] !== camera[k];
});
}
function hasEditorChanged() {
return editorState.wasResized || !editorState.position.equals(editor.position);
}
};
this.stopAutoAlignWithCamera = function() {
if (this._autoAlignWithCameraInterval) {
clearInterval(this._autoAlignWithCameraInterval);
delete this._autoAlignWithCameraInterval;
}
}
}).call(THREE.CodeEditor.prototype);
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// helper
// -=-=-=-
function align(object, x, y) { return object.position.add(y.clone().sub(x)); }
function notYetImplemented() { console.warn("NOT YET IMPLEMENTED"); }
function plane(a,b,c,d) {
var vec1 = b.clone().sub(a), vec2 = d.clone().sub(a);
return new THREE.PlaneGeometry(vec1.length(), vec2.length(), 1,1);
}
function createScrollbar(aceEditor) {
var renderer = aceEditor.renderer, scrollBarV = renderer.scrollBarV,
editorStyle = window.getComputedStyle(renderer.container),
relativeHeight = scrollBarV.element.clientHeight / scrollBarV.inner.clientHeight,
relativeTop = scrollBarV.scrollTop / scrollBarV.inner.clientHeight,
height = scrollBarV.element.clientHeight,
borderWidth = 3,
width = 10,
col = new THREE.Color(editorStyle.backgroundColor),
isDarkTheme = (col.r+col.g+col.b)/3 < .5,
backgroundColor = col.clone().add(col.clone().multiplyScalar(-.4)).getStyle();
return {
height: height * relativeHeight - borderWidth,
width: width - borderWidth,
get top() { return height * (scrollBarV.scrollTop / scrollBarV.inner.clientHeight) + borderWidth; },
left: renderer.container.clientWidth - width - borderWidth,
borderWidth: borderWidth,
backgroundColor: backgroundColor,
borderColor: isDarkTheme ? col.add(new THREE.Color('white').multiplyScalar(.2)).getStyle() : backgroundColor,
borderRounding: 4
};
}
})(THREE);