// TODO: chore: embed dependencies // TODO: refactor: make cap/end naming consistent // TODO: refactor: extract components to different files // TODO: feat: check xr support // TODO: feat: detect hands // TODO: feat: detect surfaces // TODO: feat: anchor between sessions // TODO: feat: depth // https://medium.com/@ramithrodrigo/webxr-device-api-depth-sensing-an-introduction-72accf544e3d // TODO: feat: generate random puzzle import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; import { ARButton } from 'three/addons/webxr/ARButton.js'; // Initialise scene const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.xr.enabled = true; document.body.appendChild(renderer.domElement); document.body.appendChild(ARButton.createButton(renderer)); // Set up lights const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight.position.set(1, 3, 2); scene.add(directionalLight); scene.add(directionalLight.target); // Position the camera camera.position.z = 5; const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.update(); // Make materials const redMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 }); const greenMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); const blueMaterial = new THREE.MeshLambertMaterial({ color: 0x0000ff }); function makeCylinder(material) { const radius = .25; const height = 1; const radialSegments = 8; const geometry = new THREE.CylinderGeometry(radius, radius, height, radialSegments); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); return mesh; } class CornerCurve extends THREE.Curve { constructor(scale) { super(); this.scale = scale; } getPoint(t) { const tx = 0; const ty = Math.cos(.5 * Math.PI * t) * this.scale - .5; const tz = -Math.sin(.5 * Math.PI * t) * this.scale + .5; return new THREE.Vector3(tx, ty, tz); } } function makeCorner(material) { const path = new CornerCurve(.5); const tubularSegments = 20; const radius = .25; const radialSegments = 8; const closed = false; const geometry = new THREE.TubeGeometry(path, tubularSegments, radius, radialSegments, closed); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); return mesh; } function makeCap(material) { const radius = .25; const height = .5; const radialSegments = 8; const cylinder = new THREE.CylinderGeometry(radius, radius, height, radialSegments); // Shift cylinder vertices down const positionAttribute = cylinder.getAttribute('position'); const vertex = new THREE.Vector3(); for (let i = 0; i < positionAttribute.count; i++) { vertex.fromBufferAttribute(positionAttribute, i); positionAttribute.setXYZ(i, vertex.x, vertex.y - .25, vertex.z); } const sphereRadius = .375; const widthSegments = 12; const heightSegments = 8; const sphere = new THREE.SphereGeometry(sphereRadius, widthSegments, heightSegments); var capGeometry = BufferGeometryUtils.mergeGeometries([cylinder, sphere], false); const mesh = new THREE.Mesh(capGeometry, material); scene.add(mesh); return mesh; } const PIECE_TYPE_END = 0; const PIECE_TYPE_CORNER = 1; const PIECE_TYPE_STRAIGHT = 2; // Piece encoding // // Each piece can be aligned to either one or two faces of a cube. We associate each face // of a cube with the position of a bit in a 6-bit number, starting with the bottom face, // clockwise (looking up) around the middle faces starting with the closest face to the // viewer, and ending with the top face. // // 32 // // +---------+ // /| /| // / | 8 / | // 16 +---------+ | 4 // | +------|--+ // | / 2 | / // |/ |/ // +---------+ // // 1 // // Ends are associated with one face, corners with two adjacent faces and lines with // two opposing faces. // // To correctly orient pieces we need to associate an encoding with a particular orientation // of a mesh. Orientations are 3D vectors containing multiples of 90ยบ rotations around the x, // y and z axes. const encodings = new Map(); // End pieces encodings.set(1, {type: PIECE_TYPE_END, rotation: new THREE.Vector3(0, 0, 0)}); encodings.set(2, {type: PIECE_TYPE_END, rotation: new THREE.Vector3(3, 0, 0)}); encodings.set(4, {type: PIECE_TYPE_END, rotation: new THREE.Vector3(0, 0, 1)}); encodings.set(8, {type: PIECE_TYPE_END, rotation: new THREE.Vector3(1, 0, 0)}); encodings.set(16, {type: PIECE_TYPE_END, rotation: new THREE.Vector3(0, 0, 3)}); encodings.set(32, {type: PIECE_TYPE_END, rotation: new THREE.Vector3(2, 0, 0)}); // Corners encodings.set(1|2, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 0, 0)}); encodings.set(1|4, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 1, 0)}); encodings.set(1|8, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 2, 0)}); encodings.set(1|16, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 3, 0)}); encodings.set(32|2, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(2, 2, 0)}); encodings.set(32|4, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(2, 1, 0)}); encodings.set(32|8, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(2, 0, 0)}); encodings.set(32|16,{type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(2, 3, 0)}); encodings.set(2|4, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 0, 1)}); encodings.set(4|8, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 1, 1)}); encodings.set(8|16, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(1, 3, 0)}); encodings.set(16|2, {type: PIECE_TYPE_CORNER, rotation: new THREE.Vector3(0, 0, 3)}); // Straights encodings.set(1|32, {type: PIECE_TYPE_STRAIGHT, rotation: new THREE.Vector3(0, 0, 0)}); encodings.set(2|8, {type: PIECE_TYPE_STRAIGHT, rotation: new THREE.Vector3(1, 0, 0)}); encodings.set(4|16, {type: PIECE_TYPE_STRAIGHT, rotation: new THREE.Vector3(0, 0, 1)}); function makePiece(code) { const pieceData = encodings.get(code); let mesh = null; switch (pieceData.type) { case PIECE_TYPE_END: mesh = makeCap(blueMaterial); break; case PIECE_TYPE_CORNER: mesh = makeCorner(greenMaterial); break; case PIECE_TYPE_STRAIGHT: mesh = makeCylinder(redMaterial); break; } const rotation = pieceData.rotation; mesh.rotation.x = rotation.x * Math.PI * .5; mesh.rotation.y = rotation.y * Math.PI * .5; mesh.rotation.z = rotation.z * Math.PI * .5; return mesh; } let meshes = []; function addPieceGrid() { const encodingsArray = [...encodings.entries()]; encodingsArray.forEach(([key, value], index) => { let mesh = makePiece(key); // Arrange pieces in a grid const sideLength = Math.round(Math.sqrt(encodingsArray.length)); mesh.position.x = (Math.floor(index % sideLength) - sideLength * .5) * 1.2; mesh.position.y = (Math.floor(index / sideLength) - sideLength * .5) * 1.2; meshes.push(mesh); }); } function addTestPuzzle() { let puzzleData = [ [4, -1, 1, 1], [4|16, 0, 1, 1], [16|8, 1, 1, 1], [2|8, 1, 1, 0], [2|16, 1, 1, -1], [16|4, 0, 1, -1], [4|2, -1, 1, -1], [8|4, -1, 1, 0], [16|1, 0, 1, 0], [32|4, 0, 0, 0], [16|8, 1, 0, 0], [2|16, 1, 0, -1], [4|16, 0, 0, -1], [4|2, -1, 0, -1], [8|2, -1, 0, 0], [8|4, -1, 0, 1], [16|4, 0, 0, 1], [16|1, 1, 0, 1], [32|16, 1, -1, 1], [4|16, 0, -1, 1], [4|8, -1, -1, 1], [2|8, -1, -1, 0], [2|4, -1, -1, -1], [16|4, 0, -1, -1], [16|2, 1, -1, -1], [8|16, 1, -1, 0], [4, 0, -1, 0] ]; for (var i = 0; i < puzzleData.length; i++) { const datum = puzzleData[i]; const key = datum[0]; const mesh = makePiece(key); mesh.position.set(datum[1], datum[2], datum[3]); meshes.push(mesh); } } //addPieceGrid(); addTestPuzzle(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });