From ad5fb4126a483e53d62f200d32e4148dd5fb0256 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 22 Apr 2024 17:45:54 +0100 Subject: [PATCH 01/10] Duplicate ShapeEditor Ellipse as a start for Point --- src/js/shapeEditorTest.js | 5 + src/js/shape_editor/ellipse.js | 535 ++++++++++++++++++++++++++- src/js/shape_editor/shape_manager.js | 7 +- 3 files changed, 544 insertions(+), 3 deletions(-) diff --git a/src/js/shapeEditorTest.js b/src/js/shapeEditorTest.js index 419863099..3ff0b5b9f 100644 --- a/src/js/shapeEditorTest.js +++ b/src/js/shapeEditorTest.js @@ -247,6 +247,11 @@ $(function() { "y": 260.5, "x": 419}); + shapeManager.addShapeJson({"type": "Point", + "strokeWidth": 2, + "y": 30, + "x": 30}); + var s = shapeManager.addShapeJson({"type": "Line", "strokeColor": "#00ff00", "strokeWidth": 2, diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index d736fd8fd..8b0fb21fd 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -25,6 +25,537 @@ import Raphael from "raphael"; +const POINT_RADIUS = 5; +var Point = function Point(options) { + var self = this; + this.manager = options.manager; + this.paper = options.paper; + + if (options.id) { + this._id = options.id; + } else { + this._id = this.manager.getRandomId(); + } + this._x = options.x; + this._y = options.y; + this._radiusX = POINT_RADIUS; + this._radiusY = POINT_RADIUS; + this._rotation = options.rotation || 0; + + // We handle transform matrix by creating this.Matrix + // This is used as a one-off transform of the handles positions + // when they are created. This then updates the _x, _y, _radiusX, _radiusY & rotation + // of the Point itself (see below) + if (options.transform && options.transform.startsWith("matrix")) { + var tt = options.transform + .replace("matrix(", "") + .replace(")", "") + .split(" "); + var a1 = parseFloat(tt[0]); + var a2 = parseFloat(tt[1]); + var b1 = parseFloat(tt[2]); + var b2 = parseFloat(tt[3]); + var c1 = parseFloat(tt[4]); + var c2 = parseFloat(tt[5]); + this.Matrix = Raphael.matrix(a1, a2, b1, b2, c1, c2); + } + + if (this._radiusX === 0 || this._radiusY === 0) { + this._yxRatio = 0.5; + } else { + this._yxRatio = this._radiusY / this._radiusX; + } + + this._strokeColor = options.strokeColor; + this._strokeWidth = options.strokeWidth || 2; + this._selected = false; + this._zoomFraction = 1; + if (options.zoom) { + this._zoomFraction = options.zoom / 100; + } + if (options.area) { + this._area = options.area; + } else { + this._area = this._radiusX * this._radiusY * Math.PI; + } + this.handle_wh = 6; + + this.element = this.paper.ellipse(); + this.element.attr({ "fill-opacity": 0.01, fill: "#fff", cursor: "pointer" }); + + // Drag handling of ellipse + if (this.manager.canEdit) { + this.element.drag( + function (dx, dy) { + // DRAG, update location and redraw + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + + var offsetX = dx - this.prevX; + var offsetY = dy - this.prevY; + this.prevX = dx; + this.prevY = dy; + + // Manager handles move and redraw + self.manager.moveSelectedShapes(offsetX, offsetY, true); + return false; + }, + function () { + // START drag: note the start location + self._handleMousedown(); + this.prevX = 0; + this.prevY = 0; + return false; + }, + function () { + // STOP + // notify changed if moved + if (this.prevX !== 0 || this.prevY !== 0) { + self.manager.notifySelectedShapesChanged(); + } + return false; + } + ); + } + + // create handles, applying this.Matrix if set + this.createHandles(); + // update x, y, radiusX, radiusY & rotation + // If we have Matrix, recalculate width/height ratio based on all handles + var resizeWidth = !!this.Matrix; + this.updateShapeFromHandles(resizeWidth); + // and draw the Ellipse + this.drawShape(); +}; + +Point.prototype.toJson = function toJson() { + var rv = { + type: "Ellipse", + x: this._x, + y: this._y, + radiusX: this._radiusX, + radiusY: this._radiusY, + area: this._radiusX * this._radiusY * Math.PI, + rotation: this._rotation, + strokeWidth: this._strokeWidth, + strokeColor: this._strokeColor, + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +Point.prototype.compareCoords = function compareCoords(json) { + var selfJson = this.toJson(), + match = true; + if (json.type !== selfJson.type) { + return false; + } + ["x", "y", "radiusX", "radiusY", "rotation"].forEach(function (c) { + if (Math.round(json[c]) !== Math.round(selfJson[c])) { + match = false; + } + }); + return match; +}; + +// Useful for pasting json with an offset +Point.prototype.offsetCoords = function offsetCoords(json, dx, dy) { + json.x = json.x + dx; + json.y = json.y + dy; + return json; +}; + +// Shift this shape by dx and dy +Point.prototype.offsetShape = function offsetShape(dx, dy) { + this._x = this._x + dx; + this._y = this._y + dy; + this.drawShape(); +}; + +// handle start of drag by selecting this shape +// if not already selected +Point.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Point.prototype.setColor = function setColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Point.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Point.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Point.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Point.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Point.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Point.prototype.intersectRegion = function intersectRegion(region) { + var path = this.manager.regionToPath(region, this._zoomFraction * 100); + var f = this._zoomFraction, + x = parseInt(this._x * f, 10), + y = parseInt(this._y * f, 10); + + if (Raphael.isPointInsidePath(path, x, y)) { + return true; + } + var path2 = this.getPath(), + i = Raphael.pathIntersection(path, path2); + return i.length > 0; +}; + +Point.prototype.getPath = function getPath() { + // Adapted from https://github.com/poilu/raphael-boolean + var a = this.element.attrs, + radiusX = a.radiusX, + radiusY = a.radiusY, + cornerPoints = [ + [a.x - radiusX, a.y - radiusY], + [a.x + radiusX, a.y - radiusY], + [a.x + radiusX, a.y + radiusY], + [a.x - radiusX, a.y + radiusY], + ], + path = []; + var radiusShift = [ + [ + [0, 1], + [1, 0], + ], + [ + [-1, 0], + [0, 1], + ], + [ + [0, -1], + [-1, 0], + ], + [ + [1, 0], + [0, -1], + ], + ]; + + //iterate all corners + for (var i = 0; i <= 3; i++) { + //insert starting point + if (i === 0) { + path.push(["M", cornerPoints[0][0], cornerPoints[0][1] + radiusY]); + } + + //insert "curveto" (radius factor .446 is taken from Inkscape) + var c1 = [ + cornerPoints[i][0] + radiusShift[i][0][0] * radiusX * 0.446, + cornerPoints[i][1] + radiusShift[i][0][1] * radiusY * 0.446, + ]; + var c2 = [ + cornerPoints[i][0] + radiusShift[i][1][0] * radiusX * 0.446, + cornerPoints[i][1] + radiusShift[i][1][1] * radiusY * 0.446, + ]; + var p2 = [ + cornerPoints[i][0] + radiusShift[i][1][0] * radiusX, + cornerPoints[i][1] + radiusShift[i][1][1] * radiusY, + ]; + path.push(["C", c1[0], c1[1], c2[0], c2[1], p2[0], p2[1]]); + } + path.push(["Z"]); + path = path.join(",").replace(/,?([achlmqrstvxz]),?/gi, "$1"); + + if (this._rotation !== 0) { + path = Raphael.transformPath(path, "r" + this._rotation); + } + return path; +}; + +Point.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Point.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Point.prototype.updateHandle = function updateHandle( + handleId, + x, + y, + shiftKey +) { + // Refresh the handle coordinates, then update the specified handle + // using MODEL coordinates + this._handleIds = this.getHandleCoords(); + var h = this._handleIds[handleId]; + h.x = x; + h.y = y; + var resizeWidth = handleId === "left" || handleId === "right"; + this.updateShapeFromHandles(resizeWidth, shiftKey); +}; + +Point.prototype.updateShapeFromHandles = function updateShapeFromHandles( + resizeWidth, + shiftKey +) { + var hh = this._handleIds, + lengthX = hh.end.x - hh.start.x, + lengthY = hh.end.y - hh.start.y, + widthX = hh.left.x - hh.right.x, + widthY = hh.left.y - hh.right.y, + rot; + // Use the 'start' and 'end' handles to get rotation and length + if (lengthX === 0) { + this._rotation = 90; + } else if (lengthX > 0) { + rot = Math.atan(lengthY / lengthX); + this._rotation = Raphael.deg(rot); + } else if (lengthX < 0) { + rot = Math.atan(lengthY / lengthX); + this._rotation = 180 + Raphael.deg(rot); + } + + // centre is half-way between 'start' and 'end' handles + this._x = (hh.start.x + hh.end.x) / 2; + this._y = (hh.start.y + hh.end.y) / 2; + // Radius-x is half of distance between handles + this._radiusX = Math.sqrt(lengthX * lengthX + lengthY * lengthY) / 2; + // Radius-y may depend on handles OR on x/y ratio + if (resizeWidth) { + this._radiusY = Math.sqrt(widthX * widthX + widthY * widthY) / 2; + this._yxRatio = this._radiusY / this._radiusX; + } else { + if (shiftKey) { + this._yxRatio = 1; + } + this._radiusY = this._yxRatio * this._radiusX; + } + this._area = this._radiusX * this._radiusY * Math.PI; + + this.drawShape(); +}; + +Point.prototype.drawShape = function drawShape() { + var strokeColor = this._strokeColor, + strokeW = this._strokeWidth; + + var f = this._zoomFraction, + x = this._x * f, + y = this._y * f, + radiusX = this._radiusX * f, + radiusY = this._radiusY * f; + + this.element.attr({ + cx: x, + cy: y, + rx: radiusX, + ry: radiusY, + stroke: strokeColor, + "stroke-width": strokeW, + }); + this.element.transform("r" + this._rotation); + + if (this.isSelected()) { + this.handles.show().toFront(); + } else { + this.handles.hide(); + } + + // handles have been updated (model coords) + this._handleIds = this.getHandleCoords(); + var hnd, h_id, hx, hy; + for (var h = 0, l = this.handles.length; h < l; h++) { + hnd = this.handles[h]; + h_id = hnd.h_id; + hx = this._handleIds[h_id].x * this._zoomFraction; + hy = this._handleIds[h_id].y * this._zoomFraction; + hnd.attr({ x: hx - this.handle_wh / 2, y: hy - this.handle_wh / 2 }); + } +}; + +Point.prototype.setSelected = function setSelected(selected) { + this._selected = !!selected; + this.drawShape(); +}; + +Point.prototype.createHandles = function createHandles() { + // ---- Create Handles ----- + + // NB: handleIds are used to calculate ellipse coords + // so handledIds are scaled to MODEL coords, not zoomed. + this._handleIds = this.getHandleCoords(); + + var self = this, + // map of centre-points for each handle + handleAttrs = { + stroke: "#4b80f9", + fill: "#fff", + cursor: "move", + "fill-opacity": 1.0, + }; + + // draw handles + self.handles = this.paper.set(); + var _handle_drag = function () { + return function (dx, dy, mouseX, mouseY, event) { + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + // on DRAG... + var absX = dx + this.ox, + absY = dy + this.oy; + self.updateHandle(this.h_id, absX, absY, event.shiftKey); + return false; + }; + }; + var _handle_drag_start = function () { + return function () { + // START drag: simply note the location we started + // we scale by zoom to get the 'model' coordinates + this.ox = (this.attr("x") + this.attr("width") / 2) / self._zoomFraction; + this.oy = (this.attr("y") + this.attr("height") / 2) / self._zoomFraction; + return false; + }; + }; + var _handle_drag_end = function () { + return function () { + // simply notify manager that shape has changed + self.manager.notifyShapesChanged([self]); + return false; + }; + }; + + var hsize = this.handle_wh, + hx, + hy, + handle; + for (var key in this._handleIds) { + hx = this._handleIds[key].x; + hy = this._handleIds[key].y; + // If we have a transformation matrix, apply it... + if (this.Matrix) { + var matrixStr = this.Matrix.toTransformString(); + // Matrix that only contains rotation and translation + // E.g. t111.894472287,-140.195845758r32.881,0,0 Will be handled correctly: + // Resulting handles position and x,y radii will be calculated + // so we don't need to apply transform to ellipse itself + // BUT, if we have other transforms such as skew, we can't do this. + // Best to just show warning if Raphael can't resolve matrix to simpler transforms: + // E.g. m2.39,-0.6,2.1,0.7,-1006,153 + if (matrixStr.indexOf("m") > -1) { + console.log( + "Matrix only supports rotation & translation. " + + matrixStr + + " may contain skew for shape: ", + this.toJson() + ); + } + var mx = this.Matrix.x(hx, hy); + var my = this.Matrix.y(hx, hy); + hx = mx; + hy = my; + // update the source coordinates + this._handleIds[key].x = hx; + this._handleIds[key].y = hy; + } + handle = this.paper.rect(hx - hsize / 2, hy - hsize / 2, hsize, hsize); + handle.attr({ cursor: "move" }); + handle.h_id = key; + handle.line = self; + + if (this.manager.canEdit) { + handle.drag(_handle_drag(), _handle_drag_start(), _handle_drag_end()); + } + self.handles.push(handle); + } + + self.handles.attr(handleAttrs).hide(); // show on selection +}; + +Point.prototype.getHandleCoords = function getHandleCoords() { + // Returns MODEL coordinates (not zoom coordinates) + var rot = Raphael.rad(this._rotation), + x = this._x, + y = this._y, + radiusX = this._radiusX, + radiusY = this._radiusY, + startX = x - Math.cos(rot) * radiusX, + startY = y - Math.sin(rot) * radiusX, + endX = x + Math.cos(rot) * radiusX, + endY = y + Math.sin(rot) * radiusX, + leftX = x + Math.sin(rot) * radiusY, + leftY = y - Math.cos(rot) * radiusY, + rightX = x - Math.sin(rot) * radiusY, + rightY = y + Math.cos(rot) * radiusY; + + return { + start: { x: startX, y: startY }, + end: { x: endX, y: endY }, + left: { x: leftX, y: leftY }, + right: { x: rightX, y: rightY }, + }; +}; + +// Class for creating Point. +var CreatePoint = function CreatePoint(options) { + this.paper = options.paper; + this.manager = options.manager; +}; + +CreatePoint.prototype.startDrag = function startDrag(startX, startY) { + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + + this.point = new Point({ + manager: this.manager, + paper: this.paper, + x: startX, + y: startY, + radiusX: 0, + radiusY: 0, + area: 0, + rotation: 0, + strokeWidth: strokeWidth, + zoom: zoom, + strokeColor: strokeColor, + }); +}; + +CreatePoint.prototype.drag = function drag(dragX, dragY, shiftKey) { + this.point.updateHandle("end", dragX, dragY, shiftKey); +}; + +CreatePoint.prototype.stopDrag = function stopDrag() { + // Don't create ellipse of zero size (click, without drag) + var coords = this.point.toJson(); + if (coords.radiusX < 2) { + this.point.destroy(); + delete this.ellipse; + return; + } + // on the 'new:shape' trigger, this shape will already be selected + this.point.setSelected(true); + this.manager.addShape(this.ellipse); +}; + + var Ellipse = function Ellipse(options) { var self = this; this.manager = options.manager; @@ -511,7 +1042,7 @@ Ellipse.prototype.getHandleCoords = function getHandleCoords() { }; }; -// Class for creating Lines. +// Class for creating Ellipse. var CreateEllipse = function CreateEllipse(options) { this.paper = options.paper; this.manager = options.manager; @@ -554,4 +1085,4 @@ CreateEllipse.prototype.stopDrag = function stopDrag() { this.manager.addShape(this.ellipse); }; -export { CreateEllipse, Ellipse }; +export { CreatePoint, Point, CreateEllipse, Ellipse }; diff --git a/src/js/shape_editor/shape_manager.js b/src/js/shape_editor/shape_manager.js index 78d8daa20..775946474 100644 --- a/src/js/shape_editor/shape_manager.js +++ b/src/js/shape_editor/shape_manager.js @@ -28,7 +28,7 @@ import $ from "jquery"; import { CreateRect, Rect } from "./rect"; import { CreateLine, Line, CreateArrow, Arrow } from "./line"; -import { CreateEllipse, Ellipse } from "./ellipse"; +import { CreatePoint, Point, CreateEllipse, Ellipse } from "./ellipse"; import { Polygon, Polyline } from "./polygon"; var ShapeManager = function ShapeManager(elementId, width, height, options) { @@ -412,6 +412,11 @@ ShapeManager.prototype.createShapeJson = function createShapeJson(jsonShape) { options.transform = s.transform; options.area = s.radiusX * s.radiusY * Math.PI; newShape = new Ellipse(options); + } else if (s.type === "Point") { + options.x = s.x; + options.y = s.y; + options.area = 0; + newShape = new Point(options); } else if (s.type === "Rectangle") { options.x = s.x; options.y = s.y; From 3f00da2a18b215ee8c29c7f4f0b6a43a083e776d Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 22 Apr 2024 23:11:23 +0100 Subject: [PATCH 02/10] Simplify Point, remove handle drag behaviour etc --- src/js/shape_editor/ellipse.js | 102 ++++++++------------------------- 1 file changed, 24 insertions(+), 78 deletions(-) diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index 8b0fb21fd..957b4d967 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -46,19 +46,19 @@ var Point = function Point(options) { // This is used as a one-off transform of the handles positions // when they are created. This then updates the _x, _y, _radiusX, _radiusY & rotation // of the Point itself (see below) - if (options.transform && options.transform.startsWith("matrix")) { - var tt = options.transform - .replace("matrix(", "") - .replace(")", "") - .split(" "); - var a1 = parseFloat(tt[0]); - var a2 = parseFloat(tt[1]); - var b1 = parseFloat(tt[2]); - var b2 = parseFloat(tt[3]); - var c1 = parseFloat(tt[4]); - var c2 = parseFloat(tt[5]); - this.Matrix = Raphael.matrix(a1, a2, b1, b2, c1, c2); - } + // if (options.transform && options.transform.startsWith("matrix")) { + // var tt = options.transform + // .replace("matrix(", "") + // .replace(")", "") + // .split(" "); + // var a1 = parseFloat(tt[0]); + // var a2 = parseFloat(tt[1]); + // var b1 = parseFloat(tt[2]); + // var b2 = parseFloat(tt[3]); + // var c1 = parseFloat(tt[4]); + // var c2 = parseFloat(tt[5]); + // this.Matrix = Raphael.matrix(a1, a2, b1, b2, c1, c2); + // } if (this._radiusX === 0 || this._radiusY === 0) { this._yxRatio = 0.5; @@ -83,7 +83,7 @@ var Point = function Point(options) { this.element = this.paper.ellipse(); this.element.attr({ "fill-opacity": 0.01, fill: "#fff", cursor: "pointer" }); - // Drag handling of ellipse + // Drag handling of point if (this.manager.canEdit) { this.element.drag( function (dx, dy) { @@ -407,39 +407,13 @@ Point.prototype.createHandles = function createHandles() { handleAttrs = { stroke: "#4b80f9", fill: "#fff", - cursor: "move", + cursor: "default", "fill-opacity": 1.0, }; - // draw handles + // draw handles - Can't drag handles to resize, but they are useful + // simply to indicate that the Point is selected self.handles = this.paper.set(); - var _handle_drag = function () { - return function (dx, dy, mouseX, mouseY, event) { - dx = dx / self._zoomFraction; - dy = dy / self._zoomFraction; - // on DRAG... - var absX = dx + this.ox, - absY = dy + this.oy; - self.updateHandle(this.h_id, absX, absY, event.shiftKey); - return false; - }; - }; - var _handle_drag_start = function () { - return function () { - // START drag: simply note the location we started - // we scale by zoom to get the 'model' coordinates - this.ox = (this.attr("x") + this.attr("width") / 2) / self._zoomFraction; - this.oy = (this.attr("y") + this.attr("height") / 2) / self._zoomFraction; - return false; - }; - }; - var _handle_drag_end = function () { - return function () { - // simply notify manager that shape has changed - self.manager.notifyShapesChanged([self]); - return false; - }; - }; var hsize = this.handle_wh, hx, @@ -448,40 +422,10 @@ Point.prototype.createHandles = function createHandles() { for (var key in this._handleIds) { hx = this._handleIds[key].x; hy = this._handleIds[key].y; - // If we have a transformation matrix, apply it... - if (this.Matrix) { - var matrixStr = this.Matrix.toTransformString(); - // Matrix that only contains rotation and translation - // E.g. t111.894472287,-140.195845758r32.881,0,0 Will be handled correctly: - // Resulting handles position and x,y radii will be calculated - // so we don't need to apply transform to ellipse itself - // BUT, if we have other transforms such as skew, we can't do this. - // Best to just show warning if Raphael can't resolve matrix to simpler transforms: - // E.g. m2.39,-0.6,2.1,0.7,-1006,153 - if (matrixStr.indexOf("m") > -1) { - console.log( - "Matrix only supports rotation & translation. " + - matrixStr + - " may contain skew for shape: ", - this.toJson() - ); - } - var mx = this.Matrix.x(hx, hy); - var my = this.Matrix.y(hx, hy); - hx = mx; - hy = my; - // update the source coordinates - this._handleIds[key].x = hx; - this._handleIds[key].y = hy; - } handle = this.paper.rect(hx - hsize / 2, hy - hsize / 2, hsize, hsize); handle.attr({ cursor: "move" }); handle.h_id = key; handle.line = self; - - if (this.manager.canEdit) { - handle.drag(_handle_drag(), _handle_drag_start(), _handle_drag_end()); - } self.handles.push(handle); } @@ -490,11 +434,13 @@ Point.prototype.createHandles = function createHandles() { Point.prototype.getHandleCoords = function getHandleCoords() { // Returns MODEL coordinates (not zoom coordinates) + let margin = 2; + var rot = Raphael.rad(this._rotation), x = this._x, y = this._y, - radiusX = this._radiusX, - radiusY = this._radiusY, + radiusX = this._radiusX + margin, + radiusY = this._radiusY + margin, startX = x - Math.cos(rot) * radiusX, startY = y - Math.sin(rot) * radiusX, endX = x + Math.cos(rot) * radiusX, @@ -528,8 +474,8 @@ CreatePoint.prototype.startDrag = function startDrag(startX, startY) { paper: this.paper, x: startX, y: startY, - radiusX: 0, - radiusY: 0, + radiusX: POINT_RADIUS, + radiusY: POINT_RADIUS, area: 0, rotation: 0, strokeWidth: strokeWidth, @@ -1007,7 +953,7 @@ Ellipse.prototype.createHandles = function createHandles() { handle = this.paper.rect(hx - hsize / 2, hy - hsize / 2, hsize, hsize); handle.attr({ cursor: "move" }); handle.h_id = key; - handle.line = self; + // handle.line = self; if (this.manager.canEdit) { handle.drag(_handle_drag(), _handle_drag_start(), _handle_drag_end()); From 5aced7490ae8ea1efc03672c56695a780672dfab Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 23 Apr 2024 15:40:41 +0100 Subject: [PATCH 03/10] Support creation of Points --- src/js/shape_editor/ellipse.js | 4 ++-- src/js/shape_editor/shape_manager.js | 5 +++-- src/shapeEditorTest.html | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index 957b4d967..a7a51686f 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -485,7 +485,7 @@ CreatePoint.prototype.startDrag = function startDrag(startX, startY) { }; CreatePoint.prototype.drag = function drag(dragX, dragY, shiftKey) { - this.point.updateHandle("end", dragX, dragY, shiftKey); + // no drag behaviour on Point creation }; CreatePoint.prototype.stopDrag = function stopDrag() { @@ -498,7 +498,7 @@ CreatePoint.prototype.stopDrag = function stopDrag() { } // on the 'new:shape' trigger, this shape will already be selected this.point.setSelected(true); - this.manager.addShape(this.ellipse); + this.manager.addShape(this.point); }; diff --git a/src/js/shape_editor/shape_manager.js b/src/js/shape_editor/shape_manager.js index 775946474..cac45c517 100644 --- a/src/js/shape_editor/shape_manager.js +++ b/src/js/shape_editor/shape_manager.js @@ -36,7 +36,7 @@ var ShapeManager = function ShapeManager(elementId, width, height, options) { options = options || {}; // Keep track of state, strokeColor etc - this.STATES = ["SELECT", "RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"]; + this.STATES = ["SELECT", "RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON", "POINT"]; this._state = "SELECT"; this._strokeColor = "#ff0000"; this._strokeWidth = 2; @@ -87,6 +87,7 @@ var ShapeManager = function ShapeManager(elementId, width, height, options) { ELLIPSE: new CreateEllipse({ manager: this, paper: this.paper }), LINE: new CreateLine({ manager: this, paper: this.paper }), ARROW: new CreateArrow({ manager: this, paper: this.paper }), + POINT: new CreatePoint({ manager: this, paper: this.paper }), }; this.createShape = this.shapeFactories.LINE; @@ -172,7 +173,7 @@ ShapeManager.prototype.setState = function setState(state) { return; } // When creating shapes, cover existing shapes with newShapeBg - var shapes = ["RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"]; + var shapes = ["RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON", "POINT"]; if (shapes.indexOf(state) > -1) { this.newShapeBg.toFront(); this.newShapeBg.attr({ cursor: "crosshair" }); diff --git a/src/shapeEditorTest.html b/src/shapeEditorTest.html index be90dc573..e23c17799 100644 --- a/src/shapeEditorTest.html +++ b/src/shapeEditorTest.html @@ -38,7 +38,9 @@ Line:
Arrow: - +
+ Point: +
From b154402fbcd5e7f3eab1f502a76339c6a05698d5 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 23 Apr 2024 18:03:06 +0100 Subject: [PATCH 04/10] Add Point button to ROI edit dialog --- src/css/figure.css | 3 +++ src/images/point-icon-24.png | Bin 0 -> 2477 bytes src/templates/shapes/shape_toolbar.template.html | 3 +++ 3 files changed, 6 insertions(+) create mode 100644 src/images/point-icon-24.png diff --git a/src/css/figure.css b/src/css/figure.css index 9ae08c934..1f951a2ed 100644 --- a/src/css/figure.css +++ b/src/css/figure.css @@ -1259,6 +1259,9 @@ .ellipse-icon{ background-image: url("../images/ellipse-icon-16.png"); } + .point-icon{ + background-image: url("../images/point-icon-24.png"); + } .polygon-icon{ background-image: url("../images/polygon-icon-16.png"); } diff --git a/src/images/point-icon-24.png b/src/images/point-icon-24.png new file mode 100644 index 0000000000000000000000000000000000000000..3d5c1d46f506aac6cf4ea76f269825cefbf24bea GIT binary patch literal 2477 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1$&97 zuPgg?HX$Azmd2mE0)RrYC9V-A&iT2ysd*&~&PAz-C8;S2<(VZJ3LzP(3hti10q;{( z^MK|ld%8G=cRQ2526?f(*qSpmV-$VO%tmbuR6L%GA`G^A`~Q#%*GWQFg3X3!Q!|q zL)U~?9nFIX-8JZZ^i%|wr$#DbATl3_!fnOEASyC|`mlvFv3jvsi(3erMYvT1WoRDB hL*D|t;WsJ?3V|! +
From 82f12dc78b15d251eadaf4ff5cb06371ee8eaa84 Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 25 Apr 2024 16:34:15 +0100 Subject: [PATCH 05/10] Support adding Points from OMERO ROIs --- src/css/figure.css | 1 + src/js/shape_editor/ellipse.js | 8 ++------ src/js/views/roi_loader_view.js | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/css/figure.css b/src/css/figure.css index 1f951a2ed..c6fca3894 100644 --- a/src/css/figure.css +++ b/src/css/figure.css @@ -1261,6 +1261,7 @@ } .point-icon{ background-image: url("../images/point-icon-24.png"); + background-position: center; } .polygon-icon{ background-image: url("../images/polygon-icon-16.png"); diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index a7a51686f..75987cd09 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -130,13 +130,9 @@ var Point = function Point(options) { Point.prototype.toJson = function toJson() { var rv = { - type: "Ellipse", + type: "Point", x: this._x, y: this._y, - radiusX: this._radiusX, - radiusY: this._radiusY, - area: this._radiusX * this._radiusY * Math.PI, - rotation: this._rotation, strokeWidth: this._strokeWidth, strokeColor: this._strokeColor, }; @@ -152,7 +148,7 @@ Point.prototype.compareCoords = function compareCoords(json) { if (json.type !== selfJson.type) { return false; } - ["x", "y", "radiusX", "radiusY", "rotation"].forEach(function (c) { + ["x", "y"].forEach(function (c) { if (Math.round(json[c]) !== Math.round(selfJson[c])) { match = false; } diff --git a/src/js/views/roi_loader_view.js b/src/js/views/roi_loader_view.js index 6022b5ede..04a617059 100644 --- a/src/js/views/roi_loader_view.js +++ b/src/js/views/roi_loader_view.js @@ -32,6 +32,7 @@ var RoiLoaderView = Backbone.View.extend({ 'Line': 'line-icon', 'Arrow': 'arrow-icon', 'Polygon': 'polygon-icon', + 'Point': 'point-icon', 'Polyline': 'polyline-icon'}, addOmeroShape: function(event) { From b69b2f66fb34563118e04ab71f26dde1c3b8d862 Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 26 Apr 2024 10:26:09 +0100 Subject: [PATCH 06/10] Figure export script fixes for Points --- .../scripts/omero/figure_scripts/Figure_To_Pdf.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py index 406242e87..c15ddcfb7 100644 --- a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py +++ b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py @@ -465,7 +465,7 @@ def draw_ellipse(self, shape): cy = self.page_height - c['y'] rx = shape['radiusX'] * self.scale ry = shape['radiusY'] * self.scale - rotation = (shape['rotation'] + self.panel['rotation']) * -1 + rotation = (shape.get('rotation', 0) + self.panel['rotation']) * -1 r, g, b, a = self.get_rgba(shape['strokeColor']) self.canvas.setStrokeColorRGB(r, g, b, alpha=a) @@ -761,7 +761,7 @@ def draw_ellipse(self, shape): cy = ctr['y'] rx = self.scale * shape['radiusX'] ry = self.scale * shape['radiusY'] - rotation = (shape['rotation'] + self.panel['rotation']) * -1 + rotation = (shape.get('rotation', 0) + self.panel['rotation']) * -1 width = int((rx * 2) + w) height = int((ry * 2) + w) @@ -772,7 +772,9 @@ def draw_ellipse(self, shape): rgba = ShapeToPdfExport.get_rgba_int(shape['strokeColor']) ellipse_draw.ellipse((0, 0, width, height), fill=rgba) rgba = self.get_rgba_int(shape.get('fillColor', '#00000000')) - ellipse_draw.ellipse((w, w, width - w, height - w), fill=rgba) + # when rx is close to zero (for a Point, scaled down) we don't need inner ellipse + if (width - w) >= w: + ellipse_draw.ellipse((w, w, width - w, height - w), fill=rgba) temp_ellipse = temp_ellipse.rotate(rotation, resample=Image.BICUBIC, expand=True) # Use ellipse as mask, so transparent part is not pasted From 8b004a5892615a104e0151c17620e96e2c72d81c Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 29 Apr 2024 14:14:29 +0100 Subject: [PATCH 07/10] flake8 fix --- omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py index 33773b9b3..508a264fe 100644 --- a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py +++ b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py @@ -772,7 +772,7 @@ def draw_ellipse(self, shape): rgba = ShapeToPdfExport.get_rgba_int(shape['strokeColor']) ellipse_draw.ellipse((0, 0, width, height), fill=rgba) rgba = self.get_rgba_int(shape.get('fillColor', '#00000000')) - # when rx is close to zero (for a Point, scaled down) we don't need inner ellipse + # when rx is ~zero (for a Point, scaled down) don't need inner ellipse if (width - w) >= w: ellipse_draw.ellipse((w, w, width - w, height - w), fill=rgba) temp_ellipse = temp_ellipse.rotate(rotation, resample=Image.BICUBIC, From 04769d9ac08e9d513ae9306a3eff02399b908feb Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 30 Apr 2024 13:13:59 +0100 Subject: [PATCH 08/10] Fix Copy-and-Paste of Points between panels --- src/js/models/panel_model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/models/panel_model.js b/src/js/models/panel_model.js index ee207fb30..fdb27d6e0 100644 --- a/src/js/models/panel_model.js +++ b/src/js/models/panel_model.js @@ -170,7 +170,7 @@ return true; } var points; - if (shape.type === "Ellipse") { + if (shape.type === "Ellipse" || shape.type === "Point") { points = [[shape.cx, shape.cy]]; } else if (shape.type === "Rectangle") { points = [[shape.x, shape.y], From 3179811fd0a1d65e2dc5f56013cf92e086da24ab Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 30 Apr 2024 13:15:26 +0100 Subject: [PATCH 09/10] Simplify intersectRegion() for Point --- src/js/shape_editor/ellipse.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index 75987cd09..2047aff5f 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -212,12 +212,7 @@ Point.prototype.intersectRegion = function intersectRegion(region) { x = parseInt(this._x * f, 10), y = parseInt(this._y * f, 10); - if (Raphael.isPointInsidePath(path, x, y)) { - return true; - } - var path2 = this.getPath(), - i = Raphael.pathIntersection(path, path2); - return i.length > 0; + return Raphael.isPointInsidePath(path, x, y); }; Point.prototype.getPath = function getPath() { From 41b3686e00f764da75b8ad9697f20488a5163b9d Mon Sep 17 00:00:00 2001 From: William Moore Date: Sun, 15 Sep 2024 22:33:51 +0100 Subject: [PATCH 10/10] Move shape_editor Point code to separate point.js --- src/js/shape_editor/ellipse.js | 470 +------------------------- src/js/shape_editor/point.js | 477 +++++++++++++++++++++++++++ src/js/shape_editor/shape_manager.js | 3 +- 3 files changed, 480 insertions(+), 470 deletions(-) create mode 100644 src/js/shape_editor/point.js diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index 2047aff5f..7823cf3df 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -25,474 +25,6 @@ import Raphael from "raphael"; -const POINT_RADIUS = 5; -var Point = function Point(options) { - var self = this; - this.manager = options.manager; - this.paper = options.paper; - - if (options.id) { - this._id = options.id; - } else { - this._id = this.manager.getRandomId(); - } - this._x = options.x; - this._y = options.y; - this._radiusX = POINT_RADIUS; - this._radiusY = POINT_RADIUS; - this._rotation = options.rotation || 0; - - // We handle transform matrix by creating this.Matrix - // This is used as a one-off transform of the handles positions - // when they are created. This then updates the _x, _y, _radiusX, _radiusY & rotation - // of the Point itself (see below) - // if (options.transform && options.transform.startsWith("matrix")) { - // var tt = options.transform - // .replace("matrix(", "") - // .replace(")", "") - // .split(" "); - // var a1 = parseFloat(tt[0]); - // var a2 = parseFloat(tt[1]); - // var b1 = parseFloat(tt[2]); - // var b2 = parseFloat(tt[3]); - // var c1 = parseFloat(tt[4]); - // var c2 = parseFloat(tt[5]); - // this.Matrix = Raphael.matrix(a1, a2, b1, b2, c1, c2); - // } - - if (this._radiusX === 0 || this._radiusY === 0) { - this._yxRatio = 0.5; - } else { - this._yxRatio = this._radiusY / this._radiusX; - } - - this._strokeColor = options.strokeColor; - this._strokeWidth = options.strokeWidth || 2; - this._selected = false; - this._zoomFraction = 1; - if (options.zoom) { - this._zoomFraction = options.zoom / 100; - } - if (options.area) { - this._area = options.area; - } else { - this._area = this._radiusX * this._radiusY * Math.PI; - } - this.handle_wh = 6; - - this.element = this.paper.ellipse(); - this.element.attr({ "fill-opacity": 0.01, fill: "#fff", cursor: "pointer" }); - - // Drag handling of point - if (this.manager.canEdit) { - this.element.drag( - function (dx, dy) { - // DRAG, update location and redraw - dx = dx / self._zoomFraction; - dy = dy / self._zoomFraction; - - var offsetX = dx - this.prevX; - var offsetY = dy - this.prevY; - this.prevX = dx; - this.prevY = dy; - - // Manager handles move and redraw - self.manager.moveSelectedShapes(offsetX, offsetY, true); - return false; - }, - function () { - // START drag: note the start location - self._handleMousedown(); - this.prevX = 0; - this.prevY = 0; - return false; - }, - function () { - // STOP - // notify changed if moved - if (this.prevX !== 0 || this.prevY !== 0) { - self.manager.notifySelectedShapesChanged(); - } - return false; - } - ); - } - - // create handles, applying this.Matrix if set - this.createHandles(); - // update x, y, radiusX, radiusY & rotation - // If we have Matrix, recalculate width/height ratio based on all handles - var resizeWidth = !!this.Matrix; - this.updateShapeFromHandles(resizeWidth); - // and draw the Ellipse - this.drawShape(); -}; - -Point.prototype.toJson = function toJson() { - var rv = { - type: "Point", - x: this._x, - y: this._y, - strokeWidth: this._strokeWidth, - strokeColor: this._strokeColor, - }; - if (this._id) { - rv.id = this._id; - } - return rv; -}; - -Point.prototype.compareCoords = function compareCoords(json) { - var selfJson = this.toJson(), - match = true; - if (json.type !== selfJson.type) { - return false; - } - ["x", "y"].forEach(function (c) { - if (Math.round(json[c]) !== Math.round(selfJson[c])) { - match = false; - } - }); - return match; -}; - -// Useful for pasting json with an offset -Point.prototype.offsetCoords = function offsetCoords(json, dx, dy) { - json.x = json.x + dx; - json.y = json.y + dy; - return json; -}; - -// Shift this shape by dx and dy -Point.prototype.offsetShape = function offsetShape(dx, dy) { - this._x = this._x + dx; - this._y = this._y + dy; - this.drawShape(); -}; - -// handle start of drag by selecting this shape -// if not already selected -Point.prototype._handleMousedown = function _handleMousedown() { - if (!this._selected) { - this.manager.selectShapes([this]); - } -}; - -Point.prototype.setColor = function setColor(strokeColor) { - this._strokeColor = strokeColor; - this.drawShape(); -}; - -Point.prototype.getStrokeColor = function getStrokeColor() { - return this._strokeColor; -}; - -Point.prototype.setStrokeColor = function setStrokeColor(strokeColor) { - this._strokeColor = strokeColor; - this.drawShape(); -}; - -Point.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { - this._strokeWidth = strokeWidth; - this.drawShape(); -}; - -Point.prototype.getStrokeWidth = function getStrokeWidth() { - return this._strokeWidth; -}; - -Point.prototype.destroy = function destroy() { - this.element.remove(); - this.handles.remove(); -}; - -Point.prototype.intersectRegion = function intersectRegion(region) { - var path = this.manager.regionToPath(region, this._zoomFraction * 100); - var f = this._zoomFraction, - x = parseInt(this._x * f, 10), - y = parseInt(this._y * f, 10); - - return Raphael.isPointInsidePath(path, x, y); -}; - -Point.prototype.getPath = function getPath() { - // Adapted from https://github.com/poilu/raphael-boolean - var a = this.element.attrs, - radiusX = a.radiusX, - radiusY = a.radiusY, - cornerPoints = [ - [a.x - radiusX, a.y - radiusY], - [a.x + radiusX, a.y - radiusY], - [a.x + radiusX, a.y + radiusY], - [a.x - radiusX, a.y + radiusY], - ], - path = []; - var radiusShift = [ - [ - [0, 1], - [1, 0], - ], - [ - [-1, 0], - [0, 1], - ], - [ - [0, -1], - [-1, 0], - ], - [ - [1, 0], - [0, -1], - ], - ]; - - //iterate all corners - for (var i = 0; i <= 3; i++) { - //insert starting point - if (i === 0) { - path.push(["M", cornerPoints[0][0], cornerPoints[0][1] + radiusY]); - } - - //insert "curveto" (radius factor .446 is taken from Inkscape) - var c1 = [ - cornerPoints[i][0] + radiusShift[i][0][0] * radiusX * 0.446, - cornerPoints[i][1] + radiusShift[i][0][1] * radiusY * 0.446, - ]; - var c2 = [ - cornerPoints[i][0] + radiusShift[i][1][0] * radiusX * 0.446, - cornerPoints[i][1] + radiusShift[i][1][1] * radiusY * 0.446, - ]; - var p2 = [ - cornerPoints[i][0] + radiusShift[i][1][0] * radiusX, - cornerPoints[i][1] + radiusShift[i][1][1] * radiusY, - ]; - path.push(["C", c1[0], c1[1], c2[0], c2[1], p2[0], p2[1]]); - } - path.push(["Z"]); - path = path.join(",").replace(/,?([achlmqrstvxz]),?/gi, "$1"); - - if (this._rotation !== 0) { - path = Raphael.transformPath(path, "r" + this._rotation); - } - return path; -}; - -Point.prototype.isSelected = function isSelected() { - return this._selected; -}; - -Point.prototype.setZoom = function setZoom(zoom) { - this._zoomFraction = zoom / 100; - this.drawShape(); -}; - -Point.prototype.updateHandle = function updateHandle( - handleId, - x, - y, - shiftKey -) { - // Refresh the handle coordinates, then update the specified handle - // using MODEL coordinates - this._handleIds = this.getHandleCoords(); - var h = this._handleIds[handleId]; - h.x = x; - h.y = y; - var resizeWidth = handleId === "left" || handleId === "right"; - this.updateShapeFromHandles(resizeWidth, shiftKey); -}; - -Point.prototype.updateShapeFromHandles = function updateShapeFromHandles( - resizeWidth, - shiftKey -) { - var hh = this._handleIds, - lengthX = hh.end.x - hh.start.x, - lengthY = hh.end.y - hh.start.y, - widthX = hh.left.x - hh.right.x, - widthY = hh.left.y - hh.right.y, - rot; - // Use the 'start' and 'end' handles to get rotation and length - if (lengthX === 0) { - this._rotation = 90; - } else if (lengthX > 0) { - rot = Math.atan(lengthY / lengthX); - this._rotation = Raphael.deg(rot); - } else if (lengthX < 0) { - rot = Math.atan(lengthY / lengthX); - this._rotation = 180 + Raphael.deg(rot); - } - - // centre is half-way between 'start' and 'end' handles - this._x = (hh.start.x + hh.end.x) / 2; - this._y = (hh.start.y + hh.end.y) / 2; - // Radius-x is half of distance between handles - this._radiusX = Math.sqrt(lengthX * lengthX + lengthY * lengthY) / 2; - // Radius-y may depend on handles OR on x/y ratio - if (resizeWidth) { - this._radiusY = Math.sqrt(widthX * widthX + widthY * widthY) / 2; - this._yxRatio = this._radiusY / this._radiusX; - } else { - if (shiftKey) { - this._yxRatio = 1; - } - this._radiusY = this._yxRatio * this._radiusX; - } - this._area = this._radiusX * this._radiusY * Math.PI; - - this.drawShape(); -}; - -Point.prototype.drawShape = function drawShape() { - var strokeColor = this._strokeColor, - strokeW = this._strokeWidth; - - var f = this._zoomFraction, - x = this._x * f, - y = this._y * f, - radiusX = this._radiusX * f, - radiusY = this._radiusY * f; - - this.element.attr({ - cx: x, - cy: y, - rx: radiusX, - ry: radiusY, - stroke: strokeColor, - "stroke-width": strokeW, - }); - this.element.transform("r" + this._rotation); - - if (this.isSelected()) { - this.handles.show().toFront(); - } else { - this.handles.hide(); - } - - // handles have been updated (model coords) - this._handleIds = this.getHandleCoords(); - var hnd, h_id, hx, hy; - for (var h = 0, l = this.handles.length; h < l; h++) { - hnd = this.handles[h]; - h_id = hnd.h_id; - hx = this._handleIds[h_id].x * this._zoomFraction; - hy = this._handleIds[h_id].y * this._zoomFraction; - hnd.attr({ x: hx - this.handle_wh / 2, y: hy - this.handle_wh / 2 }); - } -}; - -Point.prototype.setSelected = function setSelected(selected) { - this._selected = !!selected; - this.drawShape(); -}; - -Point.prototype.createHandles = function createHandles() { - // ---- Create Handles ----- - - // NB: handleIds are used to calculate ellipse coords - // so handledIds are scaled to MODEL coords, not zoomed. - this._handleIds = this.getHandleCoords(); - - var self = this, - // map of centre-points for each handle - handleAttrs = { - stroke: "#4b80f9", - fill: "#fff", - cursor: "default", - "fill-opacity": 1.0, - }; - - // draw handles - Can't drag handles to resize, but they are useful - // simply to indicate that the Point is selected - self.handles = this.paper.set(); - - var hsize = this.handle_wh, - hx, - hy, - handle; - for (var key in this._handleIds) { - hx = this._handleIds[key].x; - hy = this._handleIds[key].y; - handle = this.paper.rect(hx - hsize / 2, hy - hsize / 2, hsize, hsize); - handle.attr({ cursor: "move" }); - handle.h_id = key; - handle.line = self; - self.handles.push(handle); - } - - self.handles.attr(handleAttrs).hide(); // show on selection -}; - -Point.prototype.getHandleCoords = function getHandleCoords() { - // Returns MODEL coordinates (not zoom coordinates) - let margin = 2; - - var rot = Raphael.rad(this._rotation), - x = this._x, - y = this._y, - radiusX = this._radiusX + margin, - radiusY = this._radiusY + margin, - startX = x - Math.cos(rot) * radiusX, - startY = y - Math.sin(rot) * radiusX, - endX = x + Math.cos(rot) * radiusX, - endY = y + Math.sin(rot) * radiusX, - leftX = x + Math.sin(rot) * radiusY, - leftY = y - Math.cos(rot) * radiusY, - rightX = x - Math.sin(rot) * radiusY, - rightY = y + Math.cos(rot) * radiusY; - - return { - start: { x: startX, y: startY }, - end: { x: endX, y: endY }, - left: { x: leftX, y: leftY }, - right: { x: rightX, y: rightY }, - }; -}; - -// Class for creating Point. -var CreatePoint = function CreatePoint(options) { - this.paper = options.paper; - this.manager = options.manager; -}; - -CreatePoint.prototype.startDrag = function startDrag(startX, startY) { - var strokeColor = this.manager.getStrokeColor(), - strokeWidth = this.manager.getStrokeWidth(), - zoom = this.manager.getZoom(); - - this.point = new Point({ - manager: this.manager, - paper: this.paper, - x: startX, - y: startY, - radiusX: POINT_RADIUS, - radiusY: POINT_RADIUS, - area: 0, - rotation: 0, - strokeWidth: strokeWidth, - zoom: zoom, - strokeColor: strokeColor, - }); -}; - -CreatePoint.prototype.drag = function drag(dragX, dragY, shiftKey) { - // no drag behaviour on Point creation -}; - -CreatePoint.prototype.stopDrag = function stopDrag() { - // Don't create ellipse of zero size (click, without drag) - var coords = this.point.toJson(); - if (coords.radiusX < 2) { - this.point.destroy(); - delete this.ellipse; - return; - } - // on the 'new:shape' trigger, this shape will already be selected - this.point.setSelected(true); - this.manager.addShape(this.point); -}; - - var Ellipse = function Ellipse(options) { var self = this; this.manager = options.manager; @@ -1022,4 +554,4 @@ CreateEllipse.prototype.stopDrag = function stopDrag() { this.manager.addShape(this.ellipse); }; -export { CreatePoint, Point, CreateEllipse, Ellipse }; +export { CreateEllipse, Ellipse }; diff --git a/src/js/shape_editor/point.js b/src/js/shape_editor/point.js new file mode 100644 index 000000000..8aa3367e3 --- /dev/null +++ b/src/js/shape_editor/point.js @@ -0,0 +1,477 @@ +/* +// Copyright (C) 2015-2024 University of Dundee & Open Microscopy Environment. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +// conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +// disclaimer in the documentation // and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +// BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +// IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import Raphael from "raphael"; + +const POINT_RADIUS = 5; +var Point = function Point(options) { + var self = this; + this.manager = options.manager; + this.paper = options.paper; + + if (options.id) { + this._id = options.id; + } else { + this._id = this.manager.getRandomId(); + } + this._x = options.x; + this._y = options.y; + this._radiusX = POINT_RADIUS; + this._radiusY = POINT_RADIUS; + this._rotation = options.rotation || 0; + + if (this._radiusX === 0 || this._radiusY === 0) { + this._yxRatio = 0.5; + } else { + this._yxRatio = this._radiusY / this._radiusX; + } + + this._strokeColor = options.strokeColor; + this._strokeWidth = options.strokeWidth || 2; + this._selected = false; + this._zoomFraction = 1; + if (options.zoom) { + this._zoomFraction = options.zoom / 100; + } + if (options.area) { + this._area = options.area; + } else { + this._area = this._radiusX * this._radiusY * Math.PI; + } + this.handle_wh = 6; + + this.element = this.paper.ellipse(); + this.element.attr({ "fill-opacity": 0.01, fill: "#fff", cursor: "pointer" }); + + // Drag handling of point + if (this.manager.canEdit) { + this.element.drag( + function (dx, dy) { + // DRAG, update location and redraw + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + + var offsetX = dx - this.prevX; + var offsetY = dy - this.prevY; + this.prevX = dx; + this.prevY = dy; + + // Manager handles move and redraw + self.manager.moveSelectedShapes(offsetX, offsetY, true); + return false; + }, + function () { + // START drag: note the start location + self._handleMousedown(); + this.prevX = 0; + this.prevY = 0; + return false; + }, + function () { + // STOP + // notify changed if moved + if (this.prevX !== 0 || this.prevY !== 0) { + self.manager.notifySelectedShapesChanged(); + } + return false; + } + ); + } + + // create handles, applying this.Matrix if set + this.createHandles(); + // update x, y, radiusX, radiusY & rotation + // If we have Matrix, recalculate width/height ratio based on all handles + var resizeWidth = !!this.Matrix; + this.updateShapeFromHandles(resizeWidth); + // and draw the Ellipse + this.drawShape(); +}; + +Point.prototype.toJson = function toJson() { + var rv = { + type: "Point", + x: this._x, + y: this._y, + strokeWidth: this._strokeWidth, + strokeColor: this._strokeColor, + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +Point.prototype.compareCoords = function compareCoords(json) { + var selfJson = this.toJson(), + match = true; + if (json.type !== selfJson.type) { + return false; + } + ["x", "y"].forEach(function (c) { + if (Math.round(json[c]) !== Math.round(selfJson[c])) { + match = false; + } + }); + return match; +}; + +// Useful for pasting json with an offset +Point.prototype.offsetCoords = function offsetCoords(json, dx, dy) { + json.x = json.x + dx; + json.y = json.y + dy; + return json; +}; + +// Shift this shape by dx and dy +Point.prototype.offsetShape = function offsetShape(dx, dy) { + this._x = this._x + dx; + this._y = this._y + dy; + this.drawShape(); +}; + +// handle start of drag by selecting this shape +// if not already selected +Point.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Point.prototype.setColor = function setColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Point.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Point.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Point.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Point.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Point.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Point.prototype.intersectRegion = function intersectRegion(region) { + var path = this.manager.regionToPath(region, this._zoomFraction * 100); + var f = this._zoomFraction, + x = parseInt(this._x * f, 10), + y = parseInt(this._y * f, 10); + + return Raphael.isPointInsidePath(path, x, y); +}; + +Point.prototype.getPath = function getPath() { + // Adapted from https://github.com/poilu/raphael-boolean + var a = this.element.attrs, + radiusX = a.radiusX, + radiusY = a.radiusY, + cornerPoints = [ + [a.x - radiusX, a.y - radiusY], + [a.x + radiusX, a.y - radiusY], + [a.x + radiusX, a.y + radiusY], + [a.x - radiusX, a.y + radiusY], + ], + path = []; + var radiusShift = [ + [ + [0, 1], + [1, 0], + ], + [ + [-1, 0], + [0, 1], + ], + [ + [0, -1], + [-1, 0], + ], + [ + [1, 0], + [0, -1], + ], + ]; + + //iterate all corners + for (var i = 0; i <= 3; i++) { + //insert starting point + if (i === 0) { + path.push(["M", cornerPoints[0][0], cornerPoints[0][1] + radiusY]); + } + + //insert "curveto" (radius factor .446 is taken from Inkscape) + var c1 = [ + cornerPoints[i][0] + radiusShift[i][0][0] * radiusX * 0.446, + cornerPoints[i][1] + radiusShift[i][0][1] * radiusY * 0.446, + ]; + var c2 = [ + cornerPoints[i][0] + radiusShift[i][1][0] * radiusX * 0.446, + cornerPoints[i][1] + radiusShift[i][1][1] * radiusY * 0.446, + ]; + var p2 = [ + cornerPoints[i][0] + radiusShift[i][1][0] * radiusX, + cornerPoints[i][1] + radiusShift[i][1][1] * radiusY, + ]; + path.push(["C", c1[0], c1[1], c2[0], c2[1], p2[0], p2[1]]); + } + path.push(["Z"]); + path = path.join(",").replace(/,?([achlmqrstvxz]),?/gi, "$1"); + + if (this._rotation !== 0) { + path = Raphael.transformPath(path, "r" + this._rotation); + } + return path; +}; + +Point.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Point.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Point.prototype.updateHandle = function updateHandle( + handleId, + x, + y, + shiftKey +) { + // Refresh the handle coordinates, then update the specified handle + // using MODEL coordinates + this._handleIds = this.getHandleCoords(); + var h = this._handleIds[handleId]; + h.x = x; + h.y = y; + var resizeWidth = handleId === "left" || handleId === "right"; + this.updateShapeFromHandles(resizeWidth, shiftKey); +}; + +Point.prototype.updateShapeFromHandles = function updateShapeFromHandles( + resizeWidth, + shiftKey +) { + var hh = this._handleIds, + lengthX = hh.end.x - hh.start.x, + lengthY = hh.end.y - hh.start.y, + widthX = hh.left.x - hh.right.x, + widthY = hh.left.y - hh.right.y, + rot; + // Use the 'start' and 'end' handles to get rotation and length + if (lengthX === 0) { + this._rotation = 90; + } else if (lengthX > 0) { + rot = Math.atan(lengthY / lengthX); + this._rotation = Raphael.deg(rot); + } else if (lengthX < 0) { + rot = Math.atan(lengthY / lengthX); + this._rotation = 180 + Raphael.deg(rot); + } + + // centre is half-way between 'start' and 'end' handles + this._x = (hh.start.x + hh.end.x) / 2; + this._y = (hh.start.y + hh.end.y) / 2; + // Radius-x is half of distance between handles + this._radiusX = Math.sqrt(lengthX * lengthX + lengthY * lengthY) / 2; + // Radius-y may depend on handles OR on x/y ratio + if (resizeWidth) { + this._radiusY = Math.sqrt(widthX * widthX + widthY * widthY) / 2; + this._yxRatio = this._radiusY / this._radiusX; + } else { + if (shiftKey) { + this._yxRatio = 1; + } + this._radiusY = this._yxRatio * this._radiusX; + } + this._area = this._radiusX * this._radiusY * Math.PI; + + this.drawShape(); +}; + +Point.prototype.drawShape = function drawShape() { + var strokeColor = this._strokeColor, + strokeW = this._strokeWidth; + + var f = this._zoomFraction, + x = this._x * f, + y = this._y * f, + radiusX = this._radiusX * f, + radiusY = this._radiusY * f; + + this.element.attr({ + cx: x, + cy: y, + rx: radiusX, + ry: radiusY, + stroke: strokeColor, + "stroke-width": strokeW, + }); + this.element.transform("r" + this._rotation); + + if (this.isSelected()) { + this.handles.show().toFront(); + } else { + this.handles.hide(); + } + + // handles have been updated (model coords) + this._handleIds = this.getHandleCoords(); + var hnd, h_id, hx, hy; + for (var h = 0, l = this.handles.length; h < l; h++) { + hnd = this.handles[h]; + h_id = hnd.h_id; + hx = this._handleIds[h_id].x * this._zoomFraction; + hy = this._handleIds[h_id].y * this._zoomFraction; + hnd.attr({ x: hx - this.handle_wh / 2, y: hy - this.handle_wh / 2 }); + } +}; + +Point.prototype.setSelected = function setSelected(selected) { + this._selected = !!selected; + this.drawShape(); +}; + +Point.prototype.createHandles = function createHandles() { + // ---- Create Handles ----- + + // NB: handleIds are used to calculate ellipse coords + // so handledIds are scaled to MODEL coords, not zoomed. + this._handleIds = this.getHandleCoords(); + + var self = this, + // map of centre-points for each handle + handleAttrs = { + stroke: "#4b80f9", + fill: "#fff", + cursor: "default", + "fill-opacity": 1.0, + }; + + // draw handles - Can't drag handles to resize, but they are useful + // simply to indicate that the Point is selected + self.handles = this.paper.set(); + + var hsize = this.handle_wh, + hx, + hy, + handle; + for (var key in this._handleIds) { + hx = this._handleIds[key].x; + hy = this._handleIds[key].y; + handle = this.paper.rect(hx - hsize / 2, hy - hsize / 2, hsize, hsize); + handle.attr({ cursor: "move" }); + handle.h_id = key; + handle.line = self; + self.handles.push(handle); + } + + self.handles.attr(handleAttrs).hide(); // show on selection +}; + +Point.prototype.getHandleCoords = function getHandleCoords() { + // Returns MODEL coordinates (not zoom coordinates) + let margin = 2; + + var rot = Raphael.rad(this._rotation), + x = this._x, + y = this._y, + radiusX = this._radiusX + margin, + radiusY = this._radiusY + margin, + startX = x - Math.cos(rot) * radiusX, + startY = y - Math.sin(rot) * radiusX, + endX = x + Math.cos(rot) * radiusX, + endY = y + Math.sin(rot) * radiusX, + leftX = x + Math.sin(rot) * radiusY, + leftY = y - Math.cos(rot) * radiusY, + rightX = x - Math.sin(rot) * radiusY, + rightY = y + Math.cos(rot) * radiusY; + + return { + start: { x: startX, y: startY }, + end: { x: endX, y: endY }, + left: { x: leftX, y: leftY }, + right: { x: rightX, y: rightY }, + }; +}; + +// Class for creating Point. +var CreatePoint = function CreatePoint(options) { + this.paper = options.paper; + this.manager = options.manager; +}; + +CreatePoint.prototype.startDrag = function startDrag(startX, startY) { + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + + this.point = new Point({ + manager: this.manager, + paper: this.paper, + x: startX, + y: startY, + radiusX: POINT_RADIUS, + radiusY: POINT_RADIUS, + area: 0, + rotation: 0, + strokeWidth: strokeWidth, + zoom: zoom, + strokeColor: strokeColor, + }); +}; + +CreatePoint.prototype.drag = function drag(dragX, dragY, shiftKey) { + // no drag behaviour on Point creation +}; + +CreatePoint.prototype.stopDrag = function stopDrag() { + // Don't create ellipse of zero size (click, without drag) + var coords = this.point.toJson(); + if (coords.radiusX < 2) { + this.point.destroy(); + delete this.ellipse; + return; + } + // on the 'new:shape' trigger, this shape will already be selected + this.point.setSelected(true); + this.manager.addShape(this.point); +}; + +export { CreatePoint, Point }; diff --git a/src/js/shape_editor/shape_manager.js b/src/js/shape_editor/shape_manager.js index cac45c517..5e7132a9f 100644 --- a/src/js/shape_editor/shape_manager.js +++ b/src/js/shape_editor/shape_manager.js @@ -28,7 +28,8 @@ import $ from "jquery"; import { CreateRect, Rect } from "./rect"; import { CreateLine, Line, CreateArrow, Arrow } from "./line"; -import { CreatePoint, Point, CreateEllipse, Ellipse } from "./ellipse"; +import { CreateEllipse, Ellipse } from "./ellipse"; +import { CreatePoint, Point } from "./point"; import { Polygon, Polyline } from "./polygon"; var ShapeManager = function ShapeManager(elementId, width, height, options) {