import { Util } from './Util' import { Units } from './ScaleBar' /** * @fileoverview * Ruler module provides measurement functionality for the OpenLIME viewer. * Allows users to measure distances in the scene with an interactive ruler tool. * Extends the Units class to handle unit conversions and formatting. * * Ruler class creates an interactive measurement tool for the OpenLIME viewer. * Features: * - Interactive distance measurement * - SVG-based visualization * - Scale-aware display * - Multiple measurement history * - Touch and mouse support * * @extends Units */ class Ruler extends Units { /** * Creates a new Ruler instance. * @param {Viewer} viewer - The OpenLIME viewer instance * @param {number} pixelSize - Size of a pixel in real-world units * @param {Object} [options] - Configuration options * @param {boolean} [options.enabled=false] - Whether the ruler is initially enabled * @param {number} [options.priority=100] - Event handling priority * @param {number} [options.fontSize=18] - Font size for measurements in pixels * @param {number} [options.markerSize=8] - Size of measurement markers in pixels * @param {string} [options.cursor='crosshair'] - Cursor style when ruler is active */ constructor(viewer, pixelSize, options) { super(options); Object.assign(this, { viewer: viewer, camera: viewer.camera, overlay: viewer.overlayElement, pixelSize: pixelSize, enabled: false, priority: 100, measure: null, //current measure history: [], //past measures fontSize: 18, markerSize: 8, cursor: "crosshair", svg: null, first: null, second: null }); if (options) Object.assign(this, options); } /** * Activates the ruler tool. * Creates SVG elements if needed and sets up event listeners. * Changes cursor to indicate tool is active. */ start() { this.enabled = true; this.previousCursor = this.overlay.style.cursor; this.overlay.style.cursor = this.cursor; if (!this.svg) { this.svg = Util.createSVGElement('svg', { class: 'openlime-ruler' }); this.svgGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); this.svg.append(this.svgGroup); this.overlay.appendChild(this.svg); this.viewer.addEvent('draw', () => this.update()); this.update(); } } /** * Deactivates the ruler tool. * Restores original cursor and clears current measurement. */ end() { this.enabled = false; this.overlay.style.cursor = this.previousCursor; this.clear(); } /** * Clears all measurements. * Removes all SVG elements and resets measurement history. */ clear() { this.svgGroup.replaceChildren([]); this.measure = null; this.history = []; } /*finish() { let m = this.measure; m.line = Util.createSVGElement('line', { x1: m.x1, y1: m.y1, x2: m.x2, y2: m.y2 }); this.svgGroup.appendChild(m.line); m.text = Util.createSVGElement('text'); m.text.textContent = this.format(this.length(m)); this.svgGroup.appendChild(m.text); this.history.push(m); this.measure = null; this.update(); }*/ /** * Updates the visual representation of all measurements. * Handles camera transformations and viewport changes. * @private */ update() { if (!this.history.length) return; //if not enabled skip let t = this.camera.getGlCurrentTransform(performance.now()); let viewport = this.camera.glViewport(); this.svg.setAttribute('viewBox', `${-viewport.w / 2} ${-viewport.h / 2} ${viewport.w} ${viewport.h}`); let c = { x: 0, y: 0 }; //this.boundingBox().corner(0); this.svgGroup.setAttribute("transform", `translate(${t.x} ${t.y}) rotate(${-t.a} 0 0) scale(${t.z} ${t.z}) translate(${c.x} ${c.y})`); for (let m of this.history) this.updateMeasure(m, t); } /** * Creates a marker SVG element. * @private * @param {number} x - X coordinate in scene space * @param {number} y - Y coordinate in scene space * @returns {SVGElement} The created marker element */ createMarker(x, y) { let m = Util.createSVGElement("path"); this.svgGroup.appendChild(m); return m; } /** * Updates a marker's position and size. * @private * @param {SVGElement} marker - The marker to update * @param {number} x - X coordinate in scene space * @param {number} y - Y coordinate in scene space * @param {number} size - Marker size in pixels */ updateMarker(marker, x, y, size) { let d = `M ${x - size} ${y} L ${x + size} ${y} M ${x} ${y - size} L ${x} ${y + size}`; marker.setAttribute('d', d); } /** * Updates measurement text display. * Handles text positioning and scaling based on camera transform. * @private * @param {Object} measure - The measurement object to update * @param {number} fontsize - Font size in pixels */ updateText(measure, fontsize) { measure.text.setAttribute('font-size', fontsize + "px"); let dx = measure.x1 - measure.x2; let dy = measure.y1 - measure.y2; let length = Math.sqrt(dx * dx + dy * dy); if (length > 0) { dx /= length; dy /= length; } if (dx < 0) { dx = -dx; dy = -dy; } let mx = (measure.x1 + measure.x2) / 2; let my = (measure.y1 + measure.y2) / 2; if (dy / dx < 0) { mx -= 0.25 * dy * fontsize; my += dx * fontsize; } else { my -= 0.25 * fontsize; mx += 0.25 * fontsize; } measure.text.setAttribute('x', mx); measure.text.setAttribute('y', my); measure.text.textContent = this.format(length * this.pixelSize); } /** * Creates a new measurement. * Sets up SVG elements for line, markers, and text. * @private * @param {number} x - Initial X coordinate * @param {number} y - Initial Y coordinate * @returns {Object} Measurement object containing all SVG elements and coordinates */ createMeasure(x, y) { let m = { marker1: this.createMarker(x, y), x1: x, y1: y, marker2: this.createMarker(x, y), x2: x, y2: y }; m.line = Util.createSVGElement('line', { x1: m.x1, y1: m.y1, x2: m.x2, y2: m.y2 }); this.svgGroup.appendChild(m.line); m.text = Util.createSVGElement('text'); m.text.textContent = ''; this.svgGroup.appendChild(m.text); return m; } /** * Updates a measurement's visual elements. * @private * @param {Object} measure - The measurement to update * @param {Transform} transform - Current camera transform */ updateMeasure(measure, transform) { let markersize = window.devicePixelRatio * this.markerSize / transform.z; this.updateMarker(measure.marker1, measure.x1, measure.y1, markersize); this.updateMarker(measure.marker2, measure.x2, measure.y2, markersize); let fontsize = window.devicePixelRatio * this.fontSize / transform.z; this.updateText(measure, fontsize); for (let p of ['x1', 'y1', 'x2', 'y2']) measure.line.setAttribute(p, measure[p]); } /** * Handles single tap/click events. * Creates or completes measurements. * @private * @param {Event} e - The pointer event * @returns {boolean} Whether the event was handled */ fingerSingleTap(e) { if (!this.enabled) return false; let transform = this.camera.getCurrentTransform(performance.now()) let { x, y } = this.camera.mapToScene(e.layerX, e.layerY, transform); if (!this.measure) { this.measure = this.createMeasure(x, y); this.history.push(this.measure); } else { this.measure.x2 = x; this.measure.y2 = y; this.measure = null; } this.update(); e.preventDefault(); } /** * Handles hover/move events. * Updates the current measurement endpoint. * @private * @param {Event} e - The pointer event * @returns {boolean} Whether the event was handled */ fingerHover(e) { if (!this.enabled || !this.measure) return false; let transform = this.camera.getCurrentTransform(performance.now()) let { x, y } = this.camera.mapToScene(e.layerX, e.layerY, transform); this.measure.x2 = x; this.measure.y2 = y; this.update(); e.preventDefault(); } }; /** * Example usage of Ruler: * ```javascript * // Create ruler with 1mm per pixel scale * const ruler = new Ruler(viewer, 0.001, { * fontSize: 24, * markerSize: 10 * }); * * // Activate ruler * ruler.start(); * * // Deactivate ruler * ruler.end(); * * // Clear measurements * ruler.clear(); * ``` */ export { Ruler }