UNPKG

@inweb/viewer-three

Version:

JavaScript library for rendering CAD and BIM files in a browser using Three.js

467 lines (373 loc) 15.6 kB
/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2025, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// import { Camera, Matrix4, Object3D, Scene, Raycaster, Vector2, Vector3, Vector4 } from "three"; import type { Viewer } from "../Viewer"; import { OrbitDragger } from "./OrbitDragger"; const PRECISION = 0.01; export class MeasureLineDragger extends OrbitDragger { private overlay: MeasureOverlay; private line: MeasureLine; private snapper: MeasureSnapper; constructor(viewer: Viewer) { super(viewer); this.overlay = new MeasureOverlay(viewer.camera, viewer.canvas); this.overlay.attach(); this.line = new MeasureLine(this.overlay); this.overlay.addLine(this.line); this.snapper = new MeasureSnapper(viewer.camera, viewer.canvas); this.snapper.update(viewer.scene); this.viewer.canvas.addEventListener("pointerdown", this.onPointerDown); this.viewer.canvas.addEventListener("pointermove", this.onPointerMove); this.viewer.canvas.addEventListener("pointerup", this.onPointerUp); this.viewer.canvas.addEventListener("pointercancel", this.onPointerCancel); this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave); this.viewer.addEventListener("render", this.renderOverlay); this.viewer.addEventListener("hide", this.updateSnapper); this.viewer.addEventListener("isolate", this.updateSnapper); this.viewer.addEventListener("show", this.updateSnapper); this.viewer.addEventListener("showall", this.updateSnapper); } override dispose() { this.viewer.canvas.removeEventListener("pointerdown", this.onPointerDown); this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove); this.viewer.canvas.removeEventListener("pointerup", this.onPointerUp); this.viewer.canvas.removeEventListener("pointercancel", this.onPointerCancel); this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave); this.viewer.removeEventListener("render", this.renderOverlay); this.viewer.removeEventListener("hide", this.updateSnapper); this.viewer.removeEventListener("isolate", this.updateSnapper); this.viewer.removeEventListener("show", this.updateSnapper); this.viewer.removeEventListener("showall", this.updateSnapper); this.overlay.detach(); this.overlay.dispose(); super.dispose(); } onPointerDown = (event: PointerEvent) => { if (event.button !== 0) return; this.line.startPoint = this.snapper.getSnapPoint(event); this.line.render(); this.viewer.canvas.setPointerCapture(event.pointerId); this.orbit.enabled = !this.line.startPoint; }; onPointerMove = (event: PointerEvent) => { if (this.orbit.enabled && this.orbit.state !== -1) return; this.line.endPoint = this.snapper.getSnapPoint(event); this.line.render(); if (this.line.startPoint) this.changed = true; // <- to prevent context menu }; onPointerUp = (event: PointerEvent) => { if (this.line.startPoint && this.line.endPoint && this.line.getDistance() >= PRECISION) { this.line = new MeasureLine(this.overlay); this.overlay.addLine(this.line); } else { this.line.startPoint = undefined; this.line.endPoint = undefined; this.line.render(); } this.viewer.canvas.releasePointerCapture(event.pointerId); this.orbit.enabled = true; }; onPointerCancel = (event: PointerEvent) => { this.viewer.canvas.dispatchEvent(new PointerEvent("pointerup", event)); }; onPointerLeave = () => { this.line.endPoint = undefined; this.line.render(); }; renderOverlay = () => { this.overlay.render(); }; updateSnapper = () => { this.snapper.update(this.viewer.scene); }; } class MeasureSnapper { private camera: Camera; private canvas: HTMLCanvasElement; private objects: Object3D[] = []; private raycaster: Raycaster; constructor(camera: Camera, canvas: HTMLCanvasElement) { this.camera = camera; this.canvas = canvas; this.raycaster = new Raycaster(); } getSnapPoint(event: PointerEvent): Vector3 | undefined { const mouse = new Vector2(event.clientX, event.clientY); const rect = this.canvas.getBoundingClientRect(); const x = ((mouse.x - rect.left) / rect.width) * 2 - 1; const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1; const coords = new Vector2(x, y); this.raycaster.setFromCamera(coords, this.camera); this.raycaster.params = { Mesh: {}, Line: { threshold: 0.25 }, Line2: { threshold: 0.25 }, LOD: {}, Points: { threshold: 0.1 }, Sprite: {}, }; const intersects = this.raycaster.intersectObjects(this.objects, false); if (intersects.length === 0) return undefined; return intersects[0].point; } update(scene: Scene) { this.objects = []; scene.traverseVisible((child) => this.objects.push(child)); } } class MeasureOverlay { public camera: Camera; public canvas: HTMLCanvasElement; public container: HTMLElement; public lines: MeasureLine[] = []; public projector: MeasureProjector; constructor(camera: Camera, canvas: HTMLCanvasElement) { this.camera = camera; this.canvas = canvas; this.projector = new MeasureProjector(camera, canvas); } attach() { this.container = document.createElement("div"); this.container.id = "measure-container"; this.container.style.background = "rgba(0,0,0,0)"; this.container.style.position = "absolute"; this.container.style.top = "0px"; this.container.style.left = "0px"; this.container.style.width = "100%"; this.container.style.height = "100%"; this.container.style.outline = "none"; this.container.style.pointerEvents = "none"; this.container.style.overflow = "hidden"; this.canvas.parentElement.appendChild(this.container); } dispose() { this.clear(); } detach() { this.container.remove(); this.container = undefined; } clear() { this.lines.forEach((line) => line.dispose()); this.lines = []; } render() { this.projector.updateProjectionMatrix(); this.lines.forEach((line) => line.render()); } update() { this.lines.forEach((line) => line.update()); } addLine(line: MeasureLine) { this.lines.push(line); } removeLine(line: MeasureLine) { this.lines = this.lines.filter((x) => x !== line); } } const _middlePoint = new Vector3(); class MeasureLine { private overlay: MeasureOverlay; private elementStartPoint: HTMLElement; private elementEndPoint: HTMLElement; private elementLine: HTMLElement; private elementLabel: HTMLElement; public startPoint: Vector3; public endPoint: Vector3; public id = Date.now(); public unit = ""; public scale = 1.0; public size = 10.0; public lineWidth = 2; public style = { border: "2px solid #FFFFFF", background: "#009bff", boxShadow: "0 0 10px rgba(0,0,0,0.5)", color: "white", font: "1rem system-ui", }; constructor(overlay: MeasureOverlay) { this.overlay = overlay; this.elementStartPoint = overlay.container.appendChild(document.createElement("div")); this.elementEndPoint = overlay.container.appendChild(document.createElement("div")); this.elementLine = overlay.container.appendChild(document.createElement("div")); this.elementLabel = overlay.container.appendChild(document.createElement("div")); this.update(); } dispose() { this.elementStartPoint.remove(); this.elementEndPoint.remove(); this.elementLine.remove(); this.elementLabel.remove(); } render() { const projector = this.overlay.projector; if (this.startPoint) { const { point, visible } = projector.projectPoint(this.startPoint); this.elementStartPoint.style.display = visible ? "block" : "none"; this.elementStartPoint.style.left = `${point.x}px`; this.elementStartPoint.style.top = `${point.y}px`; } else { this.elementStartPoint.style.display = "none"; } if (this.endPoint) { const { point, visible } = projector.projectPoint(this.endPoint); this.elementEndPoint.style.display = visible ? "block" : "none"; this.elementEndPoint.style.left = `${point.x}px`; this.elementEndPoint.style.top = `${point.y}px`; } else { this.elementEndPoint.style.display = "none"; } if (this.startPoint && this.endPoint) { const { point1, point2, visible } = projector.projectLine(this.startPoint, this.endPoint); point2.sub(point1); const angle = point2.angle(); const width = point2.length(); this.elementLine.style.display = visible ? "block" : "none"; this.elementLine.style.left = `${point1.x}px`; this.elementLine.style.top = `${point1.y}px`; this.elementLine.style.width = `${width}px`; this.elementLine.style.transform = `translate(0px, ${-this.lineWidth / 2}px) rotate(${angle}rad)`; } else { this.elementLine.style.display = "none"; } if (this.startPoint && this.endPoint) { _middlePoint.lerpVectors(this.startPoint, this.endPoint, 0.5); const { point, visible } = projector.projectPoint(_middlePoint); const distance = this.getDistance(); this.elementLabel.style.display = visible && distance >= PRECISION ? "block" : "none"; this.elementLabel.style.left = `${point.x}px`; this.elementLabel.style.top = `${point.y}px`; this.elementLabel.innerHTML = `${distance.toFixed(2)} ${this.unit}`; } else { this.elementLabel.style.display = "none"; } } update() { this.elementStartPoint.id = `markup-dot-start-${this.id}`; this.elementStartPoint.style.position = "absolute"; this.elementStartPoint.style.zIndex = "2"; this.elementStartPoint.style.width = `${this.size}px`; this.elementStartPoint.style.height = `${this.size}px`; this.elementStartPoint.style.border = this.style.border; this.elementStartPoint.style.borderRadius = `${this.size}px`; this.elementStartPoint.style.background = this.style.background; this.elementStartPoint.style.boxShadow = this.style.boxShadow; this.elementStartPoint.style.transform = "translate(-50%, -50%)"; this.elementEndPoint.id = `markup-dot-end-${this.id}`; this.elementEndPoint.style.position = "absolute"; this.elementEndPoint.style.zIndex = "2"; this.elementEndPoint.style.width = `${this.size}px`; this.elementEndPoint.style.height = `${this.size}px`; this.elementEndPoint.style.border = this.style.border; this.elementEndPoint.style.borderRadius = `${this.size}px`; this.elementEndPoint.style.background = this.style.background; this.elementEndPoint.style.boxShadow = this.style.boxShadow; this.elementEndPoint.style.transform = "translate(-50%, -50%)"; this.elementLine.id = `markup-line-${this.id}`; this.elementLine.style.position = "absolute"; this.elementLine.style.zIndex = "1"; this.elementLine.style.height = `${this.lineWidth}px`; this.elementLine.style.background = this.style.background; this.elementLine.style.boxShadow = this.style.boxShadow; this.elementLine.style.transformOrigin = `0px ${this.lineWidth / 2}px`; this.elementLabel.id = `markup-label-${this.id}`; this.elementLabel.style.position = "absolute"; this.elementLabel.style.zIndex = "3"; this.elementLabel.style.padding = "2px"; this.elementLabel.style.paddingInline = "5px"; this.elementLabel.style.borderRadius = "5px"; this.elementLabel.style.background = this.style.background; this.elementLabel.style.boxShadow = this.style.boxShadow; this.elementLabel.style.color = this.style.color; this.elementLabel.style.font = this.style.font; this.elementLabel.style.transform = "translate(-50%, -50%)"; } getDistance(): number { return this.startPoint.distanceTo(this.endPoint) / this.scale; } } let _widthHalf: number; let _heightHalf: number; const _viewMatrix = new Matrix4(); const _viewProjectionMatrix = new Matrix4(); const _vector = new Vector3(); const _vector1 = new Vector4(); const _vector2 = new Vector4(); const point = new Vector2(); const point1 = new Vector2(); const point2 = new Vector2(); class MeasureProjector { private camera: Camera; private canvas: HTMLElement; constructor(camera: Camera, canvas: HTMLCanvasElement) { this.camera = camera; this.canvas = canvas; } updateProjectionMatrix() { const rect = this.canvas.getBoundingClientRect(); _widthHalf = rect.width / 2; _heightHalf = rect.height / 2; _viewMatrix.copy(this.camera.matrixWorldInverse); _viewProjectionMatrix.multiplyMatrices(this.camera.projectionMatrix, _viewMatrix); } projectPoint(p: Vector3) { _vector.copy(p).applyMatrix4(_viewProjectionMatrix); const visible = _vector.z >= -1 && _vector.z <= 1; point.x = (_vector.x + 1) * _widthHalf; point.y = (-_vector.y + 1) * _heightHalf; return { point, visible }; } projectLine(p1: Vector3, p2: Vector3) { let visible: boolean; _vector1.copy(p1 as any).applyMatrix4(_viewProjectionMatrix); _vector2.copy(p2 as any).applyMatrix4(_viewProjectionMatrix); // see three/examples/jsm/renderers/Projector.js/clipLine for more details const bc1near = _vector1.z + _vector1.w; const bc2near = _vector2.z + _vector2.w; const bc1far = -_vector1.z + _vector1.w; const bc2far = -_vector2.z + _vector2.w; if (bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0) visible = true; else if ((bc1near < 0 && bc2near < 0) || (bc1far < 0 && bc2far < 0)) visible = false; else { let alpha1 = 0; let alpha2 = 1; if (bc1near < 0) alpha1 = Math.max(alpha1, bc1near / (bc1near - bc2near)); else if (bc2near < 0) alpha2 = Math.min(alpha2, bc1near / (bc1near - bc2near)); if (bc1far < 0) alpha1 = Math.max(alpha1, bc1far / (bc1far - bc2far)); else if (bc2far < 0) alpha2 = Math.min(alpha2, bc1far / (bc1far - bc2far)); visible = alpha2 >= alpha1; if (visible) { _vector1.lerp(_vector2, alpha1); _vector2.lerp(_vector1, 1 - alpha2); } } _vector1.multiplyScalar(1 / _vector1.w); _vector2.multiplyScalar(1 / _vector2.w); point1.x = (_vector1.x + 1) * _widthHalf; point1.y = (-_vector1.y + 1) * _heightHalf; point2.x = (_vector2.x + 1) * _widthHalf; point2.y = (-_vector2.y + 1) * _heightHalf; return { point1, point2, visible }; } }