diff --git a/index.css b/index.css index da76ed6..e393a7b 100644 --- a/index.css +++ b/index.css @@ -7,8 +7,25 @@ canvas { height: 100%; } +.header { + height: 100px; + display: flex; + flex-direction: row; + align-items: center; + + position: absolute; + top: 0px; + width: 100%; + padding: 10px; + box-sizing: border-box; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + z-index: 1; +} + .container { - font-family: 'Roboto', sans-serif; + font-family: "Roboto", sans-serif; } .input_container { @@ -22,6 +39,8 @@ canvas { .input_box { display: flex; margin-bottom: 1em; + flex-wrap: wrap; + gap: 1rem; } .input_label { @@ -62,6 +81,12 @@ canvas { bottom: 1px; top: auto; } + +#forkongithub { + display: none; +} + + @media screen and (min-width: 800px) { #forkongithub { position: fixed; @@ -85,9 +110,4 @@ canvas { -o-transform: rotate(45deg); box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8); } - - .input_container { - width: 50%; - margin-top: 1.5em; - } } diff --git a/index.html b/index.html index de6d2b0..81fb99a 100644 --- a/index.html +++ b/index.html @@ -11,16 +11,21 @@ Fork me on GitHub -
-
- - -
-
- Hide Plane +
+
+
+ Load STL: + + + + +
+
+ Hide Plane +
diff --git a/index.js b/index.js index 208694d..ff65e90 100644 --- a/index.js +++ b/index.js @@ -1,114 +1,354 @@ import * as THREE from "https://threejsfundamentals.org/threejs/resources/threejs/r127/build/three.module.js"; +import { VRButton } from "https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/webxr/VRButton.js"; +import { OrbitControls } from "https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/controls/OrbitControls.js"; +import { XRControllerModelFactory } from "https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/webxr/XRControllerModelFactory.js"; +import { STLLoader } from "https://threejsfundamentals.org/threejs/resources/threejs/r127/examples/jsm/loaders/STLLoader.js"; +import { TransformControls } from "https://threejsfundamentals.org/threejs/resources/threejs/r127/examples/jsm/controls/TransformControls.js"; -import { TrackballControls } from "https://threejsfundamentals.org/threejs/resources/threejs/r127/examples/jsm/controls/TrackballControls.js"; +let container; +let camera, scene, renderer; +let controller1, controller2; +let controllerGrip1, controllerGrip2; -let renderer, scene, planeMesh, planeMesh2, group; +let raycaster; + +const intersected = []; +const tempMatrix = new THREE.Matrix4(); + +let controls, tControls, group; + +let planeMesh, planeMesh2; let planes = []; let planesOriginal = []; +init(); +animate(); + function init() { + container = document.createElement("div"); + document.body.appendChild(container); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x808080); + + camera = new THREE.PerspectiveCamera( + 70, + window.innerWidth / window.innerHeight, + 1, + 10000 + ); + // camera.position.set(0, 1.6, 3); + camera.position.set(0, -200, 100); + + controls = new OrbitControls(camera, container); + // controls.target.set(0, 1.6, 0); + controls.update(); + + const floorGeometry = new THREE.PlaneGeometry(4, 4); + const floorMaterial = new THREE.MeshStandardMaterial({ + color: 0xeeeeee, + roughness: 1.0, + metalness: 0.0, + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.receiveShadow = true; + scene.add(floor); + + scene.add(new THREE.AmbientLight(0xffffff, 0.5)); + + // const light = new THREE.DirectionalLight(0xffffff); + // light.position.set(0, 6, 0); + // light.castShadow = true; + // light.shadow.camera.top = 2; + // light.shadow.camera.bottom = -2; + // light.shadow.camera.right = 2; + // light.shadow.camera.left = -2; + // light.shadow.mapSize.set(4096, 4096); + // scene.add(light); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); + directionalLight.position.copy(camera.position); + directionalLight.castShadow = true; + scene.add(directionalLight); + + group = new THREE.Group(); + scene.add(group); + + // // Create box1 + // const geometry1 = new THREE.BoxGeometry(0.5, 0.5, 0.5); + // const material = new THREE.MeshStandardMaterial({ + // color: "#ff0000", + // side: THREE.DoubleSide, + // }); + + // const box1 = new THREE.Mesh(geometry1, material); + // box1.name = "box1"; + // box1.position.set(0, 1, 0); + // box1.renderOrder = 6; + + // // Create box2 + // const geometry2 = new THREE.BoxGeometry(0.2, 0.2, 0.2); + // const material2 = new THREE.MeshStandardMaterial({ + // color: "#C7AC96", + // side: THREE.DoubleSide, + // }); + + // const box2 = new THREE.Mesh(geometry2, material2); + // box2.position.set(0, 1, 0); + // box2.name = "box2"; + // box2.renderOrder = 6; + + // group.name = "group"; + // group.add(box1); + // group.add(box2); + + // Create plane + // const planeGeometry = new THREE.PlaneGeometry(1, 1, 1, 1); + // const planeMaterial = new THREE.MeshStandardMaterial({ + // color: "#38382f", + // side: THREE.DoubleSide, + // }); + + // planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); + // planeMesh.position.set(0, 1, 0); + // planeMesh.rotation.x = Math.PI / 2; + // planeMesh.name = "plane"; + // scene.add(planeMesh); + + // // Create plane2 + // const planeGeometry2 = new THREE.PlaneGeometry(1, 1, 1, 1); + // const planeMaterial2 = new THREE.MeshStandardMaterial({ + // color: "#f0f0f0", + // side: THREE.DoubleSide, + // }); + + // planeMesh2 = new THREE.Mesh(planeGeometry2, planeMaterial2); + // planeMesh2.position.set(0, 1, 0); + // planeMesh2.name = "plane2"; + // scene.add(planeMesh2); + + // Renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); - renderer.setClearColor(0xf0f0f0); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); + renderer.outputEncoding = THREE.sRGBEncoding; + renderer.shadowMap.enabled = true; renderer.localClippingEnabled = true; - - let container = document.createElement("div"); - document.body.appendChild(container); + renderer.xr.enabled = true; container.appendChild(renderer.domElement); - window.addEventListener("resize", onWindowResize, false); - let camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000); - camera.position.x = 0; - camera.position.y = -200; - camera.position.z = 100; + document.body.appendChild(VRButton.createButton(renderer)); - let controls = new TrackballControls(camera, container); - controls.rotateSpeed = 10.0; + tControls = new TransformControls(camera, renderer.domElement); + tControls.addEventListener("change", render); - scene = new THREE.Scene(); - scene.add(new THREE.AmbientLight(0x505050)); + tControls.addEventListener("dragging-changed", function (event) { + controls.enabled = !event.value; + }); - // LIGHTS - const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // color, intensity - scene.add(ambientLight); + scene.add(tControls); - const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); - directionalLight.position.copy(camera.position); - directionalLight.castShadow = true; - scene.add(directionalLight); + // controllers - // Create box1 - const geometry = new THREE.BoxGeometry(100, 100, 100); - const material = new THREE.MeshStandardMaterial({ - color: "#ff0000", - side: THREE.DoubleSide, - }); + controller1 = renderer.xr.getController(0); + controller1.addEventListener("selectstart", onSelectStart); + controller1.addEventListener("selectend", onSelectEnd); + scene.add(controller1); + + controller2 = renderer.xr.getController(1); + controller2.addEventListener("selectstart", onSelectStart); + controller2.addEventListener("selectend", onSelectEnd); + scene.add(controller2); + + const controllerModelFactory = new XRControllerModelFactory(); + + controllerGrip1 = renderer.xr.getControllerGrip(0); + controllerGrip1.add( + controllerModelFactory.createControllerModel(controllerGrip1) + ); + scene.add(controllerGrip1); + + controllerGrip2 = renderer.xr.getControllerGrip(1); + controllerGrip2.add( + controllerModelFactory.createControllerModel(controllerGrip2) + ); + scene.add(controllerGrip2); + + // + + const geometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + const line = new THREE.Line(geometry); + line.name = "line"; + line.scale.z = 5; + + controller1.add(line.clone()); + controller2.add(line.clone()); + + raycaster = new THREE.Raycaster(); - const box1 = new THREE.Mesh(geometry, material); - box1.name = "box1"; - box1.renderOrder = 6; + // - // Create box2 - const geometry2 = new THREE.BoxGeometry(70, 70, 70); - const material2 = new THREE.MeshStandardMaterial({ + window.addEventListener("resize", onWindowResize); +} + +let mesh, position; + +// Load the file and get the geometry +document.getElementById("file").onchange = (e) => { + const files = e.target.files; + + if (files.length > 0) { + for (var i = 0; i < files.length; i++) { + loadFile(files[i]); + } + } +}; + +const loadFile = (file) => { + let reader = new FileReader(); + + reader.onload = () => { + const geometry = new STLLoader().parse(reader.result); + + createMeshFromFile(geometry); + }; + + reader.readAsArrayBuffer(file); +}; + +/** + * Creates the mesh from the file's geometry + * @param {THREE.BufferGeometry} geometry + */ +const createMeshFromFile = (geometry) => { + if (mesh) { + scene.remove(mesh); + } + + const material = new THREE.MeshLambertMaterial({ color: "#C7AC96", - side: THREE.DoubleSide, + wireframe: false, }); + mesh = new THREE.Mesh(geometry, material); + + // saves the position of the first element + if (!position) { + position = getCenter(mesh); + } - const box2 = new THREE.Mesh(geometry2, material2); - //box2.position.set(100, 0, 0); - box2.name = "box2"; - box2.renderOrder = 6; + mesh.position.set(-position.x, -position.y, -position.z); - group = new THREE.Group(); - group.name = "group"; - group.add(box1); - group.add(box2); - scene.add(group); + group.add(mesh); +}; - // Create plane - const planeGeometry = new THREE.PlaneGeometry(200, 200, 1, 1); - const planeMaterial = new THREE.MeshStandardMaterial({ +document.getElementById("addPlanes").addEventListener("click", () => { + const geometry = new THREE.PlaneGeometry(30, 30, 1, 1); + const material = new THREE.MeshStandardMaterial({ color: "#38382f", side: THREE.DoubleSide, }); + const mesh = new THREE.Mesh(geometry, material); + mesh.name = "plane"; - planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); - //planeMesh.position.set(50, 0, 0); - planeMesh.rotation.x = Math.PI / 2; - planeMesh.name = "plane"; - scene.add(planeMesh); + scene.add(mesh); - // Create plane2 - const planeGeometry2 = new THREE.PlaneGeometry(200, 200, 1, 1); - const planeMaterial2 = new THREE.MeshStandardMaterial({ - color: "#f0f0f0", - side: THREE.DoubleSide, - }); + tControls.attach(mesh); + tControls.setMode("translate"); +}); + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onSelectStart(event) { + const controller = event.target; - planeMesh2 = new THREE.Mesh(planeGeometry2, planeMaterial2); - //planeMesh2.position.set(50, 0, 20); - planeMesh2.name = "plane2"; - scene.add(planeMesh2); + const intersections = getIntersections(controller); - function onWindowResize() { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); + if (intersections.length > 0) { + const intersection = intersections[0]; + + const object = intersection.object; + object.material.emissive.b = 1; + controller.attach(object); + + controller.userData.selected = object; } +} - function render() { - requestAnimationFrame(render); - controls.update(); - renderer.render(scene, camera); +function onSelectEnd(event) { + const controller = event.target; + + if (controller.userData.selected !== undefined) { + const object = controller.userData.selected; + object.material.emissive.b = 0; + group.attach(object); + + controller.userData.selected = undefined; } +} - render(); +function getIntersections(controller) { + tempMatrix.identity().extractRotation(controller.matrixWorld); + + raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld); + raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix); + + return raycaster.intersectObjects(scene.children, false); } -init(); +function intersectObjects(controller) { + // Do not highlight when already selected + + if (controller.userData.selected !== undefined) return; + + const line = controller.getObjectByName("line"); + const intersections = getIntersections(controller); + + if (intersections.length > 0) { + const intersection = intersections[0]; + + const object = intersection.object; + object.material.emissive.r = 1; + intersected.push(object); + + line.scale.z = intersection.distance; + } else { + line.scale.z = 5; + } +} + +function cleanIntersected() { + while (intersected.length) { + const object = intersected.pop(); + object.material.emissive.r = 0; + } +} + +// + +function animate() { + renderer.setAnimationLoop(render); +} + +function render() { + cleanIntersected(); + + intersectObjects(controller1); + intersectObjects(controller2); + + renderer.render(scene, camera); +} const negated = document.getElementById("negated"); const negatedBox = document.getElementById("negatedBox"); @@ -116,11 +356,15 @@ const negatedBox = document.getElementById("negatedBox"); document.getElementById("clipping").addEventListener("click", () => { planes = []; planesOriginal = []; - const result = scene.children.filter((object) => object.name.startsWith("Clipping")); + const result = scene.children.filter((object) => + object.name.startsWith("Clipping") + ); if (result.length === 0) { negatedBox.style.display = "unset"; - const planesGeometry = scene.children.filter((object) => object.name.startsWith("plane")); + const planesGeometry = scene.children.filter((object) => + object.name.startsWith("plane") + ); const normals = []; const centers = []; @@ -145,9 +389,12 @@ document.getElementById("clipping").addEventListener("click", () => { }); // Calculates the barycenter of the planes - const pointx = centers.reduce((prev, curr) => prev + curr.x, 0) / centers.length; - const pointy = centers.reduce((prev, curr) => prev + curr.y, 0) / centers.length; - const pointz = centers.reduce((prev, curr) => prev + curr.z, 0) / centers.length; + const pointx = + centers.reduce((prev, curr) => prev + curr.x, 0) / centers.length; + const pointy = + centers.reduce((prev, curr) => prev + curr.y, 0) / centers.length; + const pointz = + centers.reduce((prev, curr) => prev + curr.z, 0) / centers.length; const barycenter = new THREE.Vector3(pointx, pointy, pointz); const distances = []; @@ -188,7 +435,9 @@ document.getElementById("clipping").addEventListener("click", () => { }); document.getElementById("hidePlane").addEventListener("click", () => { - const planesGeometry = scene.children.filter((object) => object.name.startsWith("plane")); + const planesGeometry = scene.children.filter((object) => + object.name.startsWith("plane") + ); planesGeometry.forEach((item) => (item.visible = !item.visible)); }); @@ -198,7 +447,9 @@ let count = 0; negated.addEventListener("click", () => { count++; - const result = scene.children.filter((object) => object.name.startsWith("Clipping")); + const result = scene.children.filter((object) => + object.name.startsWith("Clipping") + ); if (result.length > 0) { // removes the previous clipping object @@ -237,7 +488,13 @@ negated.addEventListener("click", () => { * @param {Number} renderOrder The render order of the mesh * @returns THREE.Group of meshes */ -export const createPlaneStencilGroup = (name, position, geometry, plane, renderOrder) => { +export const createPlaneStencilGroup = ( + name, + position, + geometry, + plane, + renderOrder +) => { const group = new THREE.Group(); const baseMat = new THREE.MeshBasicMaterial(); baseMat.depthWrite = false; @@ -288,7 +545,13 @@ export const createPlaneStencilGroup = (name, position, geometry, plane, renderO * @param {THREE.Plane} planesNegated The list of the negated planes * @param {THREE.Plane} planes The list of the planes */ -export const addColorToClippedMesh = (scene, group, planesNegated, planes, negatedClick) => { +export const addColorToClippedMesh = ( + scene, + group, + planesNegated, + planes, + negatedClick +) => { let object = new THREE.Group(); object.name = "ClippingGroup"; scene.add(object); @@ -298,16 +561,32 @@ export const addColorToClippedMesh = (scene, group, planesNegated, planes, negat group.children.map((mesh) => { for (let i = 0; i < planesNegated.length; i++) { const planeObj = planesNegated[i]; - const stencilGroup = createPlaneStencilGroup(mesh.name, mesh.position, mesh.geometry, planeObj, y); + const stencilGroup = createPlaneStencilGroup( + mesh.name, + mesh.position, + mesh.geometry, + planeObj, + y + ); object.add(stencilGroup); - const cap = createPlaneColored(planes, planeObj, mesh.material.color, y + 0.1, negatedClick); + const cap = createPlaneColored( + planes, + planeObj, + mesh.material.color, + y + 0.1, + negatedClick + ); cap.name = "Clipping" + mesh.name; scene.add(cap); planeObj.coplanarPoint(cap.position); - cap.lookAt(cap.position.x - planeObj.normal.x, cap.position.y - planeObj.normal.y, cap.position.z - planeObj.normal.z); + cap.lookAt( + cap.position.x - planeObj.normal.x, + cap.position.y - planeObj.normal.y, + cap.position.z - planeObj.normal.z + ); y++; } @@ -315,7 +594,13 @@ export const addColorToClippedMesh = (scene, group, planesNegated, planes, negat }); }; -const createPlaneColored = (planes, plane, color, renderOrder, negatedClick) => { +const createPlaneColored = ( + planes, + plane, + color, + renderOrder, + negatedClick +) => { const capMat = new THREE.MeshStandardMaterial({ color: color, metalness: 0.1, @@ -348,3 +633,12 @@ const getCenterPoint = (mesh) => { mesh.localToWorld(center); return center; }; + +const getCenter = (object) => { + const center = new THREE.Vector3(); + + const box3 = new THREE.Box3().setFromObject(object); + box3.getCenter(center); + + return center; +};