1488 lines
49 KiB
JavaScript
1488 lines
49 KiB
JavaScript
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
"use strict";
|
|
|
|
// dep: ext.tween
|
|
// dep: add.three
|
|
// dep: add.array
|
|
// dep: moto.orbit
|
|
gapp.register("moto.space", [], (root, exports) => {
|
|
|
|
const { moto } = root;
|
|
const { WebGLRenderer } = THREE;
|
|
const nav = navigator;
|
|
|
|
let WIN = window,
|
|
DOC = document,
|
|
SCENE = new THREE.Scene(),
|
|
WORLD = new THREE.Group(),
|
|
PI = Math.PI,
|
|
PI2 = PI / 2,
|
|
PI4 = PI / 4,
|
|
panX = 0,
|
|
panY = 0,
|
|
panZ = 0,
|
|
home = 0,
|
|
up = PI4,
|
|
gridZOff = 0,
|
|
tweenTime = 500,
|
|
tweenDelay = 20,
|
|
platformZOff = 0,
|
|
perspective = 35,
|
|
refreshTimeout = null,
|
|
refreshRequested = false,
|
|
selectRecurse = false,
|
|
defaultKeys = true,
|
|
initialized = false,
|
|
alignedTracking = false,
|
|
skyAmbient,
|
|
skyGridColor = 0xcccccc,
|
|
skyGridMaterial = undefined,
|
|
showSkyGrid = false,
|
|
showPlatform = true,
|
|
hidePlatformBelow = true,
|
|
hideGridBelow = false,
|
|
showGrid = true,
|
|
showGridLines = true,
|
|
showFocus = 0,
|
|
focalPoint = undefined,
|
|
lightInfo = {
|
|
mode: 2,
|
|
array: [],
|
|
intensity: 0.09,
|
|
debug: false
|
|
},
|
|
cameraLight = addLight(0, 0, 0, lightInfo.intensity),
|
|
trackDelta = {x:0, y:0, z:0},
|
|
origin = {x:0, y:0, z: 0},
|
|
mouse = {x: 0, y: 0},
|
|
mouseStart = null,
|
|
mouseDragPoint = null,
|
|
mouseDragStart = null,
|
|
mouseDownSelect,
|
|
mouseUpSelect,
|
|
mouseUp,
|
|
mouseDown,
|
|
mouseHover,
|
|
mouseDrag,
|
|
grid = {
|
|
origin: origin,
|
|
unitMinor: 0,
|
|
unitMajor: 0,
|
|
colorMinor: 0xeeeeee,
|
|
colorMajor: 0xcccccc,
|
|
colorX: 0xff6666,
|
|
colorY: 0x6666ff,
|
|
zoff: 0,
|
|
opacity: 1,
|
|
view: undefined,
|
|
axes: undefined,
|
|
lines: undefined
|
|
},
|
|
ruler = {
|
|
x1: 0,
|
|
x2: 0,
|
|
y1: 0,
|
|
y2: 0,
|
|
xlabel: 'X',
|
|
ylabel: 'Y',
|
|
xon: undefined,
|
|
yon: undefined,
|
|
factor: undefined,
|
|
view: undefined
|
|
},
|
|
psize = {},
|
|
timers = {},
|
|
fontColor = '#333333',
|
|
fontScale = 1.4, // computed relative to grid size
|
|
rulerColor,
|
|
axisColor,
|
|
axesOn = true,
|
|
volumeOn = true,
|
|
viewControl,
|
|
trackPlane,
|
|
platform,
|
|
platformHover,
|
|
platformClick,
|
|
platformClickAt,
|
|
platformOnMove,
|
|
platformOnMoveTime = 500,
|
|
platformMoveTimer,
|
|
platformBelt = false,
|
|
volume,
|
|
camera,
|
|
renderer,
|
|
container,
|
|
raycaster,
|
|
freezeTo,
|
|
freeze,
|
|
isRound = false,
|
|
platformMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0xeeeeee,
|
|
specular: 0xeeeeee,
|
|
shininess: 0,
|
|
transparent: true,
|
|
opacity: 0.6,
|
|
side: THREE.DoubleSide
|
|
}),
|
|
hiddenKey,
|
|
vizChange,
|
|
docVisible = true,
|
|
antiAlias = window.devicePixelRatio <= 1,
|
|
lastAction = Date.now(),
|
|
renderTime = 0,
|
|
fps = 0;
|
|
|
|
if (typeof DOC.hidden !== "undefined") {
|
|
hiddenKey = "hidden";
|
|
vizChange = "visibilitychange";
|
|
} else if (typeof DOC.msHidden !== "undefined") {
|
|
hiddenKey = "msHidden";
|
|
vizChange = "msvisibilitychange";
|
|
} else if (typeof DOC.webkitHidden !== "undefined") {
|
|
hiddenKey = "webkitHidden";
|
|
vizChange = "webkitvisibilitychange";
|
|
}
|
|
|
|
DOC.addEventListener(vizChange, () => {
|
|
docVisible = DOC[hiddenKey] ? false : true;
|
|
}, false);
|
|
|
|
function updateLastAction() {
|
|
lastAction = Date.now();
|
|
}
|
|
|
|
function delayed(key, time, fn) {
|
|
clearTimeout(timers[key]);
|
|
timers[key] = setTimeout(fn, time);
|
|
}
|
|
|
|
function valueOr(val, def) {
|
|
return val !== undefined ? val : def;
|
|
}
|
|
|
|
WORLD.contains = (obj) => {
|
|
return WORLD.children.contains(obj);
|
|
};
|
|
|
|
/** ******************************************************************
|
|
* TWEENing Functions
|
|
******************************************************************* */
|
|
|
|
function tweenit() {
|
|
TWEEN.update();
|
|
setTimeout(tweenit, tweenDelay);
|
|
}
|
|
|
|
tweenit();
|
|
|
|
function tweenCamPan(x,y,z,left,up) {
|
|
updateLastAction();
|
|
let pos = viewControl.getPosition();
|
|
pos.panX = x;
|
|
pos.panY = y;
|
|
pos.panZ = z;
|
|
if (left !== undefined) pos.left = left;
|
|
if (up !== undefined) pos.up = up;
|
|
tweenCam(pos);
|
|
}
|
|
|
|
function tweenCam(pos) {
|
|
let tf = function () {
|
|
viewControl.setPosition(this);
|
|
updateLastAction();
|
|
refresh();
|
|
};
|
|
let from = Object.clone(viewControl.getPosition());
|
|
let to = Object.clone(pos);
|
|
let dist = Math.abs(from.left - to.left);
|
|
if (dist > Math.PI) {
|
|
if (from.left < to.left) {
|
|
from.left += Math.PI * 2;
|
|
} else {
|
|
from.left -= Math.PI * 2;
|
|
}
|
|
}
|
|
new TWEEN.Tween(from).
|
|
to(to, tweenTime).
|
|
onUpdate(tf).
|
|
onComplete(() => {
|
|
viewControl.setPosition(pos);
|
|
updateLastAction();
|
|
refresh();
|
|
let { then } = pos;
|
|
if (typeof then === 'function') {
|
|
then();
|
|
}
|
|
}).
|
|
start();
|
|
}
|
|
|
|
/** ******************************************************************
|
|
* Utility Functions
|
|
******************************************************************* */
|
|
|
|
function width() { return WIN.innerWidth }
|
|
|
|
function height() { return WIN.innerHeight }
|
|
|
|
function aspect() { return width() / height() }
|
|
|
|
function addEventListener(el, key, fn) {
|
|
el.addEventListener(key, fn);
|
|
}
|
|
|
|
function addEventHandlers(el, pairs) {
|
|
for (let i=0; i<pairs.length; i += 2) {
|
|
addEventListener(el, pairs[i], pairs[i+1]);
|
|
}
|
|
}
|
|
|
|
function onEnterKey(el, fn, onblur) {
|
|
if (Array.isArray(el)) {
|
|
for (let i=0; i<el.length; i += 2) onEnterKey(el[i], el[i+1], fn);
|
|
return;
|
|
}
|
|
addEventListener(el, 'keyup', function(event) {
|
|
if (event.keyCode === 13) fn(event);
|
|
});
|
|
if (onblur) {
|
|
addEventListener(el, 'blur', function(event) {
|
|
fn(event);
|
|
});
|
|
}
|
|
}
|
|
|
|
function addLight(x, y, z, i, color = 0xffffff) {
|
|
let l = new THREE.DirectionalLight(color, i * 4);
|
|
l.position.set(x,z,y);
|
|
if (lightInfo.debug) {
|
|
let b; l.add(b = new THREE.Mesh(
|
|
new THREE.BoxGeometry(1,1,1),
|
|
new THREE.MeshBasicMaterial( {color: 0xff0000} )
|
|
)); b.scale.set(5,5,5);
|
|
}
|
|
SCENE.add(l);
|
|
return l;
|
|
}
|
|
|
|
// 4 corners bottom, 4 axis centers top
|
|
function updateLights() {
|
|
let x = psize.width;
|
|
let y = psize.depth;
|
|
let z = Math.max(x,y);
|
|
|
|
let { mode, intensity, array } = lightInfo;
|
|
|
|
// remove old lights
|
|
for (let l of array) {
|
|
SCENE.remove(l);
|
|
}
|
|
|
|
// add new
|
|
let x0 = -x/2, y0 = -y/2, z0 = 0;
|
|
let x1 = x/2, y1 = y/2, z1 = z / 2, z2 = z;
|
|
|
|
switch (mode) {
|
|
case 0: array = [
|
|
addLight( x1, y0, z1, intensity * 1.5),
|
|
addLight( x0, y1, -z1, intensity * 0.7)];
|
|
break;
|
|
case 1: array = [
|
|
addLight( x1, y1, z1, intensity * 2.5),
|
|
addLight( x0, y1, -z1, intensity * 0.5),
|
|
addLight( x0, y0, z1, intensity * 2.5, 0xeeeeee),
|
|
addLight( x1, y0, -z1, intensity * 0.5, 0xeeeeee)];
|
|
break;
|
|
case 2: array = [
|
|
addLight( x1, y1, z1, intensity * 2.5),
|
|
addLight( x0, y1, -z1, intensity * 0.5),
|
|
addLight( x0, y0, z1, intensity * 2.5, 0xeeeeee),
|
|
addLight( x1, y0, -z1, intensity * 0.5, 0xeeeeee),
|
|
addLight( 0, 0, z2, intensity * 1.2),
|
|
addLight( 0, 0, -z2, intensity * 0.8)];
|
|
break;
|
|
}
|
|
|
|
lightInfo.array = array;
|
|
lightInfo.camera = cameraLight;
|
|
lightInfo.ambient = skyAmbient;
|
|
requestRefresh();
|
|
}
|
|
|
|
function updatePlatformPosition() {
|
|
if (isRound) {
|
|
platform.position.y = -platform.scale.y/2 - platformZOff;
|
|
} else {
|
|
platform.position.y = -platform.scale.z/2 - platformZOff;
|
|
}
|
|
requestRefresh();
|
|
}
|
|
|
|
function setPlatformSize(
|
|
width = psize.width || 300,
|
|
depth = psize.depth || 300,
|
|
height = psize.height || 2.5,
|
|
maxz = psize.maxz || 100
|
|
) {
|
|
psize = { width, depth, height, maxz };
|
|
if (isRound) {
|
|
platform.scale.set(width, height, depth);
|
|
} else {
|
|
platform.scale.set(width, depth, height);
|
|
}
|
|
viewControl.maxDistance = Math.max(width,depth) * 4;
|
|
updatePlatformPosition();
|
|
updateLights();
|
|
if (volume) {
|
|
SCENE.remove(volume);
|
|
THREE.dispose(volume);
|
|
volume = null;
|
|
}
|
|
if (maxz) {
|
|
const points = [
|
|
// pillars
|
|
{x: -width/2, z: -depth/2, y: 0},
|
|
{x: -width/2, z: -depth/2, y: maxz},
|
|
{x: width/2, z: depth/2, y: 0},
|
|
{x: width/2, z: depth/2, y: maxz},
|
|
{x: -width/2, z: depth/2, y: 0},
|
|
{x: -width/2, z: depth/2, y: maxz},
|
|
{x: width/2, z: -depth/2, y: 0},
|
|
{x: width/2, z: -depth/2, y: maxz},
|
|
// top
|
|
{x: -width/2, z: -depth/2, y: maxz},
|
|
{x: -width/2, z: depth/2, y: maxz},
|
|
{x: -width/2, z: -depth/2, y: maxz},
|
|
{x: width/2, z: -depth/2, y: maxz},
|
|
{x: width/2, z: depth/2, y: maxz},
|
|
{x: width/2, z: -depth/2, y: maxz},
|
|
{x: width/2, z: depth/2, y: maxz},
|
|
{x: -width/2, z: depth/2, y: maxz},
|
|
];
|
|
SCENE.add(volume = makeLinesFromPoints(points, grid.colorMinor));
|
|
showVolume(volumeOn);
|
|
}
|
|
}
|
|
|
|
function setPlatformSizeUpdateGrid(width, depth, height, maxz) {
|
|
freeze = { width, depth, height, maxz };
|
|
setPlatformSize(width, depth, height, maxz);
|
|
setGrid(grid.unitMajor, grid.unitMinor);
|
|
clearTimeout(freezeTo);
|
|
freezeTo = setTimeout(() => { freeze = undefined }, 10);
|
|
}
|
|
|
|
function setPlatformColor(color) {
|
|
let was = platform.material.color.getHex();
|
|
platform.material.color.set(color);
|
|
requestRefresh();
|
|
return was;
|
|
}
|
|
|
|
function setFont(options) {
|
|
if (options.color) fontColor = options.color;
|
|
if (options.scale) fontScale = options.scale;
|
|
if (options.axisColor) axisColor = options.axisColor;
|
|
if (options.rulerColor) rulerColor = options.rulerColor;
|
|
updateRulers();
|
|
}
|
|
|
|
function showAxes(bool) {
|
|
axesOn = bool;
|
|
updateRulers();
|
|
}
|
|
|
|
function showVolume(bool) {
|
|
volumeOn = bool;
|
|
if (volume) volume.visible = bool;
|
|
requestRefresh();
|
|
}
|
|
|
|
function canvasInMesh(w, h, textAlign, textBaseline, color, size) {
|
|
let canvas = document.createElement('canvas'),
|
|
canvasTexture = new THREE.CanvasTexture(canvas),
|
|
plane = new THREE.PlaneGeometry(w, h),
|
|
context = canvas.getContext('2d'),
|
|
scale = 8;
|
|
|
|
canvas.width = w * scale;
|
|
canvas.height = h * scale;
|
|
|
|
canvas.addEventListener("webglcontextlost", event => {
|
|
console.log('WEB GL CONTEXT LOST');
|
|
event.preventDefault();
|
|
}, false);
|
|
|
|
canvas.addEventListener("webglcontextrestored", event => {
|
|
console.log('WEB GL CONTEXT RESTORED');
|
|
}, false);
|
|
|
|
try {
|
|
context.scale(scale, scale);
|
|
} catch (e) {
|
|
console.log('unable to create label canvas', e ? e.name : 'unknown');
|
|
return {};
|
|
}
|
|
context.fillStyle = color || fontColor;
|
|
context.font = `${size}px sans-serif`;
|
|
context.textAlign = textAlign;
|
|
context.textBaseline = textBaseline;
|
|
canvasTexture.minFilter = THREE.LinearFilter;
|
|
|
|
// set 'transparent' to false to debug mesh bounds
|
|
let material = new THREE.MeshBasicMaterial({transparent: true, map: canvasTexture});
|
|
let mesh = new THREE.Mesh(plane, material);
|
|
|
|
return { context, mesh };
|
|
}
|
|
|
|
|
|
function setRulers(
|
|
xon = ruler.xon,
|
|
yon = ruler.yon,
|
|
factor = ruler.factor || 1,
|
|
xl = ruler.xlabel || 'X',
|
|
yl = ruler.ylabel || 'Y')
|
|
{
|
|
if (xon !== ruler.xon || yon !== ruler.yon || factor !== ruler.factor || xl !== ruler.xlabel || yl !== ruler.ylabel) {
|
|
ruler.factor = factor;
|
|
ruler.xon = xon;
|
|
ruler.yon = yon;
|
|
ruler.xlabel = xl;
|
|
ruler.ylabel = yl;
|
|
updateRulers();
|
|
}
|
|
}
|
|
|
|
function updateRulers() {
|
|
let { xon, yon, factor, xlabel, ylabel } = ruler;
|
|
let x = platform.scale.x,
|
|
y = isRound ? platform.scale.z : platform.scale.y,
|
|
z = isRound ? platform.scale.y : platform.scale.z,
|
|
w = x / 2,
|
|
h = y / 2,
|
|
d = z / 2,
|
|
zp = -d - platformZOff + numOrDef(gridZOff, (z/2-0.1)),
|
|
labelSize = grid.unitMinor * fontScale,
|
|
oldView = ruler.view,
|
|
view = ruler.view = new THREE.Group();
|
|
|
|
if (xon && axesOn) {
|
|
let xPadding = labelSize * 4,
|
|
canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize),
|
|
context = canvas.context,
|
|
mesh = canvas.mesh;
|
|
if (!(context && mesh)) return;
|
|
|
|
if (platformBelt) {
|
|
for (let i = 0; i <= ruler.x2; i += grid.unitMajor) {
|
|
context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0);
|
|
}
|
|
} else {
|
|
for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) {
|
|
context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0);
|
|
}
|
|
for (let i = 0; i <= ruler.x2; i += grid.unitMajor) {
|
|
context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0);
|
|
}
|
|
}
|
|
|
|
context.font = (labelSize * 0.75) + 'px sans-serif';
|
|
context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5);
|
|
mesh.position.set(0, - h - labelSize * 2, zp);
|
|
view.add(mesh);
|
|
}
|
|
|
|
if (yon && axesOn) {
|
|
let yPadding = labelSize,
|
|
canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize),
|
|
context = canvas.context,
|
|
mesh = canvas.mesh;
|
|
if (!(context && mesh)) return;
|
|
|
|
for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) {
|
|
context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2);
|
|
}
|
|
for (let i = 0; i <= ruler.y2; i += grid.unitMajor) {
|
|
context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2);
|
|
}
|
|
|
|
context.font = (labelSize * 0.75) + 'px sans-serif';
|
|
context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2);
|
|
mesh.position.set(-w - labelSize * 2 - 5, 0, zp);
|
|
view.add(mesh);
|
|
}
|
|
|
|
Space.scene.remove(oldView);
|
|
Space.scene.add(view);
|
|
requestRefresh();
|
|
}
|
|
|
|
function setGrid(
|
|
unitMajor = grid.unitMajor,
|
|
unitMinor = grid.unitMinor,
|
|
colorMajor = grid.colorMajor,
|
|
colorMinor = grid.colorMinor)
|
|
{
|
|
if (!unitMajor) {
|
|
return;
|
|
}
|
|
if (
|
|
unitMajor !== grid.unitMajor || unitMinor !== grid.unitMinor ||
|
|
colorMajor !== grid.colorMajor || colorMinor !== grid.colorMinor
|
|
) {
|
|
grid.unitMajor = unitMajor;
|
|
grid.unitMinor = unitMinor;
|
|
grid.colorMajor = colorMajor || grid.colorMajor;
|
|
grid.colorMinor = colorMinor || grid.colorMinor;
|
|
updateGrid();
|
|
}
|
|
}
|
|
|
|
function setGridColor(opt = {}) {
|
|
grid.colorMajor = valueOr(opt.major || opt.colorMajor, grid.colorMajor);
|
|
grid.colorMinor = valueOr(opt.minor || opt.colorMinor, grid.colorMinor);
|
|
grid.colorX = valueOr(opt.colorX, grid.colorX);
|
|
grid.colorY = valueOr(opt.colorY, grid.colorY);
|
|
updateGrid();
|
|
}
|
|
|
|
function numOrDef(v, dv) {
|
|
return v !== undefined ? v : dv;
|
|
}
|
|
|
|
function modMatch(val, mod) {
|
|
let mv = Math.abs(val) % mod;
|
|
return (mv < 1) || ((mod - mv) < 1);
|
|
}
|
|
|
|
function updateGrid() {
|
|
let { view, unitMinor, unitMajor, colorMajor, colorMinor, colorX, colorY } = grid;
|
|
let oldView = view;
|
|
let axes = grid.axes = new THREE.Group();
|
|
let lines = grid.lines = new THREE.Group();
|
|
|
|
view = grid.view = new THREE.Group();
|
|
view.visible = oldView ? oldView.visible : true;
|
|
view.add(lines);
|
|
view.add(axes);
|
|
|
|
let majors = [],
|
|
minors = [],
|
|
x = platform.scale.x,
|
|
y = isRound ? platform.scale.z : platform.scale.y,
|
|
z = isRound ? platform.scale.y : platform.scale.z,
|
|
zp = -(z / 2) - platformZOff + numOrDef(gridZOff, (z/2-0.1)),
|
|
xh = x / 2,
|
|
yh = y / 2,
|
|
x1 = -xh - origin.x,
|
|
x2 = xh - origin.x,
|
|
y1 = -yh + origin.y,
|
|
y2 = yh + origin.y,
|
|
xo = x1 + xh,
|
|
yo = y1 + yh;
|
|
|
|
ruler.x1 = x1;
|
|
ruler.x2 = x2;
|
|
ruler.y1 = y1;
|
|
ruler.y2 = y2;
|
|
ruler.xo = xh - xo;
|
|
ruler.yo = yh - yo;
|
|
|
|
for (let x=-unitMinor; x>x1; x -= unitMinor) {
|
|
let oh = isRound ? Math.sqrt(1-(x/xh)*(x/xh)) * yh : yh;
|
|
let arr = modMatch(x, unitMajor) ? majors : minors;
|
|
arr.append({x:x-xo, y:-oh, z:zp}).append({x:x-xo, y:oh, z:zp});
|
|
}
|
|
for (let x=unitMinor; x<x2; x += unitMinor) {
|
|
let oh = isRound ? Math.sqrt(1-(x/xh)*(x/xh)) * yh : yh;
|
|
let arr = modMatch(x, unitMajor) ? majors : minors;
|
|
arr.append({x:x-xo, y:-oh, z:zp}).append({x:x-xo, y:oh, z:zp});
|
|
}
|
|
for (let y=-unitMinor; y>y1; y -= unitMinor) {
|
|
let ow = isRound ? Math.sqrt(1-(y/yh)*(y/yh)) * xh : xh;
|
|
let arr = modMatch(y, unitMajor) ? majors : minors;
|
|
arr.append({x:-ow, y:y-yo, z:zp}).append({x:ow, y:y-yo, z:zp});
|
|
}
|
|
for (let y=unitMinor; y<y2; y += unitMinor) {
|
|
let ow = isRound ? Math.sqrt(1-(y/yh)*(y/yh)) * xh : xh;
|
|
let arr = modMatch(y, unitMajor) ? majors : minors;
|
|
arr.append({x:-ow, y:y-yo, z:zp}).append({x:ow, y:y-yo, z:zp});
|
|
}
|
|
lines.add(makeLinesFromPoints(majors, colorMajor));
|
|
lines.add(makeLinesFromPoints(minors, colorMinor));
|
|
axes.add(makeLinesFromPoints([
|
|
{x: -xo, y:y1-yo, z:zp},
|
|
{x: -xo, y:y2-yo, z:zp},
|
|
], colorY));
|
|
axes.add(makeLinesFromPoints([
|
|
{x: x1-xo, y:-yo, z:zp},
|
|
{x: x2-xo, y:-yo, z:zp},
|
|
], colorX));
|
|
|
|
Space.scene.remove(oldView);
|
|
Space.scene.add(grid.view);
|
|
lines.visible = showGridLines;
|
|
requestRefresh();
|
|
}
|
|
|
|
function updateDraws() {
|
|
updateGrid();
|
|
updateRulers();
|
|
requestRefresh();
|
|
}
|
|
|
|
function setOrigin(x, y, z, show) {
|
|
if (grid.origin) {
|
|
let or = origin;
|
|
let unchanged = x === or.x && y === or.y && z === or.z && show === or.show;
|
|
if (!unchanged) {
|
|
Space.scene.remove(grid.origin.group);
|
|
}
|
|
if (unchanged) {
|
|
updateDraws();
|
|
return;
|
|
}
|
|
}
|
|
origin = {x, y, z, show};
|
|
if (!show) {
|
|
return;
|
|
}
|
|
let cmat = new THREE.MeshPhongMaterial({
|
|
color: 0xcceeff,
|
|
specular: 0xcceeff,
|
|
shininess: 5,
|
|
transparent: true,
|
|
opacity: 0.5,
|
|
// side: THREE.DoubleSide
|
|
});
|
|
let rmat = new THREE.MeshPhongMaterial({
|
|
color: 0x88aadd,
|
|
transparent: true,
|
|
opacity: 0.5,
|
|
// side: THREE.DoubleSide
|
|
});
|
|
let PIP = Math.PI/2;
|
|
let pi1, pi2, pi3, pi4;
|
|
let group = new THREE.Group();
|
|
grid.origin = {x, y, z, group};
|
|
group.add(pi1 = new THREE.Mesh(
|
|
new THREE.CircleGeometry(4.6, 50, PIP*0, PIP*1),
|
|
cmat
|
|
));
|
|
pi1.position.x = 0.25;
|
|
pi1.position.y = 0.25;
|
|
group.add(pi2 = new THREE.Mesh(
|
|
new THREE.CircleGeometry(4.6, 50, PIP*1, PIP*1),
|
|
cmat
|
|
));
|
|
pi2.position.x = -0.25;
|
|
pi2.position.y = 0.25;
|
|
group.add(pi3 = new THREE.Mesh(
|
|
new THREE.CircleGeometry(4.6, 50, PIP*2, PIP*1),
|
|
cmat
|
|
));
|
|
pi3.position.x = -0.25;
|
|
pi3.position.y = -0.25;
|
|
group.add(pi4 = new THREE.Mesh(
|
|
new THREE.CircleGeometry(4.6, 50, PIP*3, PIP*1),
|
|
cmat
|
|
));
|
|
pi4.position.x = 0.25;
|
|
pi4.position.y = -0.25;
|
|
let aa, bb, cc;
|
|
group.add(aa = new THREE.Mesh(
|
|
new THREE.RingGeometry(5, 5.5, 50),
|
|
rmat
|
|
));
|
|
group.add(bb = new THREE.Mesh(
|
|
new THREE.PlaneGeometry(0.5, 10),
|
|
rmat
|
|
));
|
|
group.add(cc = new THREE.Mesh(
|
|
new THREE.PlaneGeometry(10, 0.5),
|
|
rmat
|
|
));
|
|
group.rotation.x = -PI2;
|
|
group.position.x = x;
|
|
group.position.y = z;
|
|
group.position.z = y;
|
|
[bb,cc].forEach(m => {
|
|
m.renderOrder = 3;
|
|
});
|
|
Space.scene.add(group);
|
|
const scale = grid.origin.scale = () => {
|
|
const dist = camera.position.distanceTo(group.position);
|
|
const scale = Math.min(dist / 100, 0.5);
|
|
group.scale.set(scale, scale, scale);
|
|
};
|
|
scale();
|
|
updateDraws();
|
|
}
|
|
|
|
function setRound(bool) {
|
|
let current = platform;
|
|
isRound = bool;
|
|
if (bool) {
|
|
platform = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(.5, .5, 1, 60),
|
|
platformMaterial
|
|
);
|
|
platform.rotation.x = 0;
|
|
} else {
|
|
platform = new THREE.Mesh(
|
|
new THREE.BoxGeometry(1, 1, 1),
|
|
platformMaterial
|
|
);
|
|
platform.rotation.x = -PI2;
|
|
}
|
|
|
|
platform.position.y = current.position.y;
|
|
platform.visible = current.visible;
|
|
|
|
SCENE.remove(current);
|
|
SCENE.add(platform);
|
|
THREE.dispose(current);
|
|
}
|
|
|
|
function refresh() {
|
|
refreshRequested = false;
|
|
clearTimeout(refreshTimeout);
|
|
updateLastAction();
|
|
viewControl.update();
|
|
}
|
|
|
|
/** deferred refresh that collapses multiple requests */
|
|
function requestRefresh(timeout) {
|
|
if (refreshRequested === false) {
|
|
refreshRequested = true;
|
|
clearTimeout(refreshTimeout);
|
|
refreshTimeout = setTimeout(refresh, timeout || 10);
|
|
}
|
|
}
|
|
|
|
function onResize() {
|
|
updateLastAction();
|
|
camera.aspect = aspect();
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(width(), height());
|
|
container.style.width = width();
|
|
container.style.height = height();
|
|
requestRefresh();
|
|
}
|
|
|
|
function alignTracking(point, rot, out) {
|
|
if (point && rot) {
|
|
alignedTracking = true;
|
|
trackPlane.position.set(point.x, point.y, point.z);
|
|
trackPlane.rotation.set(rot.x, rot.y, rot.z);
|
|
trackDelta = out;
|
|
} else {
|
|
alignedTracking = false;
|
|
trackPlane.position.set(0, 0, 0);
|
|
trackPlane.rotation.set(PI2, 0, 0);
|
|
}
|
|
}
|
|
|
|
function cca(c) {
|
|
return c.charCodeAt(0);
|
|
}
|
|
|
|
function inputHasFocus() {
|
|
return DOC.activeElement && (DOC.activeElement != DOC.body);
|
|
}
|
|
|
|
function keyHandler(evt) {
|
|
updateLastAction();
|
|
if (!defaultKeys || inputHasFocus()) return false;
|
|
if (evt.metaKey) return false;
|
|
let handled = true;
|
|
switch (evt.charCode) {
|
|
case cca('z'):
|
|
Space.view.reset();
|
|
break;
|
|
case cca('h'):
|
|
Space.view.home();
|
|
break;
|
|
case cca('t'):
|
|
Space.view.top();
|
|
break;
|
|
case cca('f'):
|
|
Space.view.front();
|
|
cycleInd = 0;
|
|
break;
|
|
case cca('b'):
|
|
Space.view.back();
|
|
cycleInd = 2;
|
|
break;
|
|
case cca('>'):
|
|
cycleInd = (++cycleInd % cycle.length);
|
|
cycle[cycleInd]();
|
|
break;
|
|
case cca('<'):
|
|
cycleInd--;
|
|
if (cycleInd < 0) {
|
|
cycleInd += cycle.length;
|
|
}
|
|
cycle[cycleInd]();
|
|
break;
|
|
default:
|
|
handled = false;
|
|
break;
|
|
}
|
|
if (handled) evt.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
/** ******************************************************************
|
|
* ThreeJS Helper Functions
|
|
******************************************************************* */
|
|
|
|
function makeLinesFromPoints(points, color) {
|
|
if (points.length % 2 != 0) {
|
|
throw "invalid line : "+points.length;
|
|
}
|
|
const geo = new THREE.BufferGeometry();
|
|
const vrt = new Float32Array(points.length * 3);
|
|
let vi = 0;
|
|
for (let p of points) {
|
|
vrt[vi++] = p.x;
|
|
vrt[vi++] = p.y;
|
|
vrt[vi++] = p.z;
|
|
}
|
|
geo.setAttribute('position', new THREE.BufferAttribute(vrt, 3));
|
|
return new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color }));
|
|
}
|
|
|
|
function intersect(objects, recurse) {
|
|
// console.log(({int: objects}));
|
|
let ints = raycaster.intersectObjects(objects, recurse);
|
|
// console.trace({ints});
|
|
return ints;
|
|
}
|
|
|
|
/** ******************************************************************
|
|
* Mouse Functions
|
|
******************************************************************* */
|
|
|
|
function onMouseDown(event) {
|
|
updateLastAction();
|
|
if (event.target === renderer.domElement) {
|
|
DOC.activeElement.blur();
|
|
event.preventDefault();
|
|
let selection = null,
|
|
trackTo = alignedTracking ? trackPlane : platform,
|
|
isVis = trackTo.visible;
|
|
if (mouseDownSelect) selection = mouseDownSelect(undefined, event);
|
|
if (selection && selection.length > 0) {
|
|
trackTo.visible = true;
|
|
let int = intersect(selection.slice().append(trackTo), false);
|
|
trackTo.visible = isVis;
|
|
if (int.length > 0) {
|
|
let trackInt, selectInt;
|
|
for (let i=0; i<int.length; i++) {
|
|
if (!trackInt && int[i].object === trackTo) {
|
|
trackInt = int[i];
|
|
} else if (!selectInt && selection.contains(int[i].object)) {
|
|
selectInt = int[i];
|
|
}
|
|
}
|
|
if (trackInt && selectInt) {
|
|
mouseDragPoint = trackInt.point.clone();
|
|
mouseDragStart = mouseDragPoint;
|
|
viewControl.enabled = false;
|
|
}
|
|
if (selectInt) {
|
|
mouseDownSelect(selectInt, event);
|
|
}
|
|
if (mouseDown) mouseDown(event, int);
|
|
}
|
|
} else if (mouseDown) {
|
|
let int = intersect(selection.slice().append(platform), false);
|
|
if (int && int.length) mouseDown(event, int);
|
|
}
|
|
if (platformClick) {
|
|
let vis = platform.visible;
|
|
platform.visible = true;
|
|
let int = intersect([platform], false);
|
|
platform.visible = vis;
|
|
platformClickAt = int && int.length > 0 ? int[0].point : null;
|
|
}
|
|
} else {
|
|
viewControl.enabled = false;
|
|
}
|
|
mouseStart = {
|
|
x: (event.clientX / width()) * 2 - 1,
|
|
y: -(event.clientY / height()) * 2 + 1};
|
|
}
|
|
|
|
function onMouseUp(event) {
|
|
updateLastAction();
|
|
if (!viewControl.enabled) {
|
|
viewControl.enabled = true;
|
|
viewControl.onMouseUp(event);
|
|
}
|
|
let mouseEnd = {
|
|
x: (event.clientX / width()) * 2 - 1,
|
|
y: -(event.clientY / height()) * 2 + 1};
|
|
// only fire on mouse move between mouseStart (down) and up
|
|
if (mouseStart && mouseEnd.x - mouseStart.x + mouseEnd.y - mouseStart.y === 0) {
|
|
event.preventDefault();
|
|
let refresh = false,
|
|
selection = null;
|
|
if (mouseUpSelect) {
|
|
selection = mouseUpSelect();
|
|
}
|
|
if (selection && selection.length > 0) {
|
|
let int = intersect(selection, selectRecurse);
|
|
if (mouseUp) {
|
|
if (int.length) {
|
|
mouseUp(event, int);
|
|
} else {
|
|
mouseUp(event, intersect([platform], selectRecurse));
|
|
}
|
|
if (event.button === 2) {
|
|
return;
|
|
}
|
|
}
|
|
if (int.length > 0) {
|
|
mouseUpSelect(int[0], event);
|
|
refresh = true;
|
|
} else {
|
|
mouseUpSelect(null, event);
|
|
}
|
|
}
|
|
if (!refresh && platformClickAt) {
|
|
platformClick(platformClickAt);
|
|
}
|
|
if (refresh) {
|
|
requestRefresh();
|
|
}
|
|
mouseStart = null;
|
|
} else if (mouseDrag && mouseDragStart) {
|
|
mouseDrag(null,null,true);
|
|
}
|
|
mouseDragPoint = null;
|
|
mouseDragStart = null;
|
|
}
|
|
|
|
function onMouseMove(event) {
|
|
updateLastAction();
|
|
let int, vis;
|
|
|
|
const mv = new THREE.Vector2();
|
|
mv.x = ( event.clientX / window.innerWidth ) * 2 - 1;
|
|
mv.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
|
|
raycaster.setFromCamera( mv, camera );
|
|
|
|
if (viewControl.enabled) {
|
|
event.preventDefault();
|
|
let selection = mouseHover ? mouseHover() : null;
|
|
if (selection && selection.length > 0) {
|
|
int = intersect(selection, selectRecurse);
|
|
if (int.length > 0) mouseHover(int[0], event, int);
|
|
}
|
|
if ((!int || int.length == 0) && platformHover) {
|
|
vis = platform.visible;
|
|
platform.visible = true;
|
|
int = intersect([platform], false);
|
|
platform.visible = vis;
|
|
if (int && int.length > 0) platformHover(int[0].point);
|
|
}
|
|
} else if (mouseDragPoint && mouseDrag && mouseDrag()) {
|
|
event.preventDefault();
|
|
let trackTo = alignedTracking ? trackPlane : platform;
|
|
let vis = trackTo.visible;
|
|
trackTo.visible = true;
|
|
int = intersect([trackTo], false);
|
|
trackTo.visible = vis;
|
|
if (int.length > 0 && int[0].object === trackTo) {
|
|
let delta = mouseDragPoint.clone().sub(int[0].point);
|
|
let offset = mouseDragStart.clone().sub(int[0].point);
|
|
mouseDragPoint = int[0].point;
|
|
mouseDrag({
|
|
x: -delta.x,
|
|
y: delta.z,
|
|
event
|
|
}, offset.multiplyVectors(offset, trackDelta));
|
|
requestRefresh();
|
|
}
|
|
}
|
|
mouse = {
|
|
x: (event.clientX / width()) * 2 - 1,
|
|
y: -(event.clientY / height()) * 2 + 1};
|
|
}
|
|
|
|
/** ******************************************************************
|
|
* Space Object
|
|
******************************************************************* */
|
|
|
|
function updateFocus() {
|
|
if (focalPoint) {
|
|
Space.scene.remove(focalPoint);
|
|
}
|
|
if (showFocus) {
|
|
let mesh = focalPoint = new THREE.Mesh(
|
|
new THREE.SphereGeometry(1, 16, 16),
|
|
new THREE.MeshPhongMaterial({
|
|
side: THREE.DoubleSide,
|
|
specular: 0x202020,
|
|
color: 0xff0000,
|
|
shininess: 125
|
|
})
|
|
);
|
|
Space.scene.add(mesh);
|
|
mesh.position.copy(viewControl.target);
|
|
}
|
|
}
|
|
|
|
function setSky(opt = {}) {
|
|
let { grid, color, gridColor } = opt;
|
|
if (grid) Space.sky.showGrid(grid);
|
|
if (color !== undefined) Space.sky.setColor(color);
|
|
if (gridColor !== undefined) Space.sky.setGridColor(gridColor);
|
|
if (skyAmbient && opt.ambient) {
|
|
let { color, intensity } = opt.ambient;
|
|
if (color) skyAmbient.color.set(color);
|
|
if (intensity) skyAmbient.intensity = intensity;
|
|
}
|
|
}
|
|
|
|
function setPlatform(opt = {}) {
|
|
let platform = Space.platform;
|
|
let { color, round, size, grid, opacity } = opt;
|
|
let { visible, volume, zOffset, origin, light } = opt;
|
|
if (light) {
|
|
lightInfo.intensity = light;
|
|
}
|
|
if (color) {
|
|
platform.setColor(color);
|
|
}
|
|
if (round !== undefined) {
|
|
platform.setRound(round);
|
|
}
|
|
if (size) {
|
|
let { width = 300, depth = 300, height = 2.5, maxz = 300 } = size;
|
|
platform.setSize(width, depth, height, maxz);
|
|
}
|
|
if (grid) {
|
|
let { zOffset } = grid;
|
|
let { major = 25, minor = 5 } = grid;
|
|
let { colorX, colorY, colorMajor, colorMinor } = grid;
|
|
platform.setGrid(major, minor);
|
|
platform.setGridColor({ colorX, colorY, colorMajor, colorMinor });
|
|
if (zOffset !== undefined) platform.setGridZOff(zOffset);
|
|
}
|
|
if (origin) {
|
|
let { x, y, z, show } = origin;
|
|
platform.setOrigin(x || 0, y || 0, z || 0, show);
|
|
}
|
|
if (opacity !== undefined) {
|
|
platform.opacity(opacity);
|
|
}
|
|
if (volume !== undefined) {
|
|
platform.showVolume(volume);
|
|
}
|
|
if (zOffset !== undefined) {
|
|
platform.setZOff(zOffset);
|
|
}
|
|
if (visible !== undefined) {
|
|
platform.setVisible(visible);
|
|
}
|
|
}
|
|
|
|
let Space = exports({
|
|
refresh: refresh,
|
|
update: requestRefresh,
|
|
|
|
setAntiAlias(b) { antiAlias = b ? true : false },
|
|
alignTracking: alignTracking,
|
|
raycast: intersect,
|
|
|
|
event: {
|
|
addHandlers: addEventHandlers,
|
|
onEnterKey: onEnterKey,
|
|
onResize: onResize
|
|
},
|
|
|
|
sky: {
|
|
set: setSky,
|
|
|
|
showGrid: (b) => {
|
|
showSkyGrid = b;
|
|
},
|
|
|
|
setColor: (c) => {
|
|
SCENE.background = new THREE.Color(c);
|
|
},
|
|
|
|
setGridColor: (c) => {
|
|
skyGridColor = c;
|
|
if (skyGridMaterial) skyGridMaterial.color = new THREE.Color(c);
|
|
}
|
|
},
|
|
|
|
scene: {
|
|
add: function (o) {
|
|
o.rotation.x = WORLD.rotation.x;
|
|
return SCENE.add(o);
|
|
},
|
|
|
|
remove: function (o) {
|
|
THREE.dispose(o);
|
|
return SCENE.remove(o);
|
|
},
|
|
|
|
active: updateLastAction,
|
|
|
|
setFog: function(mult, color) {
|
|
if (mult) {
|
|
SCENE.fog = new THREE.Fog(color, 100, 1000);
|
|
SCENE.fog.mult = mult > 0 ? mult : 3;
|
|
} else {
|
|
SCENE.fog = undefined;
|
|
}
|
|
Space.scene.updateFog();
|
|
},
|
|
|
|
updateFog: function() {
|
|
const { fog } = SCENE;
|
|
if (fog) {
|
|
const dist = camera.position.distanceTo(viewControl.target);
|
|
fog.near = dist;
|
|
fog.far = dist * fog.mult;
|
|
}
|
|
// todo: option to clip for close views
|
|
// camera.near = dist / 2;
|
|
// camera.updateProjectionMatrix();
|
|
},
|
|
|
|
lightInfo: () => {
|
|
setTimeout(updateLights, 0);
|
|
return lightInfo;
|
|
}
|
|
},
|
|
|
|
world: {
|
|
add: function(o) {
|
|
return WORLD.add(o);
|
|
},
|
|
|
|
remove: function(o) {
|
|
THREE.dispose(o);
|
|
return WORLD.remove(o);
|
|
},
|
|
|
|
newGroup: function() {
|
|
return WORLD.newGroup();
|
|
}
|
|
},
|
|
|
|
platform: {
|
|
set: setPlatform,
|
|
update: updateDraws,
|
|
setSize: setPlatformSizeUpdateGrid,
|
|
setColor: setPlatformColor,
|
|
setOrigin,
|
|
setRulers,
|
|
setGrid,
|
|
setGridColor,
|
|
setFont,
|
|
setRound,
|
|
showAxes,
|
|
showVolume,
|
|
showGridBelow: (b) => { hideGridBelow = !b },
|
|
showGrid: (b) => { showGrid = grid.view.visible = b },
|
|
showGrid2: (b) => { showGridLines = grid.lines.visible = b },
|
|
setMaxZ: (z) => { panY = z / 2 },
|
|
setCenter: (x,y,z) => { panX = x; panY = z, panZ = y },
|
|
setHidden: (b) => { showPlatform = !b; platform.visible = !b },
|
|
setVisible: (b) => { showPlatform = b; platform.visible = b },
|
|
setHiding: (b) => { hidePlatformBelow = b },
|
|
setZOff: (z) => { platformZOff = z; updatePlatformPosition() },
|
|
setGridZOff:(z) => { gridZOff = z; updatePlatformPosition() },
|
|
setBelt: (b) => { platformBelt = b },
|
|
isHidden: () => { return !showPlatform },
|
|
isVisible: () => { return platform.visible },
|
|
isGridVisible() { return grid.view.visible },
|
|
opacity: (o) => { platform.material.opacity = o; Space.platform.setVisible(o > 0) },
|
|
onMove: (f,t) => { platformOnMove = f, platformOnMoveTime = t || platformOnMoveTime },
|
|
onHover: (f) => { platformHover = f },
|
|
onClick: (f) => { platformClick = f},
|
|
size: () => { return platform.scale },
|
|
get world() { throw "platform.world deprecated" }
|
|
},
|
|
|
|
preset: {
|
|
top: {left: home, up: 0, panX, panY, panZ},
|
|
back: {left: PI, up: PI2, panX, panY, panZ},
|
|
home: {left: home, up, panX, panY, panZ},
|
|
front: {left: 0, up: PI2, panX, panY, panZ},
|
|
right: {left: PI2, up: PI2, panX, panY, panZ},
|
|
left: {left: -PI2, up: PI2, panX, panY, panZ},
|
|
},
|
|
|
|
view: {
|
|
top: (then) => { tweenCam({left: home, up: 0, panX, panY, panZ, then}) },
|
|
back: (then) => { tweenCam({left: PI, up: PI2, panX, panY, panZ, then}) },
|
|
home: (then) => { tweenCam({left: home, up, panX, panY, panZ, then}) },
|
|
front: (then) => { tweenCam({left: 0, up: PI2, panX, panY, panZ, then}) },
|
|
right: (then) => { tweenCam({left: PI2, up: PI2, panX, panY, panZ, then}) },
|
|
left: (then) => { tweenCam({left: -PI2, up: PI2, panX, panY, panZ, then}) },
|
|
reset: () => { viewControl.reset(); requestRefresh() },
|
|
load: (cam) => { viewControl.setPosition(cam); requestRefresh() },
|
|
save: () => { return viewControl.getPosition(true) },
|
|
panTo: (x,y,z,l,u) => { tweenCamPan(x,y,z,l,u) },
|
|
setZoom: (r,v) => { viewControl.setZoom(r,v) },
|
|
setCtrl: (name) => {
|
|
if (name === 'onshape') {
|
|
viewControl.setMouse(viewControl.mouseOnshape);
|
|
} else {
|
|
viewControl.setMouse(viewControl.mouseDefault);
|
|
}
|
|
},
|
|
getFPS () { return fps },
|
|
getRMS() { return renderTime },
|
|
getFocus() { return viewControl.getTarget() },
|
|
setFocus(v) {
|
|
viewControl.setTarget(v);
|
|
updateFocus();
|
|
refresh()
|
|
},
|
|
showFocus(ms) {
|
|
showFocus = ms;
|
|
updateFocus();
|
|
},
|
|
setHome(r,u) {
|
|
home = r || 0;
|
|
up = u || PI4;
|
|
},
|
|
spin(then, count) {
|
|
Space.view.front(() => {
|
|
Space.view.right(() => {
|
|
Space.view.back(() => {
|
|
Space.view.left(() => {
|
|
if (--count > 0) {
|
|
Space.view.spin(then, count);
|
|
} else {
|
|
Space.view.front(then);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
},
|
|
get ctrl() {
|
|
return viewControl;
|
|
}
|
|
},
|
|
|
|
mouse: {
|
|
up: (f) => { mouseUp = f },
|
|
down: (f) => { mouseDown = f },
|
|
downSelect: (f) => { mouseDownSelect = f },
|
|
upSelect: (f) => { mouseUpSelect = f },
|
|
onDrag: (f) => { mouseDrag = f },
|
|
onHover: (f) => { mouseHover = f }
|
|
},
|
|
|
|
tween: {
|
|
setTime: (t) => { tweenTime = t || 500 },
|
|
setDelay: (d) => { tweenDelay = d || 20 }
|
|
},
|
|
|
|
useDefaultKeys (b) { defaultKeys = b },
|
|
selectRecurse (b) { selectRecurse = b },
|
|
renderInfo () { return renderer.info },
|
|
objects () { return WORLD.children },
|
|
|
|
screenshot(format, options) {
|
|
return renderer.domElement.toDataURL(format || "image/png", options);
|
|
},
|
|
|
|
screenshot2(param = {}) {
|
|
let oco = renderer.domElement;
|
|
let oWidth = oco.offsetWidth;
|
|
let oHeight = oco.offsetHeight;
|
|
let oRatio = oHeight / oWidth;
|
|
let width = param.width || 240;
|
|
let ncv = document.createElement('canvas');
|
|
ncv.width = width;
|
|
ncv.height = width * oRatio;
|
|
let nco = ncv.getContext('2d');
|
|
nco.drawImage(oco, 0, 0, ncv.width, ncv.height);
|
|
return {
|
|
url: ncv.toDataURL(param.format || "image/png", param.options),
|
|
width: ncv.width,
|
|
height: ncv.height
|
|
};
|
|
},
|
|
|
|
internals: () => {
|
|
return { renderer, camera, platform };
|
|
},
|
|
|
|
init: (domelement, slider, ortho) => {
|
|
container = domelement;
|
|
|
|
WORLD.rotation.x = -PI2;
|
|
SCENE.add(WORLD);
|
|
|
|
domelement.style.width = width();
|
|
domelement.style.height = height();
|
|
|
|
renderer = new WebGLRenderer({
|
|
antialias: antiAlias,
|
|
preserveDrawingBuffer: true,
|
|
logarithmicDepthBuffer: true
|
|
});
|
|
|
|
// THREE.ColorManagement.enabled = false;
|
|
// renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
|
|
renderer.localClippingEnabled = true;
|
|
camera = ortho ?
|
|
new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000) :
|
|
new THREE.PerspectiveCamera(perspective, aspect(), 0.1, 100000);
|
|
|
|
camera.position.set(0, 200, 340);
|
|
renderer.setSize(width(), height());
|
|
domelement.appendChild(renderer.domElement);
|
|
|
|
raycaster = new THREE.Raycaster();
|
|
|
|
viewControl = new moto.Orbit(camera, domelement, (position, moved) => {
|
|
if (platform) {
|
|
platform.visible = hidePlatformBelow ?
|
|
initialized && position.y >= 0 && showPlatform : showPlatform;
|
|
volume.visible = showVolume && platform.visible;
|
|
}
|
|
if (grid.view) {
|
|
grid.view.visible = hideGridBelow ? platform.visible : showGrid;
|
|
}
|
|
if (cameraLight) {
|
|
cameraLight.position.copy(camera.position);
|
|
}
|
|
if (moved && platformOnMove) {
|
|
clearTimeout(platformMoveTimer);
|
|
platformMoveTimer = setTimeout(platformOnMove, 500);
|
|
Space.scene.updateFog();
|
|
}
|
|
updateLastAction();
|
|
updateFocus();
|
|
}, (val) => {
|
|
if (camera && grid?.origin?.scale) {
|
|
grid.origin.scale();
|
|
}
|
|
if (camera && viewControl) {
|
|
// increase intersect line precision on zoom
|
|
const dist = camera.position.distanceTo(viewControl.target);
|
|
raycaster.params.Line.threshold = Math.min(1, dist / 100);
|
|
}
|
|
moto.broker.publish("space.view.zoom", camera);
|
|
updateLastAction();
|
|
if (slider) slider(val);
|
|
});
|
|
|
|
viewControl.noKeys = true;
|
|
viewControl.maxDistance = 1000;
|
|
|
|
SCENE.add(skyAmbient = new THREE.AmbientLight(0x707070));
|
|
|
|
platform = new THREE.Mesh(
|
|
new THREE.BoxGeometry(1, 1, 1),
|
|
platformMaterial
|
|
);
|
|
|
|
platform.position.y = platformZOff;
|
|
platform.rotation.x = -PI2;
|
|
platform.visible = showPlatform;
|
|
|
|
trackPlane = new THREE.Mesh(
|
|
new THREE.PlaneGeometry(100000, 100000, 1, 1),
|
|
new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0, transparent: true } )
|
|
);
|
|
trackPlane.visible = false;
|
|
trackPlane.rotation.x = PI2;
|
|
|
|
let skygrid = new THREE.Mesh(
|
|
new THREE.BoxGeometry(10000, 10000, 10000, 10, 10, 10),
|
|
skyGridMaterial =
|
|
new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide })
|
|
);
|
|
|
|
SCENE.add(platform);
|
|
SCENE.add(trackPlane);
|
|
|
|
if (showSkyGrid) {
|
|
skygrid.material.wireframe = true;
|
|
SCENE.add(skygrid);
|
|
}
|
|
|
|
addEventHandlers(WIN, [
|
|
'resize', onResize,
|
|
'mousemove', onMouseMove,
|
|
'mousedown', onMouseDown,
|
|
'mouseup', onMouseUp,
|
|
'keypress', keyHandler,
|
|
'touchstart', updateLastAction,
|
|
'touchmove', updateLastAction,
|
|
'touchend', updateLastAction
|
|
]);
|
|
|
|
let animates = 0;
|
|
let rateStart = Date.now();
|
|
let renderStart;
|
|
let renders = [];
|
|
|
|
function animate() {
|
|
animates++;
|
|
const now = Date.now();
|
|
if (now - rateStart > 1000) {
|
|
// compute stats roughly every second
|
|
const delta = now - rateStart;
|
|
fps = 1000 * animates / delta;
|
|
animates = 0;
|
|
rateStart = now;
|
|
// look for slowest rendered frame
|
|
renderTime = Math.max(0, ...renders);
|
|
renders.length = 0;
|
|
}
|
|
requestAnimationFrame(animate);
|
|
if (docVisible && !freeze && Date.now() - lastAction < 1500) {
|
|
renderStart = Date.now();
|
|
renderer.render(SCENE, camera);
|
|
// track frame render times
|
|
renders.push(Date.now() - renderStart);
|
|
} else {
|
|
fps = 0;
|
|
}
|
|
}
|
|
|
|
animate();
|
|
|
|
const ctx = renderer.getContext();
|
|
|
|
Space.info = {
|
|
ver: ctx.getParameter(ctx.VERSION),
|
|
ven: ctx.getParameter(ctx.VENDOR),
|
|
glr: ctx.getParameter(ctx.RENDERER),
|
|
pla: nav.platform,
|
|
mob: nav.maxTouchPoints > 1 || (/android/i.test(nav.userAgent))
|
|
};
|
|
|
|
initialized = true;
|
|
}
|
|
});
|
|
|
|
let cycle = [
|
|
Space.view.front,
|
|
Space.view.right,
|
|
Space.view.back,
|
|
Space.view.left,
|
|
];
|
|
|
|
let cycleInd = 0;
|
|
|
|
});
|