You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
787 lines
15 KiB
787 lines
15 KiB
import * as THREE from 'three';
|
|
|
|
import { TransformControls } from 'three/addons/controls/TransformControls.js';
|
|
|
|
import { UIPanel } from './libs/ui.js';
|
|
|
|
import { EditorControls } from './EditorControls.js';
|
|
|
|
import { ViewportCamera } from './Viewport.Camera.js';
|
|
import { ViewportInfo } from './Viewport.Info.js';
|
|
import { ViewHelper } from './Viewport.ViewHelper.js';
|
|
import { VR } from './Viewport.VR.js';
|
|
|
|
import { SetPositionCommand } from './commands/SetPositionCommand.js';
|
|
import { SetRotationCommand } from './commands/SetRotationCommand.js';
|
|
import { SetScaleCommand } from './commands/SetScaleCommand.js';
|
|
|
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
|
|
import { OrbitControls } from '/examples/jsm/controls/OrbitControls.js';
|
|
function Viewport(editor) {
|
|
|
|
const signals = editor.signals;
|
|
|
|
const container = new UIPanel();
|
|
container.setId('viewport');
|
|
container.setPosition('absolute');
|
|
|
|
container.add(new ViewportCamera(editor));
|
|
container.add(new ViewportInfo(editor));
|
|
|
|
//
|
|
|
|
let renderer = null;
|
|
let css2DRenderer = null;
|
|
let pmremGenerator = null;
|
|
|
|
const camera = editor.camera;
|
|
const scene = editor.scene;
|
|
const sceneHelpers = editor.sceneHelpers;
|
|
let showSceneHelpers = true;
|
|
|
|
// helpers
|
|
|
|
const grid = new THREE.Group();
|
|
|
|
const grid1 = new THREE.GridHelper(30, 30, 0x888888);
|
|
grid1.material.color.setHex(0x888888);
|
|
grid1.material.vertexColors = false;
|
|
grid.add(grid1);
|
|
|
|
const grid2 = new THREE.GridHelper(30, 6, 0x222222);
|
|
grid2.material.color.setHex(0x222222);
|
|
grid2.material.depthFunc = THREE.AlwaysDepth;
|
|
grid2.material.vertexColors = false;
|
|
grid.add(grid2);
|
|
|
|
const viewHelper = new ViewHelper(camera, container);
|
|
const vr = new VR(editor);
|
|
|
|
//
|
|
|
|
const box = new THREE.Box3();
|
|
|
|
const selectionBox = new THREE.Box3Helper(box);
|
|
selectionBox.material.depthTest = false;
|
|
selectionBox.material.transparent = true;
|
|
selectionBox.visible = false;
|
|
sceneHelpers.add(selectionBox);
|
|
|
|
let objectPositionOnDown = null;
|
|
let objectRotationOnDown = null;
|
|
let objectScaleOnDown = null;
|
|
|
|
const transformControls = new TransformControls(camera, container.dom);
|
|
transformControls.addEventListener('change', function () {
|
|
|
|
const object = transformControls.object;
|
|
|
|
if (object !== undefined) {
|
|
|
|
box.setFromObject(object, true);
|
|
|
|
const helper = editor.helpers[object.id];
|
|
|
|
if (helper !== undefined && helper.isSkeletonHelper !== true) {
|
|
|
|
helper.update();
|
|
|
|
}
|
|
|
|
signals.refreshSidebarObject3D.dispatch(object);
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
transformControls.addEventListener('mouseDown', function () {
|
|
|
|
const object = transformControls.object;
|
|
|
|
objectPositionOnDown = object.position.clone();
|
|
objectRotationOnDown = object.rotation.clone();
|
|
objectScaleOnDown = object.scale.clone();
|
|
|
|
controls.enabled = false;
|
|
|
|
});
|
|
transformControls.addEventListener('mouseUp', function () {
|
|
|
|
const object = transformControls.object;
|
|
|
|
if (object !== undefined) {
|
|
|
|
switch (transformControls.getMode()) {
|
|
|
|
case 'translate':
|
|
|
|
if (!objectPositionOnDown.equals(object.position)) {
|
|
|
|
editor.execute(new SetPositionCommand(editor, object, object.position, objectPositionOnDown));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'rotate':
|
|
|
|
if (!objectRotationOnDown.equals(object.rotation)) {
|
|
|
|
editor.execute(new SetRotationCommand(editor, object, object.rotation, objectRotationOnDown));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'scale':
|
|
|
|
if (!objectScaleOnDown.equals(object.scale)) {
|
|
|
|
editor.execute(new SetScaleCommand(editor, object, object.scale, objectScaleOnDown));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
controls.enabled = true;
|
|
|
|
});
|
|
|
|
sceneHelpers.add(transformControls);
|
|
|
|
// object picking
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
const mouse = new THREE.Vector2();
|
|
|
|
// events
|
|
|
|
function updateAspectRatio() {
|
|
|
|
camera.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
|
|
camera.updateProjectionMatrix();
|
|
|
|
}
|
|
|
|
function getIntersects(point) {
|
|
|
|
mouse.set((point.x * 2) - 1, - (point.y * 2) + 1);
|
|
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
|
const objects = [];
|
|
|
|
scene.traverseVisible(function (child) {
|
|
|
|
objects.push(child);
|
|
|
|
});
|
|
|
|
sceneHelpers.traverseVisible(function (child) {
|
|
|
|
if (child.name === 'picker') objects.push(child);
|
|
|
|
});
|
|
|
|
return raycaster.intersectObjects(objects, false);
|
|
|
|
}
|
|
|
|
const onDownPosition = new THREE.Vector2();
|
|
const onUpPosition = new THREE.Vector2();
|
|
const onDoubleClickPosition = new THREE.Vector2();
|
|
|
|
function getMousePosition(dom, x, y) {
|
|
|
|
const rect = dom.getBoundingClientRect();
|
|
return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
|
|
|
|
}
|
|
|
|
function handleClick() {
|
|
|
|
if (onDownPosition.distanceTo(onUpPosition) === 0) {
|
|
|
|
const intersects = getIntersects(onUpPosition);
|
|
signals.intersectionsDetected.dispatch(intersects);
|
|
|
|
render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function onMouseDown(event) {
|
|
|
|
// event.preventDefault();
|
|
|
|
const array = getMousePosition(container.dom, event.clientX, event.clientY);
|
|
onDownPosition.fromArray(array);
|
|
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
|
|
}
|
|
|
|
function onMouseUp(event) {
|
|
|
|
const array = getMousePosition(container.dom, event.clientX, event.clientY);
|
|
onUpPosition.fromArray(array);
|
|
|
|
handleClick();
|
|
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
|
|
}
|
|
|
|
function onTouchStart(event) {
|
|
|
|
const touch = event.changedTouches[0];
|
|
|
|
const array = getMousePosition(container.dom, touch.clientX, touch.clientY);
|
|
onDownPosition.fromArray(array);
|
|
|
|
document.addEventListener('touchend', onTouchEnd);
|
|
|
|
}
|
|
|
|
function onTouchEnd(event) {
|
|
|
|
const touch = event.changedTouches[0];
|
|
|
|
const array = getMousePosition(container.dom, touch.clientX, touch.clientY);
|
|
onUpPosition.fromArray(array);
|
|
|
|
handleClick();
|
|
|
|
document.removeEventListener('touchend', onTouchEnd);
|
|
|
|
}
|
|
|
|
function onDoubleClick(event) {
|
|
|
|
const array = getMousePosition(container.dom, event.clientX, event.clientY);
|
|
onDoubleClickPosition.fromArray(array);
|
|
|
|
const intersects = getIntersects(onDoubleClickPosition);
|
|
|
|
if (intersects.length > 0) {
|
|
|
|
const intersect = intersects[0];
|
|
|
|
signals.objectFocused.dispatch(intersect.object);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
container.dom.addEventListener('mousedown', onMouseDown);
|
|
container.dom.addEventListener('touchstart', onTouchStart);
|
|
container.dom.addEventListener('dblclick', onDoubleClick);
|
|
|
|
// controls need to be added *after* main logic,
|
|
// otherwise controls.enabled doesn't work.
|
|
|
|
const controls = new EditorControls(camera, container.dom);
|
|
controls.addEventListener('change', function () {
|
|
|
|
signals.cameraChanged.dispatch(camera);
|
|
signals.refreshSidebarObject3D.dispatch(camera);
|
|
|
|
});
|
|
viewHelper.controls = controls;
|
|
|
|
// signals
|
|
|
|
signals.editorCleared.add(function () {
|
|
|
|
controls.center.set(0, 0, 0);
|
|
render();
|
|
|
|
});
|
|
|
|
signals.transformModeChanged.add(function (mode) {
|
|
|
|
transformControls.setMode(mode);
|
|
|
|
});
|
|
|
|
signals.snapChanged.add(function (dist) {
|
|
|
|
transformControls.setTranslationSnap(dist);
|
|
|
|
});
|
|
|
|
signals.spaceChanged.add(function (space) {
|
|
|
|
transformControls.setSpace(space);
|
|
|
|
});
|
|
|
|
signals.rendererUpdated.add(function () {
|
|
|
|
scene.traverse(function (child) {
|
|
|
|
if (child.material !== undefined) {
|
|
|
|
child.material.needsUpdate = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.rendererCreated.add(function (newRenderer) {
|
|
|
|
if (renderer !== null) {
|
|
|
|
renderer.setAnimationLoop(null);
|
|
renderer.dispose();
|
|
pmremGenerator.dispose();
|
|
|
|
container.dom.removeChild(renderer.domElement);
|
|
|
|
}
|
|
|
|
renderer = newRenderer;
|
|
|
|
renderer.setAnimationLoop(animate);
|
|
renderer.setClearColor(0xaaaaaa);
|
|
|
|
if (window.matchMedia) {
|
|
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
mediaQuery.addEventListener('change', function (event) {
|
|
|
|
renderer.setClearColor(event.matches ? 0x333333 : 0xaaaaaa);
|
|
updateGridColors(grid1, grid2, event.matches ? [0x222222, 0x888888] : [0x888888, 0x282828]);
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
renderer.setClearColor(mediaQuery.matches ? 0x333333 : 0xaaaaaa);
|
|
updateGridColors(grid1, grid2, mediaQuery.matches ? [0x222222, 0x888888] : [0x888888, 0x282828]);
|
|
|
|
}
|
|
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(container.dom.offsetWidth, container.dom.offsetHeight);
|
|
|
|
pmremGenerator = new THREE.PMREMGenerator(renderer);
|
|
pmremGenerator.compileEquirectangularShader();
|
|
|
|
container.dom.appendChild(renderer.domElement);
|
|
|
|
|
|
|
|
|
|
|
|
render();
|
|
|
|
});
|
|
signals.rendererCSSCreated.add(function (newRenderer) {
|
|
|
|
if (css2DRenderer !== null) {
|
|
|
|
css2DRenderer.setAnimationLoop(null);
|
|
css2DRenderer.dispose();
|
|
|
|
|
|
container.dom.removeChild(css2DRenderer.domElement);
|
|
|
|
}
|
|
css2DRenderer = newRenderer;
|
|
container.dom.appendChild(css2DRenderer.domElement);
|
|
|
|
});
|
|
|
|
signals.sceneGraphChanged.add(function () {
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.cameraChanged.add(function () {
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.objectSelected.add(function (object) {
|
|
|
|
selectionBox.visible = false;
|
|
transformControls.detach();
|
|
|
|
if (object !== null && object !== scene && object !== camera) {
|
|
|
|
box.setFromObject(object, true);
|
|
|
|
if (box.isEmpty() === false) {
|
|
|
|
selectionBox.visible = true;
|
|
|
|
}
|
|
|
|
transformControls.attach(object);
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.objectFocused.add(function (object) {
|
|
|
|
controls.focus(object);
|
|
|
|
});
|
|
|
|
signals.geometryChanged.add(function (object) {
|
|
|
|
if (object !== undefined) {
|
|
|
|
box.setFromObject(object, true);
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.objectChanged.add(function (object) {
|
|
|
|
if (editor.selected === object) {
|
|
|
|
box.setFromObject(object, true);
|
|
|
|
}
|
|
|
|
if (object.isPerspectiveCamera) {
|
|
|
|
object.updateProjectionMatrix();
|
|
|
|
}
|
|
|
|
const helper = editor.helpers[object.id];
|
|
|
|
if (helper !== undefined && helper.isSkeletonHelper !== true) {
|
|
|
|
helper.update();
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.objectRemoved.add(function (object) {
|
|
|
|
controls.enabled = true; // see #14180
|
|
if (object === transformControls.object) {
|
|
|
|
transformControls.detach();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
signals.materialChanged.add(function () {
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
// background
|
|
|
|
signals.sceneBackgroundChanged.add(function (backgroundType, backgroundColor, backgroundTexture, backgroundEquirectangularTexture, backgroundBlurriness) {
|
|
|
|
switch (backgroundType) {
|
|
|
|
case 'None':
|
|
|
|
scene.background = null;
|
|
|
|
break;
|
|
|
|
case 'Color':
|
|
|
|
scene.background = new THREE.Color(backgroundColor);
|
|
|
|
break;
|
|
|
|
case 'Texture':
|
|
|
|
if (backgroundTexture) {
|
|
|
|
scene.background = backgroundTexture;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'Equirectangular':
|
|
|
|
if (backgroundEquirectangularTexture) {
|
|
|
|
backgroundEquirectangularTexture.mapping = THREE.EquirectangularReflectionMapping;
|
|
scene.background = backgroundEquirectangularTexture;
|
|
scene.backgroundBlurriness = backgroundBlurriness;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
// environment
|
|
|
|
signals.sceneEnvironmentChanged.add(function (environmentType, environmentEquirectangularTexture) {
|
|
|
|
switch (environmentType) {
|
|
|
|
case 'None':
|
|
|
|
scene.environment = null;
|
|
|
|
break;
|
|
|
|
case 'Equirectangular':
|
|
|
|
scene.environment = null;
|
|
|
|
if (environmentEquirectangularTexture) {
|
|
|
|
environmentEquirectangularTexture.mapping = THREE.EquirectangularReflectionMapping;
|
|
scene.environment = environmentEquirectangularTexture;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'ModelViewer':
|
|
|
|
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
// fog
|
|
|
|
signals.sceneFogChanged.add(function (fogType, fogColor, fogNear, fogFar, fogDensity) {
|
|
|
|
switch (fogType) {
|
|
|
|
case 'None':
|
|
scene.fog = null;
|
|
break;
|
|
case 'Fog':
|
|
scene.fog = new THREE.Fog(fogColor, fogNear, fogFar);
|
|
break;
|
|
case 'FogExp2':
|
|
scene.fog = new THREE.FogExp2(fogColor, fogDensity);
|
|
break;
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.sceneFogSettingsChanged.add(function (fogType, fogColor, fogNear, fogFar, fogDensity) {
|
|
|
|
switch (fogType) {
|
|
|
|
case 'Fog':
|
|
scene.fog.color.setHex(fogColor);
|
|
scene.fog.near = fogNear;
|
|
scene.fog.far = fogFar;
|
|
break;
|
|
case 'FogExp2':
|
|
scene.fog.color.setHex(fogColor);
|
|
scene.fog.density = fogDensity;
|
|
break;
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.viewportCameraChanged.add(function () {
|
|
|
|
const viewportCamera = editor.viewportCamera;
|
|
|
|
if (viewportCamera.isPerspectiveCamera) {
|
|
|
|
viewportCamera.aspect = editor.camera.aspect;
|
|
viewportCamera.projectionMatrix.copy(editor.camera.projectionMatrix);
|
|
|
|
} else if (viewportCamera.isOrthographicCamera) {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
// disable EditorControls when setting a user camera
|
|
|
|
controls.enabled = (viewportCamera === editor.camera);
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.exitedVR.add(render);
|
|
|
|
//
|
|
|
|
signals.windowResize.add(function () {
|
|
|
|
updateAspectRatio();
|
|
|
|
renderer.setSize(container.dom.offsetWidth, container.dom.offsetHeight);
|
|
if (css2DRenderer != null) {
|
|
|
|
css2DRenderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
}
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.showGridChanged.add(function (showGrid) {
|
|
|
|
grid.visible = showGrid;
|
|
render();
|
|
|
|
});
|
|
|
|
signals.showHelpersChanged.add(function (showHelpers) {
|
|
|
|
showSceneHelpers = showHelpers;
|
|
transformControls.enabled = showHelpers;
|
|
|
|
render();
|
|
|
|
});
|
|
signals.render.add( function(){
|
|
|
|
render();
|
|
|
|
});
|
|
|
|
signals.cameraResetted.add(updateAspectRatio);
|
|
|
|
// animations
|
|
|
|
let prevActionsInUse = 0;
|
|
|
|
const clock = new THREE.Clock(); // only used for animations
|
|
|
|
function animate() {
|
|
|
|
const mixer = editor.mixer;
|
|
const delta = clock.getDelta();
|
|
|
|
let needsUpdate = false;
|
|
|
|
// Animations
|
|
|
|
const actions = mixer.stats.actions;
|
|
|
|
if (actions.inUse > 0 || prevActionsInUse > 0) {
|
|
|
|
prevActionsInUse = actions.inUse;
|
|
|
|
mixer.update(delta);
|
|
needsUpdate = true;
|
|
|
|
}
|
|
|
|
// View Helper
|
|
|
|
if (viewHelper.animating === true) {
|
|
|
|
viewHelper.update(delta);
|
|
needsUpdate = true;
|
|
|
|
}
|
|
|
|
if (vr.currentSession !== null) {
|
|
|
|
needsUpdate = true;
|
|
|
|
}
|
|
|
|
if (needsUpdate === true) render();
|
|
|
|
}
|
|
|
|
//
|
|
|
|
let startTime = 0;
|
|
let endTime = 0;
|
|
|
|
function render() {
|
|
|
|
startTime = performance.now();
|
|
|
|
// Adding/removing grid to scene so materials with depthWrite false
|
|
// don't render under the grid.
|
|
|
|
scene.add(grid);
|
|
renderer.setViewport(0, 0, container.dom.offsetWidth, container.dom.offsetHeight);
|
|
renderer.render(scene, editor.viewportCamera);
|
|
|
|
if (css2DRenderer != null) {
|
|
console.log("css2DRenderer render")
|
|
css2DRenderer.render(scene, editor.viewportCamera);
|
|
}
|
|
scene.remove(grid);
|
|
|
|
if (camera === editor.viewportCamera) {
|
|
|
|
renderer.autoClear = false;
|
|
if (showSceneHelpers === true) renderer.render(sceneHelpers, camera);
|
|
if (vr.currentSession === null) viewHelper.render(renderer);
|
|
renderer.autoClear = true;
|
|
|
|
}
|
|
|
|
endTime = performance.now();
|
|
editor.signals.sceneRendered.dispatch(endTime - startTime);
|
|
|
|
}
|
|
|
|
return container;
|
|
|
|
}
|
|
|
|
|
|
function updateGridColors(grid1, grid2, colors) {
|
|
|
|
grid1.material.color.setHex(colors[0]);
|
|
grid2.material.color.setHex(colors[1]);
|
|
|
|
}
|
|
|
|
|
|
export { Viewport };
|
|
|