You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
15 KiB
406 lines
15 KiB
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: ` |
|
<div style="position:relative"> |
|
<canvas style="position:relative"></canvas> |
|
<div style="position:absolute;pointer-events:none;top:0"></div> |
|
<div style="position:absolute;pointer-events:none;top:0"></div> |
|
</div>`, |
|
|
|
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, |
|
}, |
|
};
|
|
|