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,
},
};