var scene; var look_at; var camera; var camera_tween; var orbitControls; var texture_loader = new THREE.TextureLoader(); var stl_loader = new THREE.STLLoader(); var objects = new Map(); const None = null; const False = false; const True = true; function texture_geometry(coords) { const geometry = new THREE.BufferGeometry(); const nI = coords[0].length; const nJ = coords.length; const vertices = []; const indices = []; const uvs = []; for (let j = 0; j < nJ; ++j) { for (let i = 0; i < nI; ++i) { const XYZ = coords[j][i] || [0, 0, 0]; vertices.push(...XYZ); uvs.push(i / (nI - 1), j / (nJ - 1)); } } for (let j = 0; j < nJ - 1; ++j) { for (let i = 0; i < nI - 1; ++i) { if (coords[j][i] && coords[j][i + 1] && coords[j + 1][i] && coords[j + 1][i + 1]) { const idx00 = i + j * nI; const idx10 = i + j * nI + 1; const idx01 = i + j * nI + nI; const idx11 = i + j * nI + 1 + nI; indices.push(idx10, idx00, idx01); indices.push(idx11, idx10, idx01); } } } geometry.setIndex(new THREE.Uint32BufferAttribute(indices, 1)); geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3)); geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)); geometry.computeVertexNormals(); geometry.computeFaceNormals(); return geometry; } function texture_material(texture) { texture.flipY = false; texture.minFilter = THREE.LinearFilter; return new THREE.MeshLambertMaterial({ map: texture, side: THREE.DoubleSide, }); } export default { template: `
`, mounted() { scene = new THREE.Scene(); objects.set("scene", scene); look_at = new THREE.Vector3(0, 0, 0); camera = new THREE.PerspectiveCamera(75, this.width / this.height, 0.1, 1000); camera.lookAt(look_at); camera.up = new THREE.Vector3(0, 0, 1); camera.position.set(0, -3, 5); scene.add(new THREE.AmbientLight(0xffffff, 0.7)); const light = new THREE.DirectionalLight(0xffffff, 0.3); light.position.set(5, 10, 40); scene.add(light); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, canvas: this.$el.children[0], }); renderer.setClearColor("#eee"); renderer.setSize(this.width, this.height); const text_renderer = new THREE.CSS2DRenderer({ element: this.$el.children[1], }); text_renderer.setSize(this.width, this.height); const text3d_renderer = new THREE.CSS3DRenderer({ element: this.$el.children[2], }); text3d_renderer.setSize(this.width, this.height); const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: "#eee" })); ground.translateZ(-0.01); ground.object_id = "ground"; scene.add(ground); const grid = new THREE.GridHelper(100, 100); grid.material.transparent = true; grid.material.opacity = 0.2; grid.rotateX(Math.PI / 2); scene.add(grid); orbitControls = new THREE.OrbitControls(camera, renderer.domElement); const render = function () { requestAnimationFrame(() => setTimeout(() => render(), 1000 / 20)); TWEEN.update(); renderer.render(scene, camera); text_renderer.render(scene, camera); text3d_renderer.render(scene, camera); }; render(); const raycaster = new THREE.Raycaster(); const click_handler = (mouseEvent) => { let x = (mouseEvent.offsetX / renderer.domElement.width) * 2 - 1; let y = -(mouseEvent.offsetY / renderer.domElement.height) * 2 + 1; raycaster.setFromCamera({ x: x, y: y }, camera); this.$emit("click", { hits: raycaster .intersectObjects(scene.children, true) .filter((o) => o.object.object_id) .map((o) => ({ object_id: o.object.object_id, point: o.point, })), click_type: mouseEvent.type, shift_key: mouseEvent.shiftKey, }); }; this.$el.onclick = click_handler; this.$el.ondblclick = click_handler; }, methods: { create(type, id, parent_id, ...args) { let mesh; if (type == "group") { mesh = new THREE.Group(); } else if (type == "line") { const start = new THREE.Vector3(...args[0]); const end = new THREE.Vector3(...args[1]); const geometry = new THREE.BufferGeometry().setFromPoints([start, end]); const material = new THREE.LineBasicMaterial({ transparent: true }); mesh = new THREE.Line(geometry, material); } else if (type == "curve") { const curve = new THREE.CubicBezierCurve3( new THREE.Vector3(...args[0]), new THREE.Vector3(...args[1]), new THREE.Vector3(...args[2]), new THREE.Vector3(...args[3]) ); const points = curve.getPoints(args[4] - 1); const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = new THREE.LineBasicMaterial({ transparent: true }); mesh = new THREE.Line(geometry, material); } else if (type == "text") { const div = document.createElement("div"); div.textContent = args[0]; div.style.cssText = args[1]; mesh = new THREE.CSS2DObject(div); } else if (type == "text3d") { const div = document.createElement("div"); div.textContent = args[0]; div.style.cssText = "userSelect:none;" + args[1]; mesh = new THREE.CSS3DObject(div); } else if (type == "texture") { const url = args[0]; const coords = args[1]; const geometry = texture_geometry(coords); const material = texture_material(texture_loader.load(url)); mesh = new THREE.Mesh(geometry, material); } else if (type == "spot_light") { mesh = new THREE.Group(); const light = new THREE.SpotLight(...args); light.position.set(0, 0, 0); light.target = new THREE.Object3D(); light.target.position.set(1, 0, 0); mesh.add(light); mesh.add(light.target); } else { let geometry; const wireframe = args.pop(); if (type == "box") geometry = new THREE.BoxGeometry(...args); if (type == "sphere") geometry = new THREE.SphereGeometry(...args); if (type == "cylinder") geometry = new THREE.CylinderGeometry(...args); if (type == "ring") geometry = new THREE.RingGeometry(...args); if (type == "quadratic_bezier_tube") { const curve = new THREE.QuadraticBezierCurve3( new THREE.Vector3(...args[0]), new THREE.Vector3(...args[1]), new THREE.Vector3(...args[2]) ); geometry = new THREE.TubeGeometry(curve, ...args.slice(3)); } if (type == "extrusion") { const shape = new THREE.Shape(); const outline = args[0]; const height = args[1]; shape.autoClose = true; shape.moveTo(outline[0][0], outline[0][1]); outline.slice(1).forEach((p) => shape.lineTo(p[0], p[1])); const settings = { depth: height, bevelEnabled: false }; geometry = new THREE.ExtrudeGeometry(shape, settings); } if (type == "stl") { const url = args[0]; geometry = new THREE.BufferGeometry(); stl_loader.load(url, (geometry) => (mesh.geometry = geometry)); } let material; if (wireframe) { mesh = new THREE.LineSegments( new THREE.EdgesGeometry(geometry), new THREE.LineBasicMaterial({ transparent: true }) ); } else { material = new THREE.MeshPhongMaterial({ transparent: true }); mesh = new THREE.Mesh(geometry, material); } } mesh.object_id = id; objects.set(id, mesh); objects.get(parent_id).add(objects.get(id)); }, material(object_id, color, opacity, side) { const material = objects.get(object_id).material; if (!material) return; material.color.set(color); material.opacity = opacity; if (side == "front") material.side = THREE.FrontSide; else if (side == "back") material.side = THREE.BackSide; else material.side = THREE.DoubleSide; }, move(object_id, x, y, z) { objects.get(object_id).position.set(x, y, z); }, scale(object_id, sx, sy, sz) { objects.get(object_id).scale.set(sx, sy, sz); }, rotate(object_id, R) { const R4 = new THREE.Matrix4().makeBasis( new THREE.Vector3(...R[0]), new THREE.Vector3(...R[1]), new THREE.Vector3(...R[2]) ); objects.get(object_id).rotation.setFromRotationMatrix(R4.transpose()); }, visible(object_id, value) { objects.get(object_id).visible = value; }, delete(object_id) { objects.get(object_id).removeFromParent(); objects.delete(object_id); }, set_texture_url(object_id, url) { const obj = objects.get(object_id); if (obj.busy) return; obj.busy = true; const on_success = (texture) => { obj.material = texture_material(texture); obj.busy = false; }; const on_error = () => (obj.busy = false); texture_loader.load(url, on_success, undefined, on_error); }, set_texture_coordinates(object_id, coords) { objects.get(object_id).geometry = texture_geometry(coords); }, move_camera(x, y, z, look_at_x, look_at_y, look_at_z, up_x, up_y, up_z, duration) { if (camera_tween) camera_tween.stop(); camera_tween = new TWEEN.Tween([ camera.position.x, camera.position.y, camera.position.z, camera.up.x, camera.up.y, camera.up.z, look_at.x, look_at.y, look_at.z, ]) .to( [ x === null ? camera.position.x : x, y === null ? camera.position.y : y, z === null ? camera.position.z : z, up_x === null ? camera.up.x : up_x, up_y === null ? camera.up.y : up_y, up_z === null ? camera.up.z : up_z, look_at_x === null ? look_at.x : look_at_x, look_at_y === null ? look_at.y : look_at_y, look_at_z === null ? look_at.z : look_at_z, ], duration * 1000 ) .onUpdate((p) => { camera.position.set(p[0], p[1], p[2]); camera.up.set(p[3], p[4], p[5]); // NOTE: before calling lookAt look_at.set(p[6], p[7], p[8]); camera.lookAt(p[6], p[7], p[8]); }) .start(); }, }, props: { width: Number, height: Number, }, };