// Fork of HTMLMesh.js from three.js. import * as THREE from 'three'; import {SelectEvent} from '../../core/Script'; import {User} from '../../core/User'; import {View} from '../core/View'; interface LinePoint { x: number; y: number; b?: boolean; } /** * A `View` that functions as a drawable canvas in 3D space. It uses * an HTML canvas as a texture on a plane, allowing users to draw on its surface * with their XR controllers. It supports basic drawing, undo, and redo * functionality. */ export class SketchPanel extends View { static dependencies = {user: User}; private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private activeHand = -1; private activeLine: LinePoint[] = []; private activeLines: LinePoint[][] = []; private removedLines: LinePoint[][] = []; private isDrawing = false; private user!: User; material: THREE.MeshBasicMaterial; constructor() { const canvas = document.createElement('canvas'); canvas.width = 1024; canvas.height = 1024; // Draw something on the canvas const ctx = canvas.getContext('2d')!; const texture = new THREE.CanvasTexture(canvas); const geometry = new THREE.PlaneGeometry( canvas.width * 0.001, canvas.height * 0.001 ); const material = new THREE.MeshBasicMaterial({ map: texture, toneMapped: false, alphaTest: 0.01, }); super({}, geometry, material); this.canvas = canvas; this.ctx = ctx; this.material = material; // view options this.width = canvas.width * 0.001; this.height = canvas.height * 0.001; this.scale.set(this.width, this.height, 1); } /** * Init the SketchPanel. */ init({user}: {user: User}) { super.init(); this.user = user; this.clearCanvas(); } getContext() { return this.ctx; } triggerUpdate() { this.material.map!.needsUpdate = true; } onSelectStart(event: SelectEvent) { if (this.activeHand !== -1) { // do nothing, drawing is in progress return; } this.activeHand = event?.target?.userData?.id ?? -1; if (this.activeHand === 0 || this.activeHand === 1) { this.activeLine = []; this.ctx.beginPath(); } } onSelectEnd(event: SelectEvent) { const id = event?.target?.userData?.id ?? -1; // check if user released an active hand if (id === this.activeHand) { // line could be empty, or contain select start only if (this.activeHand >= 0 && this.activeLine.length > 1) { this.activeLines.push(this.activeLine); // Added a new line, no more option for re-do this.removedLines = []; } this.isDrawing = false; this.activeLine = []; this.activeHand = -1; } } /** * Updates the painter's line to the current pivot position during selection. */ onSelecting(event: SelectEvent) { const id = event.target.userData.id; if (id !== this.activeHand) { return; } const data = this.user.getReticleIntersection(id); if (data) { if (data.object instanceof SketchPanel && data.uv) { const x = Math.round(data.uv.x * 1024); const y = Math.round(1024 - data.uv.y * 1024); const ctx = this.ctx; if (this.isDrawing) { ctx.lineTo(x, y); ctx.strokeStyle = 'black'; ctx.lineWidth = 6; // You can adjust the line width here ctx.stroke(); this.triggerUpdate(); this.activeLine.push({x, y}); } else { this.activeLine.push({x, y, b: true}); ctx.moveTo(x, y); this.isDrawing = true; } } else { // pointer exit from the SketchPanel this.isDrawing = false; } } else { // no plane at the pointer this.isDrawing = false; } } clearCanvas(forceUpdate = true) { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = '#FFFFFF'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Fill the entire canvas if (forceUpdate) { this.triggerUpdate(); } } removeAll() { this.activeLines = []; this.removedLines = []; this.clearCanvas(); } undo() { if (this.activeLines.length === 0) { return; } this.ctx = this.canvas.getContext('2d')!; const line = this.activeLines.pop()!; this.removedLines.push(line); this.clearCanvas(false); this.activeLines.forEach((line) => { this.#drawLine(line); }); this.triggerUpdate(); } redo() { if (this.removedLines.length === 0) { return; } const line = this.removedLines.pop()!; this.activeLines.push(line); this.#drawLine(line); this.triggerUpdate(); } #drawLine(line: LinePoint[]) { // common context options this.ctx.beginPath(); this.ctx.strokeStyle = 'black'; this.ctx.lineWidth = 6; line.forEach((point) => { if (point.b) { this.ctx.moveTo(point.x, point.y); } else { this.ctx.lineTo(point.x, point.y); this.ctx.stroke(); } }); } update() { // empty } }