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 };