Source: EditorSvgAnnotation.js

import { Skin } from './Skin.js';
import { Util } from './Util.js';
import { simplify, smooth, smoothToPath } from './Simplify.js'
import { LayerSvgAnnotation } from './LayerSvgAnnotation.js'
import { CoordinateSystem } from './CoordinateSystem.js'

/**
 * EditorSvgAnnotation enables creation and editing of SVG annotations in OpenLIME.
 * Optimized version with simplified erase tool functionality.
 */
class EditorSvgAnnotation {
	constructor(viewer, layer, options) {
		this.layer = layer;
		Object.assign(this, {
			viewer: viewer,
			panning: false,
			tool: null,
			startPoint: null,
			currentLine: [],
			annotation: null,
			priority: 20000,
			pinSize: 36, // Default pin size in pixels at zoom level 1
			classes: {
				'': { stroke: '#000', label: '' },
				'class1': { stroke: '#770', label: '' },
				'class2': { stroke: '#707', label: '' },
				'class3': { stroke: '#777', label: '' },
				'class4': { stroke: '#070', label: '' },
				'class5': { stroke: '#007', label: '' },
				'class6': { stroke: '#077', label: '' },
			},
			tools: {
				point: {
					img: '<svg width=24 height=24><circle cx=12 cy=12 r=3 fill="red" stroke="gray"/></svg>',
					tooltip: 'New point',
					tool: Point,
				},
				pin: {
					template: (x, y, annotation, size) => {
						const idx = annotation?.data?.idx || '?';
						return `<svg xmlns='http://www.w3.org/2000/svg' x='${x}' y='${y}' width='${size}' height='${size}' class='pin'
						viewBox='0 0 18 18'><path d='M 0,0 C 0,0 4,0 8,0 12,0 16,4 16,8 16,12 12,16 8,16 4,16 0,12 0,8 0,4 0,0 0,0 Z'/><text class='pin-text' x='7' y='8' text-anchor='middle' dominant-baseline='middle'>${idx}</text></svg>`;
					},
					tooltip: 'New pin',
					tool: Pin
				},
				pen: {
					img: '<svg width=24 height=24><circle cx=12 cy=12 r=3 fill="red" stroke="gray"/></svg>',
					tooltip: 'New polyline',
					tool: Pen,
				},
				line: {
					img: `<svg width=24 height=24>
						<path d="m 4.7,4.5 c 0.5,4.8 0.8,8.5 3.1,11 2.4,2.6 4.2,-4.8 6.3,-5 2.7,-0.3 5.1,9.3 5.1,9.3" stroke-width="3" fill="none" stroke="grey"/>
						<path d="m 4.7,4.5 c 0.5,4.8 0.8,8.5 3.1,11 2.4,2.6 4.2,-4.8 6.3,-5 2.7,-0.3 5.1,9.3 5.1,9.3" stroke-width="1" fill="none" stroke="red"/></svg>`,
					tooltip: 'New line',
					tool: Line,
				},
				erase: {
					img: '',
					tooltip: 'Erase elements',
					tool: Erase,
				},
				box: {
					img: '<svg width=24 height=24><rect x=5 y=5 width=14 height=14 fill="red" stroke="gray"/></svg>',
					tooltip: 'New box',
					tool: Box,
				},
				circle: {
					img: '<svg width=24 height=24><circle cx=12 cy=12 r=7 fill="red" stroke="gray"/></svg>',
					tooltip: 'New circle',
					tool: Circle,
				},
			},
			annotation: null,
			enableState: false,
			customState: null,
			customData: null,
			editWidget: null,
			selectedCallback: null,
			createCallback: null,
			updateCallback: null,
			deleteCallback: null
		}, options);

		layer.style += Object.entries(this.classes).map((g) => {
			console.assert(g[1].hasOwnProperty('stroke'), "Classes needs a stroke property");
			return `[data-class=${g[0]}] { stroke:${g[1].stroke}; }`;
		}).join('\n');

		// Add default pin sizing based on zoom level - but more advanced
		if (!options.annotationUpdate) {
			layer.annotationUpdate = (anno, transform) => {
				this.updateAnnotationPins(anno, transform);
			};
		}

		// Register for pointer events
		viewer.pointerManager.onEvent(this);
		document.addEventListener('keyup', (e) => this.keyUp(e), false);
		
		layer.addEvent('selected', (anno) => {
			if (!anno || anno == this.annotation)
				return;
			if (this.selectedCallback) this.selectedCallback(anno);
			this.showEditWidget(anno);
		});

		layer.annotationsEntry = () => {
			let entry = {
				html: `<div class="openlime-tools"></div>`,
				list: [],
				classes: 'openlime-annotations',
				status: () => 'active',
				oncreate: () => {
					if (Array.isArray(layer.annotations))
						layer.createAnnotationsList();

					let tools = {
						'add': { action: () => { this.createAnnotation(); }, title: "New annotation" },
						'edit': { action: () => { this.toggleEditWidget(); }, title: "Edit annotations" },
						'export': { action: () => { this.exportAnnotations(); }, title: "Export annotations" },
						'trash': { action: () => { this.deleteSelected(); }, title: "Delete selected annotations" },
					};
					(async () => {
						for (const [label, tool] of Object.entries(tools)) {
							let icon = await Skin.appendIcon(entry.element.firstChild, '.openlime-' + label);
							icon.setAttribute('title', tool.title);
							icon.addEventListener('click', tool.action);
						}
					})();
				}
			}
			layer.annotationsListEntry = entry;
			return entry;
		}

		// IMPORTANT: Capture clicks in capture phase for erase tool
		// This prevents the annotation layer from handling the click first
		this.viewer.containerElement.addEventListener('click', (ev) => {
			// Only process if erase tool is active AND we have an annotation selected
			if (this.tool !== 'erase' || !this.annotation) {
				return; // Let other handlers process the event normally
			}
			
			// Don't intercept clicks on UI elements (toolbar, menus, etc.)
			const target = ev.target;
			if (target && (
				target.closest('.openlime-toolbar') ||
				target.closest('.openlime-layers-menu') || 
				target.closest('.openlime-annotation-edit') ||
				target.classList.contains('openlime-tool') ||
				target.classList.contains('openlime-button') ||
				target.closest('button') ||
				target.closest('.openlime-dialog')
			)) {
				return; // Let UI elements handle their own clicks
			}
			
			// Find the target element
			const targetElement = this._findElementUnderPointer(ev);
			
			if (targetElement) {
				// Save current state for undo
				this.saveCurrent();
				
				// Remove element from annotation
				const index = this.annotation.elements.indexOf(targetElement);
				if (index > -1) {
					this.annotation.elements.splice(index, 1);
					
					// Check if annotation is now empty
					if (this.annotation.elements.length === 0) {
						// Remove the entire annotation instead of keeping empty annotation
						this.deleteAnnotation(this.annotation.id);
						
						// Hide edit widget since annotation is gone
						this.hideEditWidget();
					} else {
						// Save and update for non-empty annotations
						this.saveAnnotation();
						this.annotation.needsUpdate = true;
						this.viewer.redraw();
					}
				}
				
				// Stop event propagation to prevent other handlers
				ev.stopImmediatePropagation();
				ev.preventDefault();
			}
			// No target element found, let the click be handled normally
		}, true); // true = capture phase (runs before other event handlers)
	}

	/**
	 * Finds the SVG element under the pointer for erase tool
	 * @param {Event} e - The event object
	 * @private
	 */
	_findElementUnderPointer(e) {
		// Temporarily disable overlay pointer events
		const overlay = this.viewer?.overlayElement || document.querySelector('.openlime-overlay');
		const prevPointerEvents = overlay ? overlay.style.pointerEvents : null;
		if (overlay) {
			overlay.style.pointerEvents = 'none';
		}
		
		let targetElement = null;
		
		try {
			// Get element from document
			let element = document.elementFromPoint(e.clientX, e.clientY);
			
			// If we hit a shadow host, try to get element from shadow root
			if (element && element.shadowRoot) {
				const shadowElement = element.shadowRoot.elementFromPoint(e.clientX, e.clientY);
				if (shadowElement) {
					element = shadowElement;
				}
			}
			
			// Check if this element (or any parent) is in our annotation
			if (element && this.annotation && Array.isArray(this.annotation.elements)) {
				let current = element;
				const elementSet = new Set(this.annotation.elements);
				
				// Walk up the DOM tree
				while (current && current !== document) {
					if (elementSet.has(current)) {
						targetElement = current;
						break;
					}
					
					// Check if current element is a child of any annotation element
					for (const annotationEl of this.annotation.elements) {
						if (annotationEl.contains && annotationEl.contains(current)) {
							targetElement = annotationEl;
							break;
						}
					}
					
					if (targetElement) break;
					current = current.parentNode;
				}
			}
			
		} finally {
			// Restore overlay pointer events
			if (overlay) {
				overlay.style.pointerEvents = prevPointerEvents ?? '';
			}
		}
		
		return targetElement;
	}

	/**
	 * Calculates the correct pin size based on current zoom level
	 * @returns {number} Pin size in pixels
	 * @private
	 */
	getCurrentPinSize() {
		const transform = this.viewer.camera.getCurrentTransform(performance.now());
		return this.pinSize / transform.z;
	}

	/**
	 * Updates pin sizes in an annotation based on transform
	 * @param {Object} anno - Annotation object
	 * @param {Object} transform - Current transform
	 * @private
	 */
	updateAnnotationPins(anno, transform) {
		let size = this.pinSize / transform.z;
		if (size !== anno.previous_pin_size) {
			anno.elements.forEach(element => {
				if (element.classList.contains('pin')) {
					element.setAttribute('width', size + 'px');
					element.setAttribute('height', size + 'px');
				}
			});
			anno.previous_pin_size = size;
		}
	}

	/**
	 * Creates a new annotation with correct initial state
	 * @returns {void}
	 */
	createAnnotation() {
		let anno = this.layer.newAnnotation();
		if (this.customData) this.customData(anno);
		if (this.enableState) this.setAnnotationCurrentState(anno);
		anno.data.idx = this.layer.annotations.length;
		anno.publish = 1;
		anno.label = anno.description = anno.class = '';
		let post = {
			id: anno.id, label: anno.label, description: anno.description, 'class': anno.class, svg: null,
			publish: anno.publish, data: anno.data
		};
		if (this.enableState) post = { ...post, state: anno.state };
		if (this.createCallback) {
			let result = this.createCallback(post);
			if (!result)
				alert("Failed to create annotation!");
		}
		this.layer.setSelected(anno);
	}

	toggleEditWidget() {
		if (this.annotation)
			return this.hideEditWidget();

		let id = this.layer.selected.values().next().value;
		if (!id)
			return;

		let anno = this.layer.getAnnotationById(id);
		this.showEditWidget(anno);
	}

	updateEditWidget() {
		let anno = this.annotation;
		let edit = this.editWidget;
		if (!anno.class)
			anno.class = '';
		edit.querySelector('[name=label]').value = anno.label || '';
		edit.querySelector('[name=description]').value = anno.description || '';
		Object.entries(anno.data).map(k => {
			edit.querySelector(`[name=data-data-${k[0]}]`).value = k[1] || '';
		});

		edit.querySelector('[name=classes]').value = anno.class;
		edit.querySelector('[name=publish]').checked = anno.publish == 1;
		edit.classList.remove('hidden');
		let button = edit.querySelector('.openlime-select-button');
		button.textContent = this.classes[anno.class].label;
		button.style.background = this.classes[anno.class].stroke;
	}

	showEditWidget(anno) {
		this.annotation = anno;
		
		// Add reference to editor for pin size calculations
		anno.editor = this;
		
		this.setTool(null);
		this.setActiveTool();
		this.layer.annotationsListEntry.element.querySelector('.openlime-edit').classList.add('active');
		(async () => {
			await this.createEditWidget();
			this.updateEditWidget();
		})();
	}

	hideEditWidget() {
		this.annotation = null;
		this.setTool(null);
		if (this.editWidget) {
			this.editWidget.classList.add('hidden');
		}
		this.layer.annotationsListEntry.element.querySelector('.openlime-edit').classList.remove('active');
	}

	async createEditWidget() {
		if (this.editWidget)
			return;
		let html = `
				<div class="openlime-annotation-edit">
					<label for="label">Title:</label> <input name="label" type="text"><br>
					<label for="description">Description:</label><br>
					<textarea name="description" cols="30" rows="5"></textarea><br>
					<span>Class:</span> 
					<div class="openlime-select">
						<input type="hidden" name="classes" value=""/>
						<div class="openlime-select-button"></div>
						<ul class="openlime-select-menu">
						${Object.entries(this.classes).map((c) =>
			`<li data-class="${c[0]}" style="background:${c[1].stroke};">${c[1].label}</li>`).join('\n')}
						</ul>
					</div>
					${Object.entries(this.annotation.data).map(k => {
				let label = k[0];
				let str = `<label for="data-data-${k[0]}">${label}:</label> <input name="data-data-${k[0]}" type="text"><br>`
				return str;
			}).join('\n')}
					<br>
					<span><button class="openlime-state">SAVE</button></span>
					<span><input type="checkbox" name="publish" value=""> Publish</span><br>
					<div class="openlime-annotation-edit-tools"></div>
				</div>`;
		let template = document.createElement('template');
		template.innerHTML = html.trim();
		let edit = template.content.firstChild;

		let select = edit.querySelector('.openlime-select');
		let button = edit.querySelector('.openlime-select-button');
		let ul = edit.querySelector('ul');
		let options = edit.querySelectorAll('li');
		let input = edit.querySelector('[name=classes]');

		let state = edit.querySelector('.openlime-state');

		state.addEventListener('click', (e) => {
			if (this.enableState) this.setAnnotationCurrentState(this.annotation);
			this.saveCurrent();
			this.saveAnnotation();
		});

		button.addEventListener('click', (e) => {
			e.stopPropagation();
			for (let o of options)
				o.classList.remove('selected');
			select.classList.toggle('active');
		});

		ul.addEventListener('click', (e) => {
			e.stopPropagation();
			input.value = e.srcElement.getAttribute('data-class');
			input.dispatchEvent(new Event('change'));
			button.style.background = this.classes[input.value].stroke;
			button.textContent = e.srcElement.textContent;
			select.classList.toggle('active');
		});

		document.addEventListener('click', (e) => {
			select.classList.remove('active');
		});

		document.querySelector('.openlime-layers-menu').appendChild(edit);

		let tools = edit.querySelector('.openlime-annotation-edit-tools');

		let pin = await Skin.appendIcon(tools, '.openlime-pin');
		pin.addEventListener('click', (e) => {
			if (this.tool === 'pin') { this.setTool(null); this.setActiveTool(); }
			else { this.setTool('pin'); this.setActiveTool(pin); }
		});

		let draw = await Skin.appendIcon(tools, '.openlime-draw');
		draw.addEventListener('click', (e) => {
			if (this.tool === 'line') { this.setTool(null); this.setActiveTool(); }
			else { this.setTool('line'); this.setActiveTool(draw); }
		});

		let erase = await Skin.appendIcon(tools, '.openlime-erase');
		erase.addEventListener('click', (e) => {
			if (this.tool === 'erase') { this.setTool(null); this.setActiveTool(); }
			else { this.setTool('erase'); this.setActiveTool(erase); }
		});

		let undo = await Skin.appendIcon(tools, '.openlime-undo');
		undo.addEventListener('click', (e) => { this.undo(); });

		let redo = await Skin.appendIcon(tools, '.openlime-redo');
		redo.addEventListener('click', (e) => { this.redo(); });

		// Setup form field event listeners
		this._setupFormEventListeners(edit);

		edit.classList.add('hidden');
		this.editWidget = edit;
	}

	_setupFormEventListeners(edit) {
		let label = edit.querySelector('[name=label]');
		label.addEventListener('blur', (e) => { 
			if (this.annotation.label != label.value) {
				this.saveCurrent(); 
				this.saveAnnotation(); 
			}
		});

		let descr = edit.querySelector('[name=description]');
		descr.addEventListener('blur', (e) => { 
			if (this.annotation.description != descr.value) {
				this.saveCurrent(); 
				this.saveAnnotation(); 
			}
		});

		let idx = edit.querySelector('[name=data-data-idx]');
		if (idx) {
			idx.addEventListener('blur', (e) => {
				if (this.annotation.data.idx != idx.value) {
					const svgPinIdx = this.annotation.elements[0];
					if (svgPinIdx) {
						const txt = svgPinIdx.querySelector(".pin-text");
						if (txt) {
							txt.textContent = idx.value;
						}
					}
					this.saveCurrent();
					this.saveAnnotation();
				}
			});
		}

		Object.entries(this.annotation.data).map(k => {
			let dataElm = edit.querySelector(`[name=data-data-${k[0]}]`);
			if (dataElm) {
				dataElm.addEventListener('blur', (e) => { 
					if (this.annotation.data[k[0]] != dataElm.value) {
						this.saveCurrent(); 
						this.saveAnnotation(); 
					}
				});
			}
		});

		let classes = edit.querySelector('[name=classes]');
		classes.addEventListener('change', (e) => { 
			if (this.annotation.class != classes.value) {
				this.saveCurrent(); 
				this.saveAnnotation(); 
			}
		});

		let publish = edit.querySelector('[name=publish]');
		publish.addEventListener('change', (e) => { 
			if (this.annotation.publish != (publish.checked ? 1 : 0)) {
				this.saveCurrent(); 
				this.saveAnnotation(); 
			}
		});
	}

	setAnnotationCurrentState(anno) {
		anno.state = window.structuredClone(this.viewer.canvas.getState());
		if (this.customState) this.customState(anno);
	}

	saveAnnotation() {
		let edit = this.editWidget;
		let anno = this.annotation;

		anno.label = edit.querySelector('[name=label]').value || '';
		anno.description = edit.querySelector('[name=description]').value || '';
		Object.entries(anno.data).map(k => {
			const element = edit.querySelector(`[name=data-data-${k[0]}]`);
			if (element) {
				anno.data[k[0]] = element.value || '';
			}
		});
		anno.publish = edit.querySelector('[name=publish]').checked ? 1 : 0;
		let select = edit.querySelector('[name=classes]');
		anno.class = select.value || '';

		let button = edit.querySelector('.openlime-select-button');
		button.style.background = this.classes[anno.class].stroke;

		for (let e of this.annotation.elements)
			e.setAttribute('data-class', anno.class);

		let post = {
			id: anno.id, label: anno.label, description: anno.description, class: anno.class,
			publish: anno.publish, data: anno.data
		};
		if (this.enableState) post = { ...post, state: anno.state };

		let serializer = new XMLSerializer();
		post.svg = `<svg xmlns="http://www.w3.org/2000/svg">
			${anno.elements.map((s) => { s.classList.remove('selected'); return serializer.serializeToString(s) }).join("\n")}  
			</svg>`;

		if (this.updateCallback) {
			let result = this.updateCallback(post);
			if (!result) {
				alert("Failed to update annotation");
				return;
			}
		}

		// Recreate the annotations list
		if (this.layer.annotationsListEntry && this.layer.annotationsListEntry.element && this.layer.annotationsListEntry.element.parentElement) {
			const list = this.layer.annotationsListEntry.element.parentElement.querySelector('.openlime-list');
			if (list) {
				const selectContainer = list.querySelector('.openlime-annotations-select');
				const wasActive = selectContainer && selectContainer.classList.contains('active');

				if (selectContainer && selectContainer._cleanup) {
					selectContainer._cleanup();
				}

				this.layer.createAnnotationsList();

				if (wasActive) {
					const newSelectContainer = list.querySelector('.openlime-annotations-select');
					if (newSelectContainer) {
						newSelectContainer.classList.add('active');
					}
				}
			}
		}

		this.layer.setSelected(anno);
	}

	deleteSelected() {
		let id = this.layer.selected.values().next().value;
		if (id)
			this.deleteAnnotation(id);
	}

	deleteAnnotation(id) {
		let anno = this.layer.getAnnotationById(id);
		if (this.deleteCallback) {
			if (!confirm(`Deleting annotation ${anno.label}, are you sure?`))
				return;
			let result = this.deleteCallback(anno);
			if (!result) {
				alert("Failed to delete this annotation.");
				return;
			}
		}
		
		// Remove SVG elements from the canvas
		this.layer.svgGroup.querySelectorAll(`[data-annotation="${anno.id}"]`).forEach(e => e.remove());

		// Remove entry from the list
		let list = this.layer.annotationsListEntry.element.parentElement.querySelector('.openlime-list');
		list.querySelectorAll(`[data-annotation="${anno.id}"]`).forEach(e => e.remove());

		this.layer.annotations = this.layer.annotations.filter(a => a !== anno);
		this.layer.clearSelected();
		this.hideEditWidget();
	}

	exportAnnotations() {
		let svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		const bBox = this.layer.boundingBox();
		svgElement.setAttribute('viewBox', `0 0 ${bBox.xHigh - bBox.xLow} ${bBox.yHigh - bBox.yLow}`);
		let style = Util.createSVGElement('style');
		style.textContent = this.layer.style;
		svgElement.appendChild(style);
		let serializer = new XMLSerializer();
		
		for (let anno of this.layer.annotations) {
			for (let e of anno.elements) {
				if (e.tagName == 'path') {
					let d = e.getAttribute('d');
					e.setAttribute('d', d.replaceAll(',', ' '));
				}
				svgElement.appendChild(e.cloneNode());
			}
		}
		let svg = serializer.serializeToString(svgElement);

		var e = document.createElement('a');
		e.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(svg));
		e.setAttribute('download', 'annotations.svg');
		e.style.display = 'none';
		document.body.appendChild(e);
		e.click();
		document.body.removeChild(e);
	}

	setActiveTool(e) {
		if (!this.editWidget) return;
		let tools = this.editWidget.querySelector('.openlime-annotation-edit-tools');
		tools.querySelectorAll('svg').forEach(a =>
			a.classList.remove('active'));
		if (e)
			e.classList.add('active');
	}

	setTool(tool) {
		this.tool = tool;
		if (this.factory && this.factory.quit)
			this.factory.quit();
		if (tool) {
			if (!(tool in this.tools)) throw "Unknown editor tool: " + tool;

			this.factory = new this.tools[tool].tool(this.tools[tool]);
			this.factory.annotation = this.annotation;
			this.factory.layer = this.layer;
			
			// Add reference to editor for pin size calculations
			if (this.annotation) {
				this.annotation.editor = this;
			}
		}
		document.querySelector('.openlime-overlay').classList.toggle('erase', tool == 'erase');
		document.querySelector('.openlime-overlay').classList.toggle('crosshair', tool && tool != 'erase');
	}

	// UNDO/REDO SYSTEM
	undo() {
		let anno = this.annotation;
		if (!anno) return;
		
		if (this.factory && this.factory.undo && this.factory.undo()) {
			anno.needsUpdate = true;
			this.viewer.redraw();
			return;
		}

		if (anno.history && anno.history.length) {
			anno.future.push(this.annoToData(anno));
			let data = anno.history.pop();
			this.dataToAnno(data, anno);
			anno.needsUpdate = true;
			this.viewer.redraw();
			this.updateEditWidget();
		}
	}

	redo() {
		let anno = this.annotation;
		if (!anno) return;
		
		if (this.factory && this.factory.redo && this.factory.redo()) {
			anno.needsUpdate = true;
			this.viewer.redraw();
			return;
		}
		
		if (anno.future && anno.future.length) {
			anno.history.push(this.annoToData(anno));
			let data = anno.future.pop();
			this.dataToAnno(data, anno);
			anno.needsUpdate = true;
			this.viewer.redraw();
			this.updateEditWidget();
		}
	}

	saveCurrent() {
		let anno = this.annotation;
		if (!anno.history) anno.history = [];
		anno.history.push(this.annoToData(anno));
		anno.future = [];
	}

	annoToData(anno) {
		let data = {};
		for (let i of ['id', 'label', 'description', 'class', 'publish', 'data'])
			data[i] = `${anno[i] || ''}`;
		data.elements = anno.elements.map(e => { 
			let n = e.cloneNode(); 
			n.points = e.points; 
			return n; 
		});
		return data;
	}

	dataToAnno(data, anno) {
		for (let i of ['id', 'label', 'description', 'class', 'publish', 'data'])
			anno[i] = `${data[i]}`;
		anno.elements = data.elements.map(e => { 
			let n = e.cloneNode(); 
			n.points = e.points; 
			return n; 
		});
	}

	// EVENT HANDLERS
	keyUp(e) {
		if (e.defaultPrevented) return;
		switch (e.key) {
			case 'Escape':
				if (this.tool) {
					this.setActiveTool();
					this.setTool(null);
					e.preventDefault();
				}
				break;
			case 'Delete':
				this.deleteSelected();
				break;
			case 'z':
				if (e.ctrlKey) this.undo();
				break;
			case 'Z':
				if (e.ctrlKey) this.redo();
				break;
		}
	}

	panStart(e) {
		if (e.buttons != 1 || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)
			return;
		if (!['line', 'box', 'circle'].includes(this.tool))
			return;
		this.panning = true;
		e.preventDefault();

		this.saveCurrent();
		const pos = this.mapToSvg(e);
		let shape = this.factory.create(pos, e);
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	panMove(e) {
		if (!this.panning) return false;
		const pos = this.mapToSvg(e);
		this.factory.adjust(pos, e);
	}

	panEnd(e) {
		if (!this.panning) return false;
		this.panning = false;

		const pos = this.mapToSvg(e);
		let changed = this.factory.finish(pos, e);
		if (!changed) {
			this.annotation.history.pop();
		} else {
			this.saveAnnotation();
		}
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	fingerHover(e) {
		if (this.tool != 'line') return;
		e.preventDefault();
		const pos = this.mapToSvg(e);
		let changed = this.factory.hover(pos, e);
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	fingerSingleTap(e) {
		if (!['point', 'pin', 'line', 'erase'].includes(this.tool))
			return;
		e.preventDefault();

		// For erase tool, ensure we have an annotation and factory
		if (this.tool === 'erase' && (!this.annotation || !this.factory)) {
			return;
		}

		this.saveCurrent();

		const pos = this.mapToSvg(e);
		let changed = this.factory.tap(pos, e);
		
		if (!changed) {
			this.annotation.history.pop();
		} else {
			this.saveAnnotation();
		}
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	fingerDoubleTap(e) {
		if (!['line'].includes(this.tool)) return;
		e.preventDefault();

		this.saveCurrent();
		const pos = this.mapToSvg(e);
		let changed = this.factory.doubleTap(pos, e);
		
		if (!changed) {
			this.annotation.history.pop();
		} else {
			this.saveAnnotation();
		}
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	mapToSvg(e) {
		// For erase tool, find the element under the pointer
		if (this.tool === 'erase') {
			e.targetElement = this._findElementUnderPointer(e);
		}

		const p = { x: e.offsetX, y: e.offsetY };
		const layerT = this.layer.transform;
		const useGL = false;
		const layerbb = this.layer.boundingBox();
		const layerSize = { w: layerbb.width(), h: layerbb.height() };
		let pos = CoordinateSystem.fromCanvasHtmlToImage(p, this.viewer.camera, layerT, layerSize, useGL);
		p.x += 1;
		let pos1 = CoordinateSystem.fromCanvasHtmlToImage(p, this.viewer.camera, layerT, layerSize, useGL);
		pos.pixelSize = Math.abs(pos1.x - pos.x);
		return pos;
	}
}

// TOOL CLASSES
class Point {
	tap(pos) {
		let point = Util.createSVGElement('circle', { cx: pos.x, cy: pos.y, r: 10, class: 'point' });
		this.annotation.elements.push(point);
		return true;
	}
}

class Pin {
	constructor(options) {
		Object.assign(this, options);
	}
	tap(pos) {
		// Calculate correct pin size for current zoom level
		const currentSize = this.annotation.editor?.getCurrentPinSize() || 36;
		
		const str = this.template(pos.x, pos.y, this.annotation, currentSize);
		let parser = new DOMParser();
		let point = parser.parseFromString(str, "image/svg+xml").documentElement;
		
		// Add reference to editor for future updates
		if (!this.annotation.editor) {
			// Find the editor instance from the factory
			if (this.layer && this.layer.editor) {
				this.annotation.editor = this.layer.editor;
			}
		}
		
		// Add to elements array
		this.annotation.elements.push(point);
		return true;
	}
}

class Pen {
	constructor() {
		this.points = [];
	}
	create(pos) {
		this.points.push(pos);
		if (this.points.length == 1) {
			this.path = Util.createSVGElement('path', { d: `M${pos.x} ${pos.y}`, class: 'line' });
			return this.path;
		}
		let p = this.path.getAttribute('d');
		this.path.setAttribute('d', p + ` L${pos.x} ${pos.y}`);
		this.path.points = this.points;
	}
	undo() {
		if (!this.points.length) return;
		this.points.pop();
		let d = this.points.map((p, i) => `${i == 0 ? 'M' : 'L'}${p.x} ${p.y}`).join(' ');
		this.path.setAttribute('d', d);

		if (this.points.length < 2) {
			this.points = [];
			this.annotation.elements = this.annotation.elements.filter((e) => e != this.path);
		}
	}
}

class Box {
	constructor() {
		this.origin = null;
		this.box = null;
	}

	create(pos) {
		this.origin = pos;
		this.box = Util.createSVGElement('rect', { x: pos.x, y: pos.y, width: 0, height: 0, class: 'rect' });
		this.annotation.elements.push(this.box);
		return this.box;
	}

	adjust(pos) {
		let p = this.origin;
		this.box.setAttribute('x', Math.min(p.x, pos.x));
		this.box.setAttribute('width', Math.abs(pos.x - p.x));
		this.box.setAttribute('y', Math.min(p.y, pos.y));
		this.box.setAttribute('height', Math.abs(pos.y - p.y));
	}

	finish(pos) {
		return true;
	}
}

class Circle {
	constructor() {
		this.origin = null;
		this.circle = null;
	}
	create(pos) {
		this.origin = pos;
		this.circle = Util.createSVGElement('circle', { cx: pos.x, cy: pos.y, r: 0, class: 'circle' });
		this.annotation.elements.push(this.circle);
		return this.circle;
	}
	adjust(pos) {
		let p = this.origin;
		let r = Math.hypot(pos.x - p.x, pos.y - p.y);
		this.circle.setAttribute('r', r);
	}
	finish() {
		return true;
	}
}

class Line {
	constructor() {
		this.history = []
	}
	create(pos) {
		for (let e of this.annotation.elements) {
			if (!e.points || e.points.length < 2)
				continue;
			if (Line.distance(e.points[0], pos) / pos.pixelSize < 5) {
				e.points.reverse();
				this.path = e;
				this.path.setAttribute('d', Line.svgPath(e.points));
				this.history = [this.path.points.length];
				return;
			}
			if (Line.distanceToLast(e.points, pos) < 5) {
				this.path = e;
				this.adjust(pos);
				this.history = [this.path.points.length];
				return;
			}
		}
		this.path = Util.createSVGElement('path', { d: `M${pos.x} ${pos.y}`, class: 'line' });
		this.path.points = [pos];
		this.history = [this.path.points.length];
		this.annotation.elements.push(this.path);
	}

	tap(pos) {
		if (!this.path) {
			this.create(pos);
			return false;
		} else {
			if (this.adjust(pos))
				this.history = [this.path.points.length - 1];
			return true;
		}
	}

	doubleTap(pos) {
		if (!this.path) return false;
		if (this.adjust(pos)) {
			this.history = [this.path.points.length - 1];
			this.path = null;
		}
		return false;
	}

	hover(pos, event) {
		return;
	}

	quit() {
		return;
	}

	adjust(pos) {
		let gap = Line.distanceToLast(this.path.points, pos);
		if (gap / pos.pixelSize < 4) return false;

		this.path.points.push(pos);
		this.path.setAttribute('d', Line.svgPath(this.path.points));
		return true;
	}

	finish() {
		this.path.setAttribute('d', Line.svgPath(this.path.points));
		return true;
	}

	undo() {
		if (!this.path || !this.history.length)
			return false;
		this.path.points = this.path.points.slice(0, this.history.pop());
		this.path.setAttribute('d', Line.svgPath(this.path.points));
		return true;
	}

	redo() {
		return false;
	}

	static svgPath(points) {
		let tolerance = 1.5 * points[0].pixelSize;
		let tmp = simplify(points, tolerance);
		let smoothed = smooth(tmp, 90, true);
		return smoothToPath(smoothed);
	}

	static distanceToLast(line, point) {
		let last = line[line.length - 1];
		return Line.distance(last, point);
	}

	static distance(a, b) {
		let dx = a.x - b.x;
		let dy = a.y - b.y;
		return Math.sqrt(dx * dx + dy * dy);
	}
}

/**
 * Simplified Erase class that removes entire SVG elements
 */
class Erase {
	tap(pos, event) {
		// Get the target element from the event (set in mapToSvg)
		const targetElement = event.targetElement;
		
		if (!targetElement) {
			return false; // No element found under pointer
		}

		// Simply remove from annotation elements array
		const index = this.annotation.elements.indexOf(targetElement);
		if (index > -1) {
			this.annotation.elements.splice(index, 1);
			
			// Mark annotation as needing update so it gets redrawn
			this.annotation.needsUpdate = true;
			return true; // Element was successfully removed
		}

		return false;
	}

	// These methods are required by the factory system but not used for simple erase
	create(pos, event) { return this.tap(pos, event); }
	adjust(pos, event) { return false; }
	finish(pos, event) { return false; }
}

export { EditorSvgAnnotation }