Source: Ruler.js

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 }