janusweb
Version:
Web client for JanusVR worlds
1,373 lines (1,311 loc) • 72.3 kB
JavaScript
elation.elements.define('janus.ui.editor.button', class extends elation.elements.ui.togglebutton {
create() {
super.create();
let editpanel = document.querySelector('ui-panel[name="topright"]');
if (editpanel) {
editpanel.hide();
}
janus.engine.systems.controls.addContext('editor', {
'toggle': [ 'keyboard_f1', (ev) => { if (ev.value) this.toggle(); } ],
});
janus.engine.systems.controls.activateContext('editor');
}
onactivate() {
let inventorypanel = document.querySelector('ui-collapsiblepanel[name="right"]');
if (inventorypanel) {
let editorpanel = inventorypanel.querySelector('janus-ui-editor-panel');
if (!editorpanel) {
editorpanel = elation.elements.create('janus-ui-editor-panel', { append: inventorypanel });
}
let inventory = inventorypanel.querySelector('janus-ui-inventory');
if (!inventory) {
inventory = elation.elements.create('janus-ui-inventory', { append: inventorypanel });
elation.events.add(inventory, 'assetselect', (ev) => editorpanel.handleInventorySelect(ev));
}
}
let editpanel = document.querySelector('ui-panel[name="topright"]');
if (editpanel) {
editpanel.show();
}
player.camera.camera.layers.enable(10);
janus.engine.systems.render.setdirty();
setTimeout(() => {
inventorypanel.expand();
}, 100);
}
ondeactivate() {
let inventorypanel = document.querySelector('ui-collapsiblepanel[name="right"]');
if (inventorypanel) {
inventorypanel.collapse();
}
let editpanel = document.querySelector('ui-panel[name="topright"]');
if (editpanel) {
editpanel.hide();
}
player.camera.camera.layers.disable(10);
janus.engine.systems.render.setdirty();
}
});
elation.elements.define('janus.ui.editor.panel', class extends elation.elements.base {
create() {
//this.innerHTML = elation.template.get('janus.ui.editor.panel');
let elements = elation.elements.fromTemplate('janus.ui.editor.panel', this);
/*
elation.events.add(elements.transformtype, 'change', (ev) => this.handleTransformTypeChange(ev));
elation.events.add(elements.transformspace, 'change', (ev) => this.handleTransformSpaceChange(ev));
//elements.transformglobal.addEventListener('deactivate', (ev) => this.handleTransformGlobalDeactivate(ev));
elation.events.add(elements.translatesnap, 'change', (ev) => this.handleTranslateSnapChange(ev));
elation.events.add(elements.rotatesnap, 'change', (ev) => this.handleRotateSnapChange(ev));
*/
elation.events.add(elements.debugview, 'click', (ev) => this.handleDebugviewClick(ev));
elation.events.add(elements.viewsource, 'click', (ev) => this.handleViewSourceClick(ev));
elation.events.add(elements.export, 'click', (ev) => this.handleExportClick(ev));
// If we've made any changes and the user tries to leave, prompt them to verify that they want to throw the changes away
window.addEventListener('beforeunload', (ev) => { if (this.history.length > 0) { ev.returnValue = false; ev.preventDefault(); return "You've made changes to this room, do you really want to leave?"; } });
this.elements = elements;
this.roomedit = {
snap: .01,
rotationsnap: 5,
modes: ['pos', 'rotation', 'scale', 'col', 'wireframe', 'lighting', 'fog', 'shadow_cast', 'shadow_receive'],
movespeed: new THREE.Vector3(),
modeid: 0,
object: false,
transforming: false,
};
this.history = [];
this.editObjectSetSnap(this.roomedit.snap);
janus.engine.systems.controls.addContext('roomedit', {
//'accept': [ 'keyboard_enter', (ev) => { if (ev.value) this.editObjectStop(); } ],
//'cancel': [ 'keyboard_esc', (ev) => this.editObjectCancel(ev) ],
'delete': [ 'keyboard_delete', (ev) => this.editObjectDelete(ev) ],
'mode': [ 'keyboard_nomod_tab', (ev) => this.editObjectToggleMode(ev) ],
'mode_reverse': [ 'keyboard_shift_tab', (ev) => this.editObjectToggleModeReverse(ev) ],
//'toggle_raycast': [ 'keyboard_shift', (ev) => this.editObjectToggleRaycast(ev) ],
/*
'manipulate_left': [ 'keyboard_j', (ev) => this.editObjectManipulateLeft(ev) ],
'manipulate_right': [ 'keyboard_l', (ev) => this.editObjectManipulateRight(ev) ],
'manipulate_up': [ 'keyboard_i', (ev) => this.editObjectManipulateUp(ev) ],
'manipulate_down': [ 'keyboard_k', (ev) => this.editObjectManipulateDown(ev) ],
'manipulate_in': [ 'keyboard_u', (ev) => this.editObjectManipulateIn(ev) ],
'manipulate_out': [ 'keyboard_o', (ev) => this.editObjectManipulateOut(ev) ],
*/
'manipulate_toggle':[ 'keyboard_space', (ev) => this.editObjectManipulateToggle(ev) ],
//'manipulate_mouse': [ 'mouse_delta', (ev) => this.editObjectManipulateMouse ],
'snap_ones': [ 'keyboard_1', (ev) => this.editObjectSnapOnes(ev) ],
'snap_tenths': [ 'keyboard_2', (ev) => this.editObjectSnapTenths(ev) ],
'snap_hundredths': [ 'keyboard_3', (ev) => this.editObjectSnapHundredths(ev) ],
'snap_thousandths': [ 'keyboard_4', (ev) => this.editObjectSnapThousandths(ev) ],
'copy': [ 'keyboard_ctrl_c', (ev) => { if (ev.value == 1) this.editObjectCopy(ev); } ],
'cut': [ 'keyboard_ctrl_x', (ev) => { if (ev.value == 1) this.editObjectCut(ev); } ],
});
janus.engine.systems.controls.addContext('roomedit_togglemove', {
//'togglemove': [ 'keyboard_shift', elation.bind(this, this.editObjectToggleMove)],
'manipulate_left': [ 'keyboard_a', (ev) => this.editObjectManipulateLeft(ev) ],
'manipulate_right': [ 'keyboard_d', (ev) => this.editObjectManipulateRight(ev) ],
'manipulate_up': [ 'keyboard_w', (ev) => this.editObjectManipulateUp(ev) ],
'manipulate_down': [ 'keyboard_s', (ev) => this.editObjectManipulateDown(ev) ],
'manipulate_in': [ 'keyboard_q', (ev) => this.editObjectManipulateIn(ev) ],
'manipulate_out': [ 'keyboard_e', (ev) => this.editObjectManipulateOut(ev) ],
});
janus.engine.systems.controls.addContext('roomedit_paste', {
'add': [ 'keyboard_ctrl_a', (ev) => { if (ev.value == 1) this.showCreateDialog(ev); } ],
'paste': [ 'keyboard_ctrl_v', (ev) => { if (ev.value == 1) this.editObjectPaste(ev);} ],
});
janus.engine.systems.controls.activateContext('roomedit_paste');
this.initialized = new Set();
let inventorypanel = document.querySelector('ui-collapsiblepanel[name="right"]');
this.scenetree = elation.elements.create('janus-ui-editor-scenetree', { append: inventorypanel });
elation.events.add(this.scenetree, 'select', (ev) => { this.editObject(ev.data); });
this.objectinfo = elation.elements.create('janus-ui-editor-objectinfo', { append: inventorypanel });
if (typeof room != 'undefined') {
this.initRoomEvents(room);
}
elation.events.add(janus._target, 'room_change', (ev) => this.initRoomEvents(room));
document.addEventListener('paste', (ev) => this.handlePaste(ev));
room.addEventListener('wheel', (ev) => this.editObjectMousewheel(ev));
window.addEventListener('keydown', (ev) => this.editObjectKeydown(ev));
window.addEventListener('keyup', (ev) => this.editObjectKeyup(ev));
}
initRoomEvents(room) {
if (!this.initialized.has(room)) {
room.addEventListener('click', (ev) => this.handleRoomClick(ev));
//room.addEventListener('mouseover', (ev) => console.log('mouseover', ev.data));
//room.addEventListener('mouseout', (ev) => console.log('mouseout', ev.data));
elation.events.add(room, 'dragenter', (ev) => this.handleDragOver(ev));
elation.events.add(room, 'dragover', (ev) => this.handleDragOver(ev));
room.addEventListener('dragenter', (ev) => this.handleDragOver(ev));
room.addEventListener('dragover', (ev) => this.handleDragOver(ev));
room.addEventListener('drop', (ev) => this.handleDrop(ev));
room.addEventListener('thing_add', (ev) => this.handleThingAdd(ev));
room.addEventListener('thing_remove', (ev) => this.handleThingRemove(ev));
this.initialized.add(room);
}
if (!this.outliner_selected) {
this.outliner_selected = room.createObject('outliner', {
col: 'lime',
opacity: .4,
outline: 3,
layers: '10',
});
this.outliner_hover = room.createObject('outliner', {
col: 'gray',
opacity: .4,
outline: 1,
layers: '10',
});
setTimeout(() => {
this.outliner_selected.col = 'lime';
this.outliner_hover.col = 'gray';
this.outliner_selected.setLayers('10');
this.outliner_hover.setLayers('10');
}, 0);
let editor = this;
this.outliner_hover.update = function() {
/*
let rc = player.raycast();
if (rc.length > 0 && rc[0].object !== room) {
this.select(rc[0].object);
} else {
this.deselect();
}
*/
}
} else if (this.outliner_selected.room !== room) {
room.appendChild(this.outliner_selected);
}
}
getManipulator() {
if (typeof room == 'undefined') return;
if (!this.manipulator) {
let view = janus.engine.client.view;
this.manipulator = new THREE.TransformControls(view.actualcamera, view.canvas);
elation.events.add(this.manipulator, 'mouseDown', (ev) => {
this.roomedit.transforming = true;
this.editObjectShowInfo(this.roomedit.object);
if (this.manipulator.object && this.manipulator.object.userData.thing) {
//elation.events.fire({type: 'admin_edit_start', element: this, data: this.manipulator.object.userData.thing});
//player.disable();
}
});
elation.events.add(this.manipulator, 'mouseUp', (ev) => {
setTimeout(() => {
this.roomedit.transforming = false;
}, 0);
});
elation.events.add(this.manipulator, 'change', (ev) => {
let constraint = (ev.target.axis ? ev.target.axis.toLowerCase() : false);
this.updateEditConstraints(constraint);
janus.engine.systems.render.setdirty();
});
elation.events.add(this.manipulator, 'objectChange', (ev) => {
this.editObjectShowInfo(this.roomedit.object);
this.roomedit.object.sync = true;
});
}
if (this.manipulator.parent != room._target.objects['3d']) {
room._target.objects['3d'].add(this.manipulator);
this.manipulator.enabled = false;
}
return this.manipulator;
}
setMode(mode) {
let manipulator = this.getManipulator();
switch (mode) {
case 'pos':
manipulator.setMode('translate');
break;
case 'rotation':
manipulator.setMode('rotate');
break;
case 'scale':
manipulator.setMode('scale');
break;
}
if (this.objectinfo) {
this.objectinfo.setMode(mode);
}
}
nextMode() {
let mode = this.objectinfo.nextMode();
this.setMode(mode);
}
prevMode() {
let mode = this.objectinfo.prevMode();
this.setMode(mode);
}
setTranslationSnap(snap) {
let manipulator = this.getManipulator();
if (snap == 'Off') snap = 0;
if (manipulator) manipulator.setTranslationSnap(+snap);
/*
if (this.elements.translatesnap.value != snap) {
this.elements.translatesnap.value = snap || 'Off';
}
*/
this.roomedit.snap = snap;
}
setRotationSnap(snap) {
this.roomedit.rotationsnap = snap;
let manipulator = this.getManipulator();
if (snap == 'Off') snap = 0;
if (manipulator) manipulator.setRotationSnap(+snap * THREE.MathUtils.DEG2RAD);
/*
if (this.elements.rotatesnap.value != snap) {
this.elements.rotatesnap.value = snap || 'Off';
}
*/
}
handleTransformTypeChange(ev) {
let manipulator = this.getManipulator();
if (manipulator) manipulator.setMode(ev.data);
}
handleTransformSpaceChange(ev) {
let manipulator = this.getManipulator();
if (manipulator) {
manipulator.setSpace(ev.data);
}
}
handleTransformGlobalDeactivate(ev) {
let manipulator = this.getManipulator();
if (manipulator) manipulator.setSpace('local');
}
handleTranslateSnapChange(ev) {
console.log('set translation snap', ev.data, ev);
this.setTranslationSnap(ev.target.value);
}
handleRotateSnapChange(ev) {
/*
let snap = ev.target.value,
manipulator = this.getManipulator();
if (snap == 'Off') snap = 0;
if (manipulator) manipulator.setRotationSnap(snap * Math.PI / 180);
*/
this.setRotationSnap(ev.target.value);
}
handleDebugviewClick(ev) {
if (!ev.target.active) {
janus.engine.client.view.pickingdebug = true;
janus.engine.systems.world.enableDebug();
} else {
janus.engine.client.view.pickingdebug = false;
janus.engine.systems.world.disableDebug();
}
janus.engine.systems.render.setdirty();
}
handleViewSourceClick(ev) {
if (!this.sourcewindow) {
let win = elation.elements.create('ui-window', { append: document.body, center: true });
let editor = elation.elements.create('janus-ui-editor-source', { room: room });
win.setcontent(editor);
setTimeout(() => { win.center = false; }, 1500);
this.sourcewindow = win;
} else {
this.sourcewindow.show();
}
}
handleExportClick(ev) {
let exporter = new THREE.GLTFExporter();
let userDatas = new Map();
// Store and nullify userData objects for each object before serializing
room._target.objects['3d'].traverse(n => {
userDatas.set(n, n.userData);
n.userData = {};
elation.events.fire({element: this, type: 'webui_editor_export_node', data:{n,userData: userDatas.get(n)} })
});
elation.events.fire({element: this, type: 'webui_editor_export', data:{ scene: room._target.objects['3d']} })
exporter.parse(room._target.objects['3d'], (data) => {
// Restore userData objects
room._target.objects['3d'].traverse(n => {
if (userDatas.has(n)) {
n.userData = userDatas.get(n);
}
});
let filedata = new Blob([data], {type: 'model/gltf-binary'});
var url = window.URL.createObjectURL(filedata);
let d = new Date(),
ts = d.getFullYear() + (d.getMonth() + 1).toString().padStart(2, '0') + d.getDate().toString().padStart(2, '0') + d.getHours().toString().padStart(2, '0') + d.getMinutes().toString().padStart(2, '0') + d.getSeconds().toString().padStart(2, '0');
var a = document.createElement('A');
a.style.display = 'none';
document.body.appendChild(a);
a.href = url;
a.download = room.title + ' - ' + ts + '.glb';
a.click();
window.URL.revokeObjectURL(url);
}, { binary: true });
}
handleRoomClick(ev) {
if (ev.button == 2) {
let proxyobj = ev.element.getProxyObject();
if (this.roomedit.object) {
this.editObjectRevert();
} else if ((!room.localasset || !room.localasset.isEqual(proxyobj)) && !this.objectIsLocked(proxyobj)) {
this.roomedit.raycast = ev.ctrlKey;
this.editObject(proxyobj);
}
} else if (ev.button == 0 && !this.roomedit.transforming) {
if (this.roomedit.object) {
if (!this.roomedit.raycast) {
// raycasting means this is an initial object placement, we don't need to log a change
// FIXME - this is no longer the case, you can activate raycast mode by holding ctrl while clicking an object
this.history.push({type: 'changed', object: this.roomedit.object, state: null});
}
this.editObjectStop();
}
}
}
objectIsLocked(object) {
let locked = object.locked;
if (object.parent) {
locked = locked || this.objectIsLocked(object.parent);
}
return locked;
}
editObject(object, isnew) {
if (this.roomedit.object) this.editObjectStop()
this.roomedit.object = object;
this.roomedit.parentObject = null;
this.roomedit.objectBoundingBox = false;
this.roomedit.modeid = 0;
this.roomedit.objectIsNew = isnew;
this.roomedit.moving = true;
this.roomedit.movespeed.set(0, 0, 0);
object.sync = true;
if (!object.js_id) {
object.js_id = player.userid + '-' + window.uniqueId();
}
this.roomedit.objectBoundingBox = object.getBoundingBox(true);
object.addEventListener('load', (ev) => {
this.roomedit.objectBoundingBox = object.getBoundingBox(true);
this.editObjectUpdate();
//this.editObjectShowWireframe();
this.editObjectShowOutline();
console.log('update bbox', this.roomedit.objectBoundingBox);
});
//object.pickable = false;
//console.log('set object unpickable', object);
room.addEventListener('mousemove', (ev) => this.editObjectMousemove(ev));
elation.events.add(this, 'mousedown', this.editObjectClick);
//elation.events.add(document, 'pointerlockchange', (ev) => this.editObjectHandlePointerlock(ev));
// Back up properties so we can revert if necessary
this.roomedit.startattrs = {};
for (var i = 0; i < this.roomedit.modes.length; i++) {
var mode = this.roomedit.modes[i];
if (object[mode]) {
var val = object[mode];
if (val instanceof THREE.Vector3 ||
val instanceof THREE.Color ||
val instanceof THREE.Euler) {
val = val.toArray().join(' ');
}
this.roomedit.startattrs[mode] = val;
}
}
// activate context
janus.engine.systems.controls.activateContext('roomedit', this);
//this.editObjectShowWireframe();
this.editObjectShowOutline();
this.editObjectShowInfo(object);
let manipulator = this.getManipulator();
manipulator.attach(object.objects['3d']);
manipulator.enabled = true;
this.setMode('pos');
}
editObjectShowInfo(object) {
//let content = elation.template.get('janus.ui.editor.object.info', {object: object, editmode: this.roomedit.modes[this.roomedit.modeid]});
let inventorypanel = document.querySelector('ui-collapsiblepanel[name="right"]');
this.objectinfo.updateObject(object);
if (this.objectinfo.hidden) {
this.objectinfo.show();
}
inventorypanel.refresh();
}
editObjectShowOutline() {
if (this.roomedit.object === room) {
this.outliner_selected.deselect();
} else {
this.outliner_selected.select(this.roomedit.object);
}
}
editObjectRemoveOutline() {
if (this.outliner_selected) {
this.outliner_selected.deselect();
}
}
editObjectShowWireframe() {
if (this.roomedit.wireframe) {
this.editObjectRemoveWireframe();
}
var object = this.roomedit.object;
var obj3d = object._target.objects['3d'];
var material = new THREE.MeshPhongMaterial({
wireframe: true,
color: 0xff0000,
emissive: 0x330000,
wireframeLinewidth: 2,
depthTest: false,
transparent: true,
opacity: .1,
blending: THREE.AdditiveBlending
});
var root = false;
if (obj3d instanceof THREE.Mesh) {
root = new THREE.Mesh(obj3d.geometry, material);
root.scale.copy(obj3d.scale);
} else {
var objchild = obj3d;//.children[obj3d.children.length-1] || obj3d;
//root = objchild.clone();
/*
root = new THREE.Object3D();
let parent = root;
obj3d.traverse(function(n) {
if (n instanceof THREE.Mesh) {
n.material = material;
} else {
}
});
*/
root = this.editObjectCloneWireframe(obj3d, null, material, this.roomedit.object._target);
if (root) {
root.matrix.identity();
}
}
this.roomedit.wireframe = root;
if (root) {
this.roomedit.object._target.objects['3d'].add(root);
root.updateMatrixWorld();
}
}
editObjectCloneWireframe(obj, parent, material, constrain) {
let newobj = null;
if (obj instanceof THREE.Mesh) {
newobj = new THREE.Mesh(obj.geometry, material);
newobj.matrixAutoUpdate = false;
newobj.matrix.copy(obj.matrix);
//newobj.matrixWorld.copy(obj.matrix);
}
if (obj.children.length > 0) {
if (newobj === null) {
newobj = new THREE.Object3D();
newobj.matrixAutoUpdate = false;
newobj.matrix.copy(obj.matrix);
//newobj.matrixWorld.copy(obj.matrix);
}
for (let i = 0; i < obj.children.length; i++) {
let objthing = obj.children[i].userData.thing;
if (!objthing || !constrain || objthing === constrain) {
this.editObjectCloneWireframe(obj.children[i], newobj, material, constrain);
}
}
}
if (newobj && parent) {
parent.add(newobj);
}
return newobj;
}
editObjectRemoveWireframe() {
if (this.roomedit.wireframe && this.roomedit.wireframe.parent) {
this.roomedit.wireframe.parent.remove(this.roomedit.wireframe);
}
}
editObjectShowParentWireframe() {
if (this.roomedit.parentObject) {
let bbox = this.roomedit.parentObject.getBoundingBox(true);
if (!this.roomedit.parentwireframe) {
let root = this.roomedit.parentObject.createObject('object', { });
let hiddenfaces = root.createObject('object', {
id: 'cube',
wireframe: true,
col: V(0,0,1,.2),
depth_write: false,
depth_test: false,
pos: V()
});
root.createObject('object', {
id: 'cube',
wireframe: true,
col: 'blue',
opacity: 1,
depth_write: false,
depth_test: true
});
this.roomedit.parentwireframe = root;
}
let wireframe = this.roomedit.parentwireframe;
wireframe.scale.subVectors(bbox.max, bbox.min);
wireframe.pos.addVectors(bbox.max, bbox.min).divideScalar(2);
if (wireframe.parent != this.roomedit.parentObject) {
this.roomedit.parentObject.appendChild(wireframe);
}
}
}
editObjectRemoveParentWireframe() {
if (this.roomedit.parentwireframe && this.roomedit.parentwireframe.parent) {
//this.roomedit.parentwireframe.parent.removeChild(this.roomedit.parentwireframe);
}
}
editObjectStop(destroy) {
if (this.roomedit.object) {
if (destroy) {
this.roomedit.object.die();
} else {
this.roomedit.object.sync = true;
if (this.roomedit.collision_id) {
// restore collider
this.roomedit.object.collision_id = this.roomedit.collision_id;
this.roomedit.collision_id = false;
} else if (false) {
this.roomedit.object.collision_id = 'cube';
this.roomedit.object.collision_trigger = true;
this.roomedit.object.collision_scale = this.roomedit.objectBoundingBox.max.clone().sub(this.roomedit.objectBoundingBox.min);
this.roomedit.object.collision_pos = this.roomedit.objectBoundingBox.max.clone().add(this.roomedit.objectBoundingBox.min).multiplyScalar(.5);
//this.roomedit.object.collision_id = this.roomedit.object.id;
}
if (this.roomedit.parentObject) {
let parentpos = this.roomedit.parentObject.worldToLocal(this.roomedit.object.getWorldPosition());
this.roomedit.parentObject.add(this.roomedit.object);
this.roomedit.object.pos = parentpos;
}
this.roomedit.object.dispatchEvent({type: 'edit', bubbles: true});
}
}
//this.roomedit.object.pickable = true;
this.roomedit.object.sync = true;
this.roomedit.object = false;
this.roomedit.raycast = false;
this.roomedit.parentObject = null;
//this.editObjectRemoveWireframe();
this.editObjectRemoveOutline();
this.editObjectRemoveParentWireframe();
let manipulator = this.getManipulator();
manipulator.detach();
manipulator.enabled = false;
if (this.objectinfo) {
//this.objectinfo.hide();
}
elation.events.remove(room, 'mousemove', this.editObjectMousemove);
//elation.events.remove(this, 'wheel', this.editObjectMousewheel);
elation.events.remove(this, 'mousedown', this.editObjectClick);
elation.events.remove(document, 'pointerlockchange', this.editObjectHandlePointerlock);
// deactivate context
janus.engine.systems.controls.deactivateContext('roomedit', this);
if (!this.roomedit.moving) {
this.roomedit.moving = true;
janus.engine.systems.controls.deactivateContext('roomedit_togglemove', this);
}
}
editObjectRevert() {
var object = this.roomedit.object;
if (object) {
for (var k in this.roomedit.startattrs) {
var value = this.roomedit.startattrs[k];
object[k] = value;
}
this.editObjectStop();
}
}
editObjectMousemove(ev) {
if (this.roomedit.object) {
var obj = this.roomedit.object;
if (this.roomedit.moving && this.roomedit.raycast && ev.element.getProxyObject() !== obj) {
let raycast = player.raycast(V(0, 0, -1));
if (raycast) {
let hit;
for (let i = 0; i < raycast.length; i++) {
if (raycast[i].object !== obj) {
hit = raycast[i];
break;
}
}
if (hit) {
this.roomedit.objectPosition = hit.point;
}
} else {
this.roomedit.objectPosition = ev.data.point;
}
let manipulator = this.getManipulator();
manipulator.enabled = false;
this.editObjectUpdate();
}
if (ev.shiftKey) {
this.roomedit.parentObject = ev.data.thing;
} else {
this.roomedit.parentObject = null;
}
}
}
editObjectUpdate() {
var obj = this.roomedit.object;
if (!obj) return;
var bbox = this.roomedit.objectBoundingBox;
var point = this.roomedit.objectPosition || this.roomedit.object.position;
if (!bbox || isNaN(bbox.min.x) || isNaN(bbox.max.x)) {
bbox = this.roomedit.objectBoundingBox = obj.getBoundingBox(true);
}
var headpos = player.head.localToWorld(V(0,0,0));
var cursorpos = obj.parent.worldToLocal(translate(point, V(0, -bbox.min.y, 0)), true);
cursorpos = this.editObjectSnapVector(cursorpos, this.roomedit.snap);
var dir = V(cursorpos).sub(headpos);
var distance = dir.length();
dir.multiplyScalar(1/distance);
if (distance > 2500) distance = 10; // probably the skybox
//distance = Math.min(distance, 20);
var newpos = V(headpos).add(V(dir).multiplyScalar(distance));
obj.pos = newpos;//obj.parent.worldToLocal(newpos);
obj.sync = true;
if (this.roomedit.parentObject) {
this.editObjectShowParentWireframe();
} else if (this.roomedit.parentwireframe) {
this.editObjectRemoveParentWireframe();
}
}
editObjectMousewheel(ev) {
//this.roomedit.distancescale *= (ev.deltaY > 0 ? .9 : 1.1);
let obj = this.roomedit.object,
mode = this.editObjectGetMode(),
editor = this.objectinfo.getModeEditor();
if (obj) {
let attrtype = editor._elation.classdef.value.type,
defs = obj._proxyobject._proxydefs;
let attrname = defs[editor.propertyname][1];
if (attrtype == 'vector3') {
let move = this.roomedit.snap * (ev.deltaY < 0 ? 1 : -1);
if (ev.altKey) {
obj[attrname].x += move;
}
if (ev.ctrlKey) {
obj[attrname].y += move;
}
if (ev.shiftKey) {
obj[attrname].z += move;
}
if (!ev.ctrlKey && !ev.altKey && !ev.shiftKey) {
// Default to translating on the Y axis (FIXME - is this really the best default?)
obj[attrname].y += move;
}
this.editObjectSnapVector(obj[attrname], this.roomedit.snap);
} else if (attrtype == 'euler') {
let rot = new THREE.Euler();
let move = (ev.deltaY > 0 ? -1 : 1) * this.roomedit.rotationsnap * THREE.MathUtils.DEG2RAD;
if (ev.ctrlKey) {
rot.x += move;
}
if (ev.altKey) {
rot.y += move;
}
if (ev.shiftKey) {
rot.z += move;
}
if (!ev.ctrlKey && !ev.altKey && !ev.shiftKey) {
// Default to rotating around Y axis
rot.y += move;
}
let quat = new THREE.Quaternion().setFromEuler(obj[attrname].radians);
let quat2 = new THREE.Quaternion().setFromEuler(rot);
quat.multiply(quat2);
obj[attrname].radians.setFromQuaternion(quat);
//this.editObjectSnapVector(obj[attrname], this.roomedit.rotationsnap * THREE.MathUtils.DEG2RAD);
} else if (attrtype == 'color') {
let move = 4/255 * (ev.deltaY < 0 ? 1 : -1);
let col = obj[mode].clone();
if (ev.altKey) {
col.r += move;
}
if (ev.ctrlKey) {
col.g += move;
}
if (ev.shiftKey) {
col.b += move;
}
if (!ev.ctrlKey && !ev.altKey && !ev.shiftKey) {
// Default to translating on the Y axis (FIXME - is this really the best default?)
col.r += move;
col.g += move;
col.b += move;
}
col.r = THREE.MathUtils.clamp(col.r, 0, 1);
col.g = THREE.MathUtils.clamp(col.g, 0, 1);
col.b = THREE.MathUtils.clamp(col.b, 0, 1);
console.log('opacity after', obj.opacity);
obj[mode] = col;
obj.opacity = obj.opacity;
//this.editObjectSnapVector(obj[attrname], this.roomedit.snap);
} else if (attrtype == 'float') {
let attrdef = obj._thingdef.properties[attrname];
let amount = (ev.deltaY < 0 ? 1 : -1);
let min = -Infinity,
max = Infinity;
let divisor = 10;
if (ev.shiftKey) {
divisor *= 2;
}
if (ev.ctrlKey) {
divisor *= 10;
}
if ('min' in attrdef && 'max' in attrdef) {
min = attrdef.min;
max = attrdef.max;
amount *= (max - min) / divisor;
} else {
amount /= divisor;
}
obj[attrname] = THREE.MathUtils.clamp(obj[attrname] + amount, min, max);
} else if (attrtype == 'boolean' || attrtype == 'bool') {
if (ev.deltaY < 1) {
obj[attrname] = true;
} else {
obj[attrname] = false;
}
}
obj.sync = true;
obj.refresh();
this.editObjectShowInfo(obj);
ev.preventDefault();
}
}
editObjectKeydown(ev) {
this.updateEditConstraints(ev);
}
editObjectKeyup(ev) {
this.updateEditConstraints(ev);
}
editObjectClick(ev) {
if (this.roomedit.object) {
if (ev.button == 0) {
//this.editObjectStop();
} else if (ev.button == 2) {
if (ev.shiftKey) {
console.log('select additional object', ev.data.thing);
} else {
//this.editObjectRevert();
}
}
}
}
editObjectGetMode() {
//return this.roomedit.modes[this.roomedit.modeid];
return this.objectinfo.mode;
}
editObjectGetAttributeType(mode) {
let obj = this.roomedit.object;
let proxydef = obj._proxydefs[mode];
let attrdef = obj._thingdef.properties[proxydef[1]];
return attrdef.type;
}
editObjectToggleMode(ev) {
var roomedit = ev.target.roomedit;
if (!roomedit.object) return;
if (ev.value) {
/*
var modes = roomedit.modes,
modeid = (roomedit.modeid + 1) % modes.length;
roomedit.modeid = modeid;
ev.target.editObjectShowInfo(roomedit.object);
ev.target.setMode(modes[modeid]);
*/
ev.target.nextMode();
ev.target.editObjectShowInfo(roomedit.object);
}
}
editObjectToggleModeReverse(ev) {
var roomedit = ev.target.roomedit;
if (!roomedit.object) return;
if (ev.value) {
/*
var modes = roomedit.modes,
modeid = (modes.length + roomedit.modeid - 1) % modes.length;
roomedit.modeid = modeid;
ev.target.editObjectShowInfo(roomedit.object);
ev.target.setMode(modes[modeid]);
*/
ev.target.prevMode();
ev.target.editObjectShowInfo(roomedit.object);
}
}
editObjectManipulate(vec) {
var mode = this.editObjectGetMode();
var obj = this.roomedit.object;
var space = 'world'; // 'world', 'local', 'player'
var player = janus.player;
if (!obj) return;
switch (mode) {
case 'pos':
if (space == 'world') {
var newpos = translate(obj.pos, scalarMultiply(vec, this.roomedit.snap));
obj.pos = this.editObjectSnapVector(newpos, this.roomedit.snap);
} else if (space == 'player') {
var dir = player.head.localToWorld(vec).sub(player.head.localToWorld(V(0)));
var newpos = translate(obj.pos, scalarMultiply(dir, this.roomedit.snap));
obj.pos = this.editObjectSnapVector(newpos, this.roomedit.snap);
} else if (space == 'local') {
var dir = obj.localToWorld(vec).sub(obj.localToWorld(V(0)));
obj.pos = translate(obj.pos, scalarMultiply(dir, this.roomedit.snap));
}
break;
case 'rotation':
/*
var amount = Math.PI/2 * this.roomedit.snap;
var euler = new THREE.Euler().setFromQuaternion(obj._target.orientation);
//vec.multiplyScalar(amount);
euler.x += vec.x;
euler.y += vec.y;
euler.z += vec.z;
//var quat = new THREE.Quaternion().setFromEuler(euler);
//obj._target.properties.orientation.copy(quat);
let rot = obj.rotation;
var amount = 90;
if (this.roomedit.snap == .1) amount = 45;
else if (this.roomedit.snap == .01) amount = 15;
else if (this.roomedit.snap == .001) amount = 5;
obj.properties.rotation.set(rot.x * 180 / Math.PI + vec.x * amount, rot.y * 180 / Math.PI + vec.y * amount, rot.z * 180 / Math.PI + vec.z * amount);
obj.updateOrientationFromEuler();
obj.updateDirvecsFromOrientation();
//this.roomedit.object.pos = vec;
*/
var amount = 90;
if (this.roomedit.snap == .1) amount = 45;
else if (this.roomedit.snap == .01) amount = 15;
else if (this.roomedit.snap == .001) amount = 5;
let rot = new THREE.Euler().setFromVector3(vec.clone().multiplyScalar(amount * THREE.MathUtils.DEG2RAD));
let quat = new THREE.Quaternion();
quat.setFromEuler(rot);
//obj.rotation = V(rot.x * THREE.MathUtils.RAD2DEG, rot.y * THREE.MathUtils.RAD2DEG, rot.z * THREE.MathUtils.RAD2DEG);
obj.orientation.multiply(quat);
console.log('rotate it', vec.clone().multiplyScalar(amount), amount, rot, quat);
break;
case 'scale':
if (space == 'world') {
obj.scale.add(vec.multiplyScalar(this.roomedit.snap));
} else if (space == 'player') {
var dir = player.head.localToWorld(vec, true).sub(player.head.localToWorld(V(0)));
var newscale = translate(obj.scale, dir.multiplyScalar(this.roomedit.snap));
obj.scale = this.editObjectSnapVector(newscale, this.roomedit.snap);
} else if (space == 'local') {
var newscale = translate(obj.scale, vec.multiplyScalar(this.roomedit.snap));
obj.scale = this.editObjectSnapVector(newscale, this.roomedit.snap);
}
break;
case 'col':
let snap = 10 / 255;
obj.col = V(THREE.MathUtils.clamp(obj.col.r + vec.x * snap, 0, 1),
THREE.MathUtils.clamp(obj.col.g + vec.y * snap, 0, 1),
THREE.MathUtils.clamp(obj.col.b + vec.z * snap, 0, 1));
console.log('change color', obj.col, vec);
break;
}
obj.sync = true;
this.editObjectShowInfo(obj);
}
editObjectSnapVector(vector, snap) {
vector.x = Math.round(vector.x / snap) * snap;
vector.y = Math.round(vector.y / snap) * snap;
vector.z = Math.round(vector.z / snap) * snap;
return vector;
}
editObjectSetSnap(amount) {
this.roomedit.snap = amount;
this.setTranslationSnap(amount);
if (amount == 1) this.setRotationSnap(45);
else if (amount == .1) this.setRotationSnap(15);
else if (amount == .01) this.setRotationSnap(5);
else if (amount == .001) this.setRotationSnap(1.25);
console.log('set snap to', amount);
}
// FIXME - the following functions aren't bound to "this", because the control system isn't properly managing context
editObjectManipulateLeft(ev) {
if (ev.value) {
//ev.target.editObjectManipulate(V(-1, 0, 0));
ev.target.roomedit.movespeed.x = -1;
ev.target.roomedit.raycast = false;
ev.target.roomedit.keyrepeatdelay = 300;
} else if (ev.target.roomedit.movespeed.x == -1) {
ev.target.roomedit.movespeed.x = 0;
}
ev.target.editObjectManipulateTimer();
}
editObjectManipulateRight(ev) {
if (ev.value) {
//ev.target.editObjectManipulate(V(1,0,0));
ev.target.roomedit.movespeed.x = 1;
ev.target.roomedit.raycast = false;
ev.target.roomedit.keyrepeatdelay = 300;
} else if (ev.target.roomedit.movespeed.x == 1) {
ev.target.roomedit.movespeed.x = 0;
}
ev.target.editObjectManipulateTimer();
}
editObjectManipulateUp(ev) {
if (ev.value) {
//ev.target.editObjectManipulate(V(0,1,0));
ev.target.roomedit.movespeed.y = 1;
ev.target.roomedit.raycast = false;
ev.target.roomedit.keyrepeatdelay = 300;
} else if (ev.target.roomedit.movespeed.y == 1) {
ev.target.roomedit.movespeed.y = 0;
}
ev.target.editObjectManipulateTimer();
}
editObjectManipulateDown(ev) {
if (ev.value) {
//ev.target.editObjectManipulate(V(0,-1,0));
ev.target.roomedit.movespeed.y = -1;
ev.target.roomedit.raycast = false;
ev.target.roomedit.keyrepeatdelay = 300;
} else if (ev.target.roomedit.movespeed.y == -1) {
ev.target.roomedit.movespeed.y = 0;
}
ev.target.editObjectManipulateTimer();
}
editObjectManipulateIn(ev) {
if (ev.value) {
//ev.target.editObjectManipulate(V(0,0,1));
ev.target.roomedit.movespeed.z = -1;
ev.target.roomedit.raycast = false;
ev.target.roomedit.keyrepeatdelay = 300;
} else if (ev.target.roomedit.movespeed.z == -1) {
ev.target.roomedit.movespeed.z = 0;
}
ev.target.editObjectManipulateTimer();
}
editObjectManipulateOut(ev) {
if (ev.value) {
//ev.target.editObjectManipulate(V(0,0,-1));
ev.target.roomedit.movespeed.z = 1;
ev.target.roomedit.raycast = false;
ev.target.roomedit.keyrepeatdelay = 300;
} else if (ev.target.roomedit.movespeed.z == 1) {
ev.target.roomedit.movespeed.z = 0;
}
ev.target.editObjectManipulateTimer();
}
editObjectManipulateToggle(ev) {
if (ev.value) {
if (this.roomedit.object) {
let mode = this.editObjectGetMode();
let attrtype = this.editObjectGetAttributeType(mode);
switch (attrtype) {
case 'vector3':
case 'euler':
if (this.roomedit.moving) {
this.roomedit.moving = false;
janus.engine.systems.controls.activateContext('roomedit_togglemove', this);
} else {
this.roomedit.moving = true;
janus.engine.systems.controls.deactivateContext('roomedit_togglemove', this);
}
break;
case 'bool':
case 'boolean':
this.roomedit.object[mode] = !this.roomedit.object[mode];
this.roomedit.object.sync = true;
break;
case 'string':
let editor = this.objectinfo.getModeEditor();
}
}
}
}
editObjectManipulateTimer() {
if (this.roomedit.movespeed.x || this.roomedit.movespeed.y || this.roomedit.movespeed.z) {
this.editObjectManipulate(V(this.roomedit.movespeed));
if (this.roomedit.object && !this.roomedit.movetimer) {
this.roomedit.movetimer = setTimeout(() => {
this.roomedit.movetimer = false;
this.roomedit.keyrepeatdelay = 75;
this.editObjectManipulateTimer();
}, this.roomedit.keyrepeatdelay);
}
}
}
editObjectManipulateMouse(ev) {
ev.target.editObjectManipulate(V(ev.value[0], -ev.value[1], 0));
}
editObjectToggleRaycast(ev) {
ev.target.roomedit.raycast = ev.value;
}
editObjectSnapOnes(ev) {
if (ev.value) ev.target.editObjectSetSnap(1);
}
editObjectSnapTenths(ev) {
if (ev.value) ev.target.editObjectSetSnap(.1);
}
editObjectSnapHundredths(ev) {
if (ev.value) ev.target.editObjectSetSnap(.01);
}
editObjectSnapThousandths(ev) {
if (ev.value) ev.target.editObjectSetSnap(.001);
}
editObjectDelete(ev) {
if (ev.value) {
//ev.target.removeObject(ev.target.roomedit.object);
room.deletions.push(ev.target.roomedit.object);
this.history.push({type: 'addobjects', object: ev.target.roomedit.object});
ev.target.editObjectStop(true);
}
}
editObjectCancel(ev) {
if (ev.value) {
if (ev.target.roomedit.objectIsNew) {
ev.target.editObjectStop(true);
} else {
ev.target.editObjectRevert();
}
}
}
editObjectToggleMove(ev) {
var roomedit = ev.target.roomedit;
if (ev.value == 1) {
roomedit.moving = true;
this.engine.systems.controls.activateContext('roomedit', this);
} else if (ev.value == 0) {
roomedit.moving = false;
this.engine.systems.controls.deactivateContext('roomedit', this);
}
}
editObjectHandlePointerlock(ev) {
if (!document.pointerLockElement) {
this.editObjectRevert();
}
}
editObjectCopy(ev) {
let obj = this.roomedit.object;
console.log('copy the object', obj);
if (obj) {
this.roomedit.pastebuffer = obj;
/*
let data = new DataTransfer();
data.items.add('text/plain', 'blublub');
//data.items.add('x-janus/x-' + obj.tag, 'cool huh');
navigator.clipboard.write(data).then(() => {
console.log('success!');
}, (e) => {
console.log('FAILED!', e);
});
*/
}
}
editObjectCut(ev) {
let obj = this.roomedit.object;
if (obj) {
this.editObjectCopy(ev);
this.editObjectRevert();
obj.parent.removeChild(obj);
}
}
editObjectPaste(ev) {
if (this.roomedit.pastebuffer) {
let oldobj = this.roomedit.pastebuffer;
if (this.roomedit.object) {
this.editObjectStop();
}
let raycast = player.raycast(V(0, 0, -1));
if (raycast) {
this.roomedit.objectPosition = raycast[0].point;
}
let newobj = oldobj.clone();
this.roomedit.raycast = true;
setTimeout(() => {
this.editObject(newobj);
this.editObjectUpdate();
let manipulator = this.getManipulator();
manipulator.enabled = false;
}, 0);
//oldobj.parent.add(oldobj.clone());
}
}
handleDragOver(ev) {
//ev.dataTransfer.dropEffect = "link"
ev.preventDefault();
}
handleDrop(ev) {
ev.preventDefault();
var files = ev.dataTransfer.files,
items = ev.dataTransfer.items;
this.roomedit.raycast = true;
var objects = [];
if (files.length > 0) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
objects[i] = this.loadObjectFromFile(file);
}
this.editObject(objects[0], true);
} else if (items.length > 0) {
var types = {};
var numitems = items.length;
for (var i = 0; i < numitems; i++) {
var type = items[i].type;
types[type] = items[i];
}
if (types['text/x-jml']) {
types['text/x-jml'].getAsString((jml) => { this.loadObjectFromJML(jml) });
} else if (types['text/uri-list']) {
types['text/uri-list'].getAsString((urilist) => { let newobjects = this.loadObjectFromURIList(urilist); objects.push.apply(newobjects); }) ;
} else if (types['text/x-moz-url']) {
types['text/x-moz-url'].getAsString((urilist) => { let newobjects = this.loadObjectFromURIList(urilist); objects.push.apply(newobjects); } );
}
}
this.history.push({type: 'addobjects', objects: objects});
/*
if (janus.engine.systems.admin.hidden) {
janus.engine.systems.controls.requestPointerLock();
}
*/
}
handleThingAdd(ev) {
console.log('[editor] scene added a thing', ev);
setTimeout(() => {
this.scenetree.refreshList();
}, 0);
}
handleThingRemove(ev) {
setTimeout(() => {
this.scenetree.refreshList();
}, 0);
}
loadObjectFromURIList(list) {
var urls = list.split('\n');
var objects = [];
for (var i = 0; i < urls.length; i++) {
let newobject = false;
var hashidx = urls[i].indexOf('#');
let url = urls[i];
let trimmedurl = (hashidx == -1 ? urls[i] : urls[i].substring(0, hashidx)).trim();
let type = 'Object';
let objargs = {
js_id: player.userid + '-' + trimmedurl + '-' + window.uniqueId(),
//cull_face: 'none',
sync: true,
pos: new elation.physics.vector3().copy(player.vectors.cursor_pos),
persist: true
};
if (trimmedurl.length > 0) {
var schemeidx = trimmedurl.indexOf(':');
if (schemeidx != -1) {
// Handle special schemes which are used for internal primitives
var scheme = trimmedurl.substr(0, schemeidx);
if (scheme == 'janus') {
let newobjargs = trimmedurl.substr(schemeidx+1).split('/'),
objtype = newobjargs.shift();
for (let j = 0; j < newobjargs.length; j++) {
let parts = newobjargs[j].split('=');
objargs[parts[0]] = decodeURIComponent(parts[1]);
}
if (objargs.id) {
objargs.collision_id = objargs.id;
}
newobject = room.createObject(objtype, objargs);
objects.push(newobject);
if (objects[0]) {
this.editObject(objects[0], true);
}
}
}
if (!newobject) {
this.detectMimeTypeForURL(url).then(mimetype => {
if (!mimetype) mimetype = 'application/octet-stream';
if (trimmedurl.match(/\.(png|gif|jpg|jpeg)/i) || mimetype.match(/^image\//)) {
type = 'image';
} else if (mimetype.match(/^audio\//)) {
type = 'sound';
room.loadNewAsset('sound', {
id: objargs.js_id,
src: url
});
objargs.sound_id = objargs.js_id;
objargs.id = objargs.js_id;
objargs.auto_play = true;
} else if (mimetype.match(/^video\//)) {
type = 'video';
room.loadNewAsset('video', {
id: objargs.js_id,
src: url,
auto_play: true
});
objargs.video_id = objargs.js_id;
objargs.id = objargs.js_id;
//objargs.auto_play = true;
} else if (mimetype.match(/^text\/html/)) {
type = 'object';
room.loadNewAsset('websurface', {
id: objargs.js_id,
src: url
});
objargs.websurface_id = objargs.js_id;
objargs.id = 'plane';
} else {
objargs.id = trimmedurl;
}
if (typeof EventBridge != 'undefined') {
// if EventBridge is defined, we're (probably) running inside of High Fidelity, so just spawn this object
EventBridge.emitWebEvent(JSON.stringify({
type: 'spawn',
data: objargs.id
}));
} else {
newobject = room.createObject(type, objargs);
objects.push(newobject);
}
if (objects[0]) {
this.editObject(objects[0], true);
}
});
}
}
}
return objects;
}
detectMimeTypeForURL(url) {
return new Promise((resolve, reject) => {
// Fetch the file so we can read the MIME type.
// FIXME - Ideally we'd request only the first 1k of data with a Range header, so we don't have to download
// the whole thing just to determine type - some types can be streamed more effectively if we let the browser
// handle them (eg, video). However, CORS is a nightmare, and most servers don't whitelist Range headers.
// If mime type detection fails, we could also check for known signatures in the first 1k of data, but we don't do that here yet
if (false) {
let proxiedurl = url;
if (elation.engine.assets.corsproxy && !url.match(/^https?:\/\/localhost/) && !url.match(/^data:/) && !url.match(/^janus-vfs:/)) {
proxiedurl = elation.engine.assets.corsproxy + url;
}
fetch(proxiedurl, { headers: { /*'Range': 'bytes=0-1023'}*/ }}).then(res => resolve(res.headers.get('content-type')))
.catch((e) => {
reject();
});
} else {
elation.engine.assetdownloader.fetchURL(url).then(ev => {
let res = ev.target;
if (res.headers) {
resolve(res.headers.get('content-type'));
} else {
// FIXME - no headers, unknown type
resolve();
}
});
}
});
}
loadObjectFromJML(jml) {
room.applyEditXML(jml);
}
loadObjectFromFile(file) {
var name = file.name,
url = URL.createObjectURL(file),
object;
var type = 'object',
args = {
id: name,
pos: player.vectors.cursor_pos.clone(),
sync: true,
persist: true,
},
assetargs = {
id: name,
src: url
};
var mimeparts = file.type.split('/');
if (mimeparts[0] == 'image') {
type = 'image';
// We're going to send the local file's image data over the network, but we want to cap at a max size to avoid bogging down the network
let maxsize = 512;
args.onload = (ev) => {
if (!args.loaded) {
args.loaded = true;
let fullcan