import * as THREE from "three"; import { CSS2DRenderer, CSS2DObject } from "CSS2DRenderer"; import { CSS3DRenderer, CSS3DObject } from "CSS3DRenderer"; import { DragControls } from "DragControls"; import { OrbitControls } from "OrbitControls"; import { STLLoader } from "STLLoader"; 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(); return geometry; } function texture_material(texture) { texture.flipY = false; texture.minFilter = THREE.LinearFilter; return new THREE.MeshLambertMaterial({ map: texture, side: THREE.DoubleSide, transparent: true, }); } export default { template: `
`, mounted() { this.scene = new THREE.Scene(); this.objects = new Map(); this.objects.set("scene", this.scene); this.draggable_objects = []; window["scene_" + this.$el.id] = this.scene; // NOTE: for selenium tests only this.look_at = new THREE.Vector3(0, 0, 0); this.camera = new THREE.PerspectiveCamera(75, this.width / this.height, 0.1, 1000); this.camera.lookAt(this.look_at); this.camera.up = new THREE.Vector3(0, 0, 1); this.camera.position.set(0, -3, 5); this.scene.add(new THREE.AmbientLight(0xffffff, 0.7)); const light = new THREE.DirectionalLight(0xffffff, 0.3); light.position.set(5, 10, 40); this.scene.add(light); this.renderer = undefined; try { this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, canvas: this.$el.children[0], }); } catch { this.$el.innerHTML = "Could not create WebGL renderer."; this.$el.style.width = this.width + "px"; this.$el.style.height = this.height + "px"; this.$el.style.padding = "10px"; this.$el.style.border = "1px solid silver"; return; } this.renderer.setClearColor("#eee"); this.renderer.setSize(this.width, this.height); this.text_renderer = new CSS2DRenderer({ element: this.$el.children[1], }); this.text_renderer.setSize(this.width, this.height); this.text3d_renderer = new CSS3DRenderer({ element: this.$el.children[2], }); this.text3d_renderer.setSize(this.width, this.height); this.$nextTick(() => this.resize()); window.addEventListener("resize", this.resize, false); if (this.grid) { const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: "#eee" })); ground.translateZ(-0.01); ground.object_id = "ground"; this.scene.add(ground); const grid = new THREE.GridHelper(100, 100); grid.material.transparent = true; grid.material.opacity = 0.2; grid.rotateX(Math.PI / 2); this.scene.add(grid); } this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.drag_controls = new DragControls(this.draggable_objects, this.camera, this.renderer.domElement); const applyConstraint = (constraint, position) => { if (!constraint) return; const [variable, expression] = constraint.split("=").map((s) => s.trim()); position[variable] = eval(expression.replace(/x|y|z/g, (match) => `(${position[match]})`)); }; const handleDrag = (event) => { this.drag_constraints.split(",").forEach((constraint) => applyConstraint(constraint, event.object.position)); if (event.type === "drag") return; this.$emit(event.type, { type: event.type, object_id: event.object.object_id, object_name: event.object.name, x: event.object.position.x, y: event.object.position.y, z: event.object.position.z, }); this.controls.enabled = event.type == "dragend"; }; this.drag_controls.addEventListener("dragstart", handleDrag); this.drag_controls.addEventListener("drag", handleDrag); this.drag_controls.addEventListener("dragend", handleDrag); const render = () => { requestAnimationFrame(() => setTimeout(() => render(), 1000 / 20)); TWEEN.update(); this.renderer.render(this.scene, this.camera); this.text_renderer.render(this.scene, this.camera); this.text3d_renderer.render(this.scene, this.camera); }; render(); const raycaster = new THREE.Raycaster(); const click_handler = (mouseEvent) => { let x = (mouseEvent.offsetX / this.renderer.domElement.width) * 2 - 1; let y = -(mouseEvent.offsetY / this.renderer.domElement.height) * 2 + 1; raycaster.setFromCamera({ x: x, y: y }, this.camera); this.$emit("click3d", { hits: raycaster .intersectObjects(this.scene.children, true) .filter((o) => o.object.object_id) .map((o) => ({ object_id: o.object.object_id, object_name: o.object.name, point: o.point, })), click_type: mouseEvent.type, button: mouseEvent.button, alt_key: mouseEvent.altKey, ctrl_key: mouseEvent.ctrlKey, meta_key: mouseEvent.metaKey, shift_key: mouseEvent.shiftKey, }); }; this.$el.onclick = click_handler; this.$el.ondblclick = click_handler; this.texture_loader = new THREE.TextureLoader(); this.stl_loader = new STLLoader(); const connectInterval = setInterval(async () => { if (window.socket.id === undefined) return; this.$emit("init", { socket_id: window.socket.id }); clearInterval(connectInterval); }, 100); }, beforeDestroy() { window.removeEventListener("resize", this.resize); }, 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 CSS2DObject(div); } else if (type == "text3d") { const div = document.createElement("div"); div.textContent = args[0]; div.style.cssText = "userSelect:none;" + args[1]; mesh = new CSS3DObject(div); } else if (type == "texture") { const url = args[0]; const coords = args[1]; const geometry = texture_geometry(coords); const material = texture_material(this.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 if (type == "point_cloud") { const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.Float32BufferAttribute(args[0].flat(), 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(args[1].flat(), 3)); const material = new THREE.PointsMaterial({ size: args[2], vertexColors: true }); mesh = new THREE.Points(geometry, material); } 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; if (outline.length) { 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(); this.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; this.objects.set(id, mesh); this.objects.get(parent_id).add(this.objects.get(id)); }, name(object_id, name) { if (!this.objects.has(object_id)) return; this.objects.get(object_id).name = name; }, material(object_id, color, opacity, side) { if (!this.objects.has(object_id)) return; const material = this.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) { if (!this.objects.has(object_id)) return; this.objects.get(object_id).position.set(x, y, z); }, scale(object_id, sx, sy, sz) { if (!this.objects.has(object_id)) return; this.objects.get(object_id).scale.set(sx, sy, sz); }, rotate(object_id, R) { if (!this.objects.has(object_id)) return; const R4 = new THREE.Matrix4().makeBasis( new THREE.Vector3(...R[0]), new THREE.Vector3(...R[1]), new THREE.Vector3(...R[2]) ); this.objects.get(object_id).rotation.setFromRotationMatrix(R4.transpose()); }, visible(object_id, value) { if (!this.objects.has(object_id)) return; this.objects.get(object_id).visible = value; }, draggable(object_id, value) { if (!this.objects.has(object_id)) return; if (value) this.draggable_objects.push(this.objects.get(object_id)); else this.draggable_objects.pop(this.objects.get(object_id)); }, delete(object_id) { if (!this.objects.has(object_id)) return; this.objects.get(object_id).removeFromParent(); this.objects.delete(object_id); this.draggable_objects.pop(this.objects.get(object_id)); }, set_texture_url(object_id, url) { if (!this.objects.has(object_id)) return; const obj = this.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); this.texture_loader.load(url, on_success, undefined, on_error); }, set_texture_coordinates(object_id, coords) { if (!this.objects.has(object_id)) return; this.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 (this.camera_tween) this.camera_tween.stop(); this.camera_tween = new TWEEN.Tween([ this.camera.position.x, this.camera.position.y, this.camera.position.z, this.camera.up.x, this.camera.up.y, this.camera.up.z, this.look_at.x, this.look_at.y, this.look_at.z, ]) .to( [ x === null ? this.camera.position.x : x, y === null ? this.camera.position.y : y, z === null ? this.camera.position.z : z, up_x === null ? this.camera.up.x : up_x, up_y === null ? this.camera.up.y : up_y, up_z === null ? this.camera.up.z : up_z, look_at_x === null ? this.look_at.x : look_at_x, look_at_y === null ? this.look_at.y : look_at_y, look_at_z === null ? this.look_at.z : look_at_z, ], duration * 1000 ) .onUpdate((p) => { this.camera.position.set(p[0], p[1], p[2]); this.camera.up.set(p[3], p[4], p[5]); // NOTE: before calling lookAt this.look_at.set(p[6], p[7], p[8]); this.camera.lookAt(p[6], p[7], p[8]); this.controls.target.set(p[6], p[7], p[8]); }) .start(); }, resize() { const { clientWidth, clientHeight } = this.$el; this.renderer.setSize(clientWidth, clientHeight); this.text_renderer.setSize(clientWidth, clientHeight); this.text3d_renderer.setSize(clientWidth, clientHeight); this.camera.aspect = clientWidth / clientHeight; this.camera.updateProjectionMatrix(); }, }, props: { width: Number, height: Number, grid: Boolean, drag_constraints: String, }, };