diff --git a/tools/spatialDraw/DrawingManager.js b/tools/spatialDraw/DrawingManager.js index d9e91b4..93d303b 100644 --- a/tools/spatialDraw/DrawingManager.js +++ b/tools/spatialDraw/DrawingManager.js @@ -1,5 +1,3 @@ -/* global MeshLine, MeshLineMaterial, MeshLineRaycast */ - /** Class that draws to a 3D scene. */ class DrawingManager { /** @@ -10,7 +8,8 @@ class DrawingManager { constructor(scene, camera) { this.toolMap = { 'LINE': new DrawingManager.Tool.Line(this), - 'ICON': new DrawingManager.Tool.Icon(this) + 'POLYGON': new DrawingManager.Tool.Polygon(this), + 'ICON': new DrawingManager.Tool.Icon(this), }; this.cursorMap = { 'PROJECTION': new DrawingManager.Cursor.SmoothProjection(), @@ -614,6 +613,150 @@ DrawingManager.Tool.Line = class extends DrawingManager.Tool { } }; +DrawingManager.Tool.Polygon = class extends DrawingManager.Tool { + /** + * Creates a Polygon Tool. + */ + constructor(drawingManager) { + super(drawingManager); + + this.currentPolygon = null; + + this.lastPointTime = 0; + this.minimumUpdate = { + distance: 5, + time: 1000 + }; + } + + /** + * Starts drawing with the tool. + * @param {THREE.Object3D} parent - The parent object to draw in. + * @param {THREE.Vector3} position - The position of the cursor. + */ + startDraw(parent, position) { + this.currentPolygon = { + points: [position.clone()], + meshLine: new MeshLine(), + obj: null, + }; + + this.lastPointTime = Date.now(); + } + + /** + * Updates drawing with the tool. + * @param {THREE.Object3D} parent - The parent object to draw in. + * @param {THREE.Vector3} position - The position of the cursor. + */ + moveDraw(parent, position) { + if (!this.currentPolygon) { + return; + } + const lastPosition = this.currentPolygon.points[this.currentPolygon.points.length - 1]; + const newPosition = position.clone(); + + if (newPosition.clone().sub(lastPosition).length() < this.minimumUpdate.length && Date.now() - this.lastPointTime < this.minimumUpdate.time) { + return; // Return if the cursor hasn't moved far enough and enough time hasn't passed, simplifies path when cursor doesn't move much for a bit + } + this.lastPointTime = Date.now(); + + this.currentPolygon.points.push(newPosition); + if (this.currentPolygon.points.length < 3) { + this.currentPolygon.meshLine.setPoints(this.currentPolygon.points); + } else { + this.currentPolygon.meshLine.setPoints([...this.currentPolygon.points, this.currentPolygon.points[0]]); + } + if (this.currentPolygon.obj) { // Clear previous in-progress drawing + this.currentPolygon.obj.geometry.dispose(); + this.currentPolygon.obj.material.dispose(); + this.currentPolygon.obj.parent.remove(this.currentPolygon.obj); + this.currentPolygon.obj = null; + } + const meshLine = new THREE.Mesh(this.currentPolygon.meshLine, this.meshLineMaterial); + meshLine.raycast = MeshLineRaycast; + this.currentPolygon.obj = meshLine; + parent.add(this.currentPolygon.obj); + } + + /** + * Finishes drawing with the tool. Can be called when tool is not currently drawing. + * @param {THREE.Object3D} parent - The parent object to draw in. + * @param {THREE.Vector3} position - The position of the cursor. + */ + endDraw(parent) { + if (this.currentPolygon && this.currentPolygon.obj) { + const drawingId = `${Math.round(Math.random() * 100000000)}`; + + const serialized = { + tool: 'POLYGON', + points: this.currentPolygon.points, + size: this.size, + color: this.color, + drawingId: drawingId + }; + this.drawFromSerialized(parent, serialized); + + this.currentPolygon.obj.geometry.dispose(); + this.currentPolygon.obj.material.dispose(); + this.currentPolygon.obj.parent.remove(this.currentPolygon.obj); + this.currentPolygon = null; + + const undoEvent = { + type: 'erase', + data: { + drawingId + } + }; + + this.drawingManager.pushUndoEvent(undoEvent); + this.lastPointTime = 0; + } else { + this.currentPolygon = null; + this.lastPointTime = 0; + } + } + + /** + * Creates a drawing from a serialized version. + * @param {THREE.Object3D} parent - The parent object to draw in. + * @param {Object} drawing - The serialized object defining the object to be drawn. + */ + drawFromSerialized(parent, drawing) { + const meshLineMaterial = generateMeshLineMaterial(drawing.size, drawing.color); + + const points = drawing.points.map(point => new THREE.Vector3(point.x, point.y, point.z)); + if (points.length >= 3) { + points.push(points[0]); + } + const meshLine = new MeshLine(); + meshLine.setPoints(points); + const mesh = new THREE.Mesh(meshLine, meshLineMaterial); + mesh.raycast = MeshLineRaycast; + mesh.drawingId = drawing.drawingId; + mesh.serialized = drawing; + parent.add(mesh); + } + + /** + * Sets the drawing size. + * @param {number} size - The size. + */ + setSize(size) { + super.setSize(size); + this.meshLineMaterial = generateMeshLineMaterial(size, this.color); + } + + /** + * Sets the drawing color. + * @param {string} color - The color. + */ + setColor(color) { + super.setColor(color); + this.meshLineMaterial = generateMeshLineMaterial(this.size, color); + } +}; + DrawingManager.Tool.Icon = class extends DrawingManager.Tool { /** * Creates an Icon Tool. diff --git a/tools/spatialDraw/index.html b/tools/spatialDraw/index.html index 9b81195..42e9832 100644 --- a/tools/spatialDraw/index.html +++ b/tools/spatialDraw/index.html @@ -1,5 +1,6 @@ + Spatial Sensor @@ -15,12 +16,14 @@ font-weight: normal; font-style: normal; } + @font-face { font-family: 'Roboto'; src: url('resources/roboto-bold.ttf'); font-weight: bold; font-style: normal; } + #text { position: absolute; left: 0; @@ -39,27 +42,29 @@ cursor: pointer; font-family: "Roboto", "Avenir", "Futura", Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 500%; - color: rgb(45,255,255); + color: rgb(45, 255, 255); } + #text:hover { opacity: 0.75; cursor: pointer; } + #launchIcon { - background-color: rgba(0,0,0,0); + background-color: rgba(0, 0, 0, 0); width: 400px; height: 400px; border-radius: 50px; } - + #uiParent { display: flex; justify-content: start; align-items: center; } - + #ui { - background-color: rgba(0,0,0,0.9); + background-color: rgba(0, 0, 0, 0.9); border-radius: 0 20px 20px 0; width: fit-content; padding: 10px; @@ -69,12 +74,12 @@ align-items: center; transform-origin: left; } - + .circle { border-radius: 100%; margin-top: 10px; } - + .circle:first-child { margin-top: 0; } @@ -83,45 +88,57 @@ width: 100%; height: 100%; } - + .color { width: 44px; height: 44px; } - + .color.active { width: 36px; height: 36px; border: 4px solid white; } - + + .polygon { + display: inline-block; + border-radius: 100%; + width: 36px; + height: 36px; + border: 4px solid slategray; + } + + .polygon.active { + border: 4px solid white; + } + .erase { width: 36px; height: 36px; border: 4px solid slategray; } - + .erase.active { border: 4px solid white; } - + .size { width: 36px; height: 36px; border: 4px solid white; display: none; } - + .size.active { display: initial; } - + .undo { width: 36px; height: 36px; border: 4px solid slategray; } - + .undo.active { border: 4px solid white; } @@ -152,13 +169,14 @@ width: 36px; height: 36px; border: 4px solid slategray; - transform: translateY(0); /* needed to prevent fixed child from positioning relative to #ui*/ + transform: translateY(0); + /* needed to prevent fixed child from positioning relative to #ui*/ } .iconMenu.active { border: 4px solid white; } - + .iconMenuPopout { border-radius: 0 15px 15px 0; padding: 8px; @@ -166,28 +184,28 @@ top: -12px; left: calc(100% + 14px); width: max-content; - background-color: rgba(0,0,0,0.9); + background-color: rgba(0, 0, 0, 0.9); display: none; } - + .iconMenuPopout.active { display: initial; } - + .cursorMenu { margin-top: 10px; } - + .cursor { width: 52px; height: 52px; background-color: darkslategray; } - + .cursor.active { background-color: slategray; } - + .projectionCursor { border-radius: 10px 10px 0 0; } @@ -195,7 +213,7 @@ .offsetCursor { border-radius: 0 0 10px 10px; } - + .cursor svg { width: 100%; height: 100%; @@ -203,29 +221,44 @@ .tool-color-gradient { /*animation: tool-color-gradient-anim 0.3s infinite alternate;*/ - background: linear-gradient(90deg, #000000,#000000, #00ffff, #ee7752, #e73c7e, #23a6d5, #23d5ab,#000000, #000000); + background: linear-gradient(90deg, #000000, #000000, #00ffff, #ee7752, #e73c7e, #23a6d5, #23d5ab, #000000, #000000); background-size: 2000% 200%; animation: gradient 10s infinite; border-radius: 50px; } @keyframes gradient { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } } +
- + - - - + + +
@@ -263,11 +296,19 @@ +
+ + + + +
- + + @@ -278,7 +319,10 @@ - + + @@ -288,8 +332,13 @@ - - + + + + @@ -298,19 +347,29 @@ - - + + + +
- + - - - + + + + + + @@ -319,9 +378,18 @@ - - - + + + + + + @@ -331,17 +399,31 @@ - + + - - + + + + - - - + + + + + + @@ -351,7 +433,8 @@ - + + @@ -361,8 +444,11 @@ - - + + @@ -371,8 +457,12 @@ - - + + + @@ -386,4 +476,5 @@ + diff --git a/tools/spatialDraw/index.js b/tools/spatialDraw/index.js index 0fe9709..a1d9604 100644 --- a/tools/spatialDraw/index.js +++ b/tools/spatialDraw/index.js @@ -1,4 +1,4 @@ -/* global DrawingManager, realGl, gl, proxies */ +/* global SpatialInterface, Envelope, DrawingManager, realGl, gl, proxies */ gl.enableWebGL2 = false; @@ -87,6 +87,8 @@ sizeCircles.forEach((circle, i) => { }); const colorCircles = Array.from(ui.children).filter(child => child.classList.contains('color')); const eraseCircle = Array.from(ui.children).filter(child => child.classList.contains('erase'))[0]; +const polygonCircle = Array.from(ui.children).filter(child => child.classList.contains('polygon'))[0]; + colorCircles.forEach(circle => { circle.style.backgroundColor = circle.dataset.color; circle.addEventListener('pointerdown', e => { @@ -100,6 +102,14 @@ eraseCircle.addEventListener('pointerdown', e => { eraseCircle.classList.add('active'); drawingManager.setEraseMode(true); }); +polygonCircle.addEventListener('pointerdown', e => { + e.stopPropagation(); + if (drawingManager.tool !== drawingManager.toolMap['POLYGON']) { + drawingManager.setTool(drawingManager.toolMap['POLYGON']); + } else { + drawingManager.setTool(drawingManager.toolMap['LINE']); + } +}); const undoCircle = Array.from(ui.children).filter(child => child.classList.contains('undo'))[0]; undoCircle.addEventListener('pointerdown', e => { e.stopPropagation(); @@ -234,9 +244,15 @@ function initDrawingApp() { const toolName = Object.keys(drawingManager.toolMap).find(name => drawingManager.toolMap[name] === tool); if (toolName === 'ICON') { iconMenu.classList.add('active'); + polygonCircle.classList.remove('active'); } else { iconCircles.forEach(iconCircle => iconCircle.classList.remove('active')); iconMenu.classList.remove('active'); + if (toolName === 'POLYGON') { + polygonCircle.classList.add('active'); + } else { + polygonCircle.classList.remove('active'); + } } });