From 88ac4861e4135ec9faa6156cd1be37fa730ad0e6 Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Mon, 2 Dec 2024 11:40:03 +0100 Subject: [PATCH 1/7] feat: multipolygon factory, fftoolAdd for multipolygon --- modules/annotations/annotations.js | 8 +- modules/annotations/freeFormTool.js | 269 +++++++++++------- modules/annotations/objectGenericFactories.js | 163 ++++++++++- 3 files changed, 339 insertions(+), 101 deletions(-) diff --git a/modules/annotations/annotations.js b/modules/annotations/annotations.js index 704ab0b..66b333e 100644 --- a/modules/annotations/annotations.js +++ b/modules/annotations/annotations.js @@ -1356,6 +1356,7 @@ window.OSDAnnotations = class extends XOpatModuleSingleton { OSDAnnotations.registerAnnotationFactory(OSDAnnotations.Ellipse, false); OSDAnnotations.registerAnnotationFactory(OSDAnnotations.Ruler, false); OSDAnnotations.registerAnnotationFactory(OSDAnnotations.Polygon, false); + OSDAnnotations.registerAnnotationFactory(OSDAnnotations.Multipolygon, false); /** * Polygon factory, the only factory required within the module @@ -2229,8 +2230,11 @@ OSDAnnotations.StateFreeFormTool = class extends OSDAnnotations.AnnotationState if (!o.sessionID) return false; let factory = o._factory(); if (!factory.isEditable()) return false; - const result = factory.isImplicit() ? - factory.toPointArray(o, OSDAnnotations.AnnotationObjectFactory.withObjectPoint) : o.points; + const result = factory.isImplicit() + ? factory.toPointArray(o, OSDAnnotations.AnnotationObjectFactory.withObjectPoint) + : factory.factoryID !== "multipolygon" + ? o.points + : factory.restoreOriginalCoords(o._objects[0].points, o); // intersection with only the outer polygon of multipolygon ? if (!result) return false; return {object: o, asPolygon: result}; } diff --git a/modules/annotations/freeFormTool.js b/modules/annotations/freeFormTool.js index 7ff593f..ead6e7b 100644 --- a/modules/annotations/freeFormTool.js +++ b/modules/annotations/freeFormTool.js @@ -21,6 +21,15 @@ OSDAnnotations.FreeFormTool = class { USER_INTERFACE.addHtml(``, this._context.id); this._node = document.getElementById("annotation-cursor"); + + this._offscreenCanvas = document.createElement('canvas'); + this._offscreenCanvas.width = this._context.overlay._containerWidth; + this._offscreenCanvas.height = this._context.overlay._containerHeight; + this._ctx2d = this._offscreenCanvas.getContext('2d', { willReadFrequently: true }); + //this._ctx2d.imageSmoothingEnabled = false; + + this.MagicWand = OSDAnnotations.makeMagicWand(); + this.ref = VIEWER.scalebar.getReferencedTiledImage(); } /** @@ -37,8 +46,11 @@ OSDAnnotations.FreeFormTool = class { let objectFactory = this._context.getAnnotationObjectFactory(object.factoryID); this._created = created; + this._ctx2d.clearRect(0, 0, this._ctx2d.canvas.width, this._ctx2d.canvas.height); + this._ctx2d.fillStyle = 'white'; + if (objectFactory !== undefined) { - if (objectFactory.factoryID !== "polygon") { //object can be used immedietaly + if (objectFactory.factoryID !== "polygon" && objectFactory.factoryID !== "multipolygon") { //object can be used immedietaly let points = Array.isArray(created) ? points : ( objectFactory.supportsBrush() ? objectFactory.toPointArray(object, @@ -52,7 +64,8 @@ OSDAnnotations.FreeFormTool = class { return; } } else { - let newPolygon = created ? object : this._context.polygonFactory.copy(object, object.points); + const factory = objectFactory.factoryID === "polygon" ? this._context.polygonFactory : this._context.objectFactories.multipolygon; + let newPolygon = created ? object : factory.copy(object, null); this._setupPolygon(newPolygon, object); } @@ -200,7 +213,14 @@ OSDAnnotations.FreeFormTool = class { this._updatePerformed = this._update(point) || this._updatePerformed; if (this.polygon) { - this.polygon._setPositionDimensions({}); + if (this.polygon.factoryID === "multipolygon") { + this.polygon._objects.forEach(obj => { + obj._setPositionDimensions({}); + }); + } else { + this.polygon._setPositionDimensions({}); + } + this._context.canvas.renderAll(); } } catch (e) { @@ -251,49 +271,43 @@ OSDAnnotations.FreeFormTool = class { return null; } - //TODO sometimes the greinerHormann cycling, vertices are NaN values, do some measurement and kill after it takes too long (2+s ?) - _union (nextMousePos) { - if (!this.polygon || this._toDistancePointsAsObjects(this.mousePos, nextMousePos) < this.radius / 3) return false; + _drawPolygon(polygon) { + this._ctx2d.moveTo(polygon[0].x, polygon[0].y); - let radPoints = this.getCircleShape(nextMousePos); - //console.log(radPoints); - let polyPoints = this.polygon.get("points"); - //avoid 'Leaflet issue' - expecting a polygon that is not 'closed' on points (first != last) - if (this._toDistancePointsAsObjects(polyPoints[0], polyPoints[polyPoints.length - 1]) < this.radius) polyPoints.pop(); - this.mousePos = nextMousePos; + for (let i = 1; i < polygon.length; i++) { + this._ctx2d.lineTo(polygon[i].x, polygon[i].y); + } + this._ctx2d.lineTo(polygon[0].x, polygon[0].y); + this._ctx2d.closePath(); + } - let calcSize = OSDAnnotations.PolygonUtilities.approximatePolygonArea; + _rasterizePolygons(originalPoints, isPolygon) { + let points = []; + let firstPolygon; - //compute union - try { - var union = greinerHormann.union(polyPoints, radPoints); - } catch (e) { - console.warn("Unable to unify polygon with tool.", this.polygon, radPoints, e); - return false; + if (isPolygon) { + points = originalPoints.map(point => this.ref.imageToWindowCoordinates(new OpenSeadragon.Point(point.x, point.y))); + firstPolygon = points; + } else { + points = originalPoints.map(subPolygonPoints => { + return subPolygonPoints.map(point => + this.ref.imageToWindowCoordinates(new OpenSeadragon.Point(point.x, point.y)) + ); + }); + + firstPolygon = points[0]; } - if (union) { - if (typeof union[0][0] === 'number') { // single linear ring - return false; - } + this._ctx2d.beginPath(); + this._drawPolygon(firstPolygon); - if (union.length > 1) union = this._unify(union); - - let maxIdx = 0,maxScore = 0; - for (let j = 0; j < union.length; j++) { - let measure = calcSize(union[j]); - if (measure.diffX < this.radius || measure.diffY < this.radius) continue; - let area = measure.diffX * measure.diffY; - let score = 2*area + union[j].length; - if (score > maxScore) { - maxScore = score; - maxIdx = j; - } + if (!isPolygon) { + for (let i = 1; i < points.length; i++) { + this._drawPolygon(points[i]); } - this.polygon.set({points: this.simplifier(union[maxIdx])}); - return true; } - return false; + + this._ctx2d.fill("evenodd"); } //initialize object so that it is ready to be modified @@ -307,6 +321,18 @@ OSDAnnotations.FreeFormTool = class { this._context.addHelperAnnotation(polyObject); } + if (polyObject.factoryID === "polygon") { + this._rasterizePolygons(polyObject.points, true); + } else { + const points = polyObject._objects.map(subPolygon => + this._context.objectFactories.multipolygon.restoreOriginalCoords( + subPolygon.points, + polyObject + ) + ); + this._rasterizePolygons(points, false) + } + polyObject.moveCursor = 'crosshair'; } @@ -317,82 +343,135 @@ OSDAnnotations.FreeFormTool = class { this._setupPolygon(polygon, object); } - //try to merge polygon list into one polygons using 'greinerHormann.union' repeated call and simplyfiing the polygon - _unify(unions) { - let i = 0, len = unions.length ** 2 + 10, primary = [], secondary = []; + _changeFactory(factory, contourPoints) { + let newObject = factory.copy(this.polygon, contourPoints); + newObject.factoryID = factory.factoryID; - unions.forEach(u => { - primary.push(this.simplifier(u)); - }); - while (i < len) { - if (primary.length < 2) break; + if (!this._created) { + this._context.replaceAnnotation(this.polygon, this.initial, true); + this.polygon = newObject; + this._context.replaceAnnotation(this.initial, this.polygon, true); + } else { + this._context.deleteHelperAnnotation(this.polygon); + this.polygon = newObject; + this._context.addHelperAnnotation(this.polygon); + } + } - i++; - let j = 0; - for (; j < primary.length - 1; j += 2) { - let ress = greinerHormann.union(primary[j], primary[j + 1]); + _union (nextMousePos) { + if (!this.polygon || this._toDistancePointsAsObjects(this.mousePos, nextMousePos) < this.radius / 3) return false; - if (typeof ress[0][0] === 'number') { - ress = [ress]; - } - secondary = ress.concat(secondary); //reverse order for different union call in the next loop + this.mousePos = nextMousePos; + this._ctx2d.fillStyle = 'white'; + + let contours = this._getContours(); + + // go through contours and delete too small inner polygons + + if (contours.length === 0 || (contours.length > 1 && (contours[0].inner || !contours[1].inner))) return false; + + if (contours.length === 1) { // polygon + if (contours[0].inner) return false; + if (this.initial.factoryID !== "multipolygon") { + this.polygon.set({points: contours[0].points}); + } else { + this._changeFactory(this._context.polygonFactory, contours[0].points); + } + + } else { // multipolygon + let contourPoints = contours.map(contour => contour.points); + + if (this.initial.factoryID === "multipolygon") { + this.polygon = this._context.objectFactories.multipolygon.swapHoles(this.polygon, contourPoints); + } else { + this._changeFactory(this._context.objectFactories.multipolygon, contourPoints); } - if (j === primary.length - 1) secondary.push(primary[j]); - primary = secondary; - secondary = []; } - return primary; + return true; } - _subtract (nextMousePos) { + _subtract (nextMousePos) { // to do if (!this.polygon || this._toDistancePointsAsObjects(this.mousePos, nextMousePos) < this.radius / 3) return false; - let radPoints = this.getCircleShape(nextMousePos); - let polyPoints = this.polygon.get("points"); this.mousePos = nextMousePos; + this._ctx2d.fillStyle = 'black'; - let calcSize = OSDAnnotations.PolygonUtilities.approximatePolygonArea; + let contours = this._getContours(); - try { - var difference = greinerHormann.diff(polyPoints, radPoints); - } catch (e) { - console.warn("Unable to diff polygon with tool.", this.polygon, radPoints, e); - return false; + if (contours.length === 0){ // deletion + this.finish(true); + return true; + } + + if (contours.length === 1 ) { //polygon + this.polygon.set({points: contours[0].points}); + } else { + + let calcSize = OSDAnnotations.PolygonUtilities.approximatePolygonArea; + let points = calcSize(contours[0].points) > calcSize(contours[1].points) ? contours[0].points : contours[1].points; + this.polygon.set({points: points}); } - if (difference) { - let polygon; - if (typeof difference[0][0] === 'number') { // single linear ring - polygon = this.simplifier(difference); - } else { - if (difference.length > 1) difference = this._unify(difference); - - let maxIdx = 0, maxArea = 0, maxScore = 0; - for (let j = 0; j < difference.length; j++) { - let measure = calcSize(difference[j]); - if (measure.diffX < this.radius || measure.diffY < this.radius) continue; - let area = measure.diffX * measure.diffY; - let score = 2*area + difference[j].length; - if (score > maxScore) { - maxArea = area; - maxScore = score; - maxIdx = j; - } - } + return true; + } + + _getContours() { + this._rasterizePolygons(this.getCircleShape(this.mousePos), true); + + const imageData = this._ctx2d.getImageData(0, 0, this._ctx2d.canvas.width, this._ctx2d.canvas.height); + const mask = this._getBinaryMask(imageData.data, imageData.width, imageData.height); + if (!mask.bounds) return []; + + let contours = this.MagicWand.traceContours(mask); + contours = this.MagicWand.simplifyContours(contours, 1, 30); + + const imageContours = contours.map(contour => ({ + ...contour, + points: contour.points.map(point => + this.ref.windowToImageCoordinates(new OpenSeadragon.Point(point.x, point.y)) + ) + })); + + return imageContours; + } - if (maxArea < this.radius * this.radius / 2) { //largest area ceased to exist: finish - delete this.initial.moveCursor; - delete this.polygon.moveCursor; - this.finish(true); - return true; + _getBinaryMask(data, width, height) { + let mask = new Uint8ClampedArray(width * height); + let maxX = -1, minX = width, maxY = -1, minY = height, bounds; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + const r = data[index]; + + if (r === 255) { + mask[y * width + x] = 1; + + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; } + } + } - polygon = this.simplifier(difference[maxIdx]); + if (maxX === -1 || maxY === -1) { + bounds = null; + } else { + bounds = { + minX: minX, + minY: minY, + maxX: maxX, + maxY: maxY } - this.polygon.set({points: polygon}); - return true; } - return false; + + return { + data: mask, + width: width, + height: height, + bounds: bounds, + } } _toDistancePointsAsObjects(pointA, pointB) { diff --git a/modules/annotations/objectGenericFactories.js b/modules/annotations/objectGenericFactories.js index e895f7f..d7d5cac 100644 --- a/modules/annotations/objectGenericFactories.js +++ b/modules/annotations/objectGenericFactories.js @@ -715,7 +715,7 @@ OSDAnnotations.Point = class extends OSDAnnotations.Ellipse { } }; -OSDAnnotations.ExplicitPointsObjectFactory = class extends OSDAnnotations.AnnotationObjectFactory { +OSDAnnotations.ExplicitPointsObjectFactory = class extends OSDAnnotations.AnnotationObjectFactory { /// constructor(context, autoCreationStrategy, presetManager, factoryID, type, fabricClass, withHelperPoints=true) { super(context, autoCreationStrategy, presetManager, factoryID, type); @@ -1337,7 +1337,7 @@ OSDAnnotations.Line = class extends OSDAnnotations.AnnotationObjectFactory { } }; -OSDAnnotations.Polygon = class extends OSDAnnotations.ExplicitPointsObjectFactory { +OSDAnnotations.Polygon = class extends OSDAnnotations.ExplicitPointsObjectFactory { /// constructor(context, autoCreationStrategy, presetManager) { super(context, autoCreationStrategy, presetManager, "polygon", "polygon", fabric.Polygon, true); } @@ -1420,7 +1420,7 @@ OSDAnnotations.Group = class extends OSDAnnotations.AnnotationObjectFactory { * @param {Array} parameters array of objects * @param {Object} options see parent class */ - create(parameters, options) { + create(parameters, options) { ///// return this.configure(new fabric.Group(parameters), options); } @@ -1429,11 +1429,13 @@ OSDAnnotations.Group = class extends OSDAnnotations.AnnotationObjectFactory { //todo use factory instead object.forEachObject(o => { - o.fill = options.fill; + //o.fill = options.fill; o.stroke = options.stroke; o.color = options.color; o.originalStrokeWidth = options.originalStrokeWidth; }); + + return object; } _eachChildAndFactory(ofObject, executor, method="map") { @@ -1579,3 +1581,156 @@ OSDAnnotations.Group = class extends OSDAnnotations.AnnotationObjectFactory { return "Complex Annotation"; } }; + +OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFactory { + + constructor(context, autoCreationStrategy, presetManager) { + super(context, autoCreationStrategy, presetManager, "multipolygon", "group"); + this._current = null; + + this._polygonFactory = new OSDAnnotations.Polygon(context, autoCreationStrategy, presetManager); + } + + getIcon() { + return "view_timeline"; + } + + fabricStructure() { + return ["polygon"]; + } + + getDescription(ofObject) { + return `Multipolygon [${Math.round(ofObject.left)}, ${Math.round(ofObject.top)}]`; + } + + getCurrentObject() { + return this._current; + } + + title() { + return "Multipolygon"; + } + + create(parameters, options) { + let polygons = this._createPolygons(parameters, options); + let multipolygon = this._createGroup(polygons, options); + + return multipolygon; + } + + _createPolygons(parameters, options) { + // to do + //let outerPolygon = this._polygonFactory.create(parameters[0], options); // { ...options, globalCompositeOperation: "destination-out" }); //source-out , destination-out, xor + //let holes = parameters.slice(1).map(param => { + // let customOptions = { ...options , fill: "transparent", globalCompositeOperation: ' destination-out' }; + // return this._polygonFactory.create(param, customOptions); + //}); + + //clipPath + //return [...holes, outerPolygon]; + return parameters.map(param => this._polygonFactory.create(param, options)); + } + + _createGroup(polygons, options) { + let multipolygon = new fabric.Group(polygons); + multipolygon = this.configure(multipolygon, options); + + this._adjustPointsRelativeToGroup(multipolygon); + return multipolygon; + } + + _adjustPointsRelativeToGroup(group) { + group._objects.forEach(obj => { + obj.points = obj.points.map(p => ({ + x: p.x - (group.left + group.width / 2), + y: p.y - (group.top + group.height / 2) + })); + obj.setCoords(); + }); + } + + configure(object, options) { + const instance = super.configure(object, options); + + delete instance.points; + return instance + } + + restoreOriginalCoords(points, multipolygon) { + return points.map(p => ({ + x: p.x + (multipolygon.left + multipolygon.width / 2), + y: p.y + (multipolygon.top + multipolygon.height / 2) + })); + } + + swapHoles(group, newParameters) { + const props = this.copyProperties(group); + delete props.left; + delete props.top; + delete props.width; + delete props.height; + delete props.points; + + let newPolygons = this._createPolygons(newParameters, props); + group._objects = newPolygons; + + this._adjustPointsRelativeToGroup(group); + return group; + } + + copy(ofObject, parameters=undefined) { + const props = this.copyProperties(ofObject); + delete props.left; + delete props.top; + delete props.width; + delete props.height; + delete props.points; + + if (!Array.isArray(parameters)) { + parameters = ofObject._objects.map(obj => { + return this.restoreOriginalCoords(obj.points, ofObject); + }); + } + return this.create(parameters, props);; + } + + exportsGeometry() { + return [["points"]]; + } + + // to do + //edit(theObject) { + //} + //recalculate(theObject) { + //} + + getArea(theObject) { + let area = this._polygonFactory.getArea(theObject._objects[0]); + + for (let i = 1; i < theObject._objects.length; i++) { + area -= this._polygonFactory.getArea(theObject._objects[i]); + } + + return area; + } + + onZoom(ofObject, graphicZoom, realZoom) { + ofObject._objects.forEach(o => { + o.set({strokeWidth: ofObject.originalStrokeWidth/graphicZoom}); + }); + } + + isImplicit() { + return false; + } + + updateRendering(ofObject, preset, visualProperties, defaultVisualProperties) { + ofObject._objects.forEach(o => { + this._polygonFactory.updateRendering(o, preset, visualProperties, defaultVisualProperties); + }); + } + + toPointArray(obj, converter, digits=undefined, quality=1) { + return undefined; + } +}; From 3914ee89d68bd82506a382bbbced21fdf7b59fbd Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Fri, 6 Dec 2024 16:50:06 +0100 Subject: [PATCH 2/7] feat: fftool remove for multipolygon --- modules/annotations/annotations.js | 47 ++++++-- modules/annotations/freeFormTool.js | 106 +++++++++++------- modules/annotations/objectGenericFactories.js | 35 ++++-- modules/annotations/objects.js | 21 ++++ 4 files changed, 150 insertions(+), 59 deletions(-) diff --git a/modules/annotations/annotations.js b/modules/annotations/annotations.js index 66b333e..8a0dd6a 100644 --- a/modules/annotations/annotations.js +++ b/modules/annotations/annotations.js @@ -2234,7 +2234,7 @@ OSDAnnotations.StateFreeFormTool = class extends OSDAnnotations.AnnotationState ? factory.toPointArray(o, OSDAnnotations.AnnotationObjectFactory.withObjectPoint) : factory.factoryID !== "multipolygon" ? o.points - : factory.restoreOriginalCoords(o._objects[0].points, o); // intersection with only the outer polygon of multipolygon ? + : factory.getMultipolygonPoints(o); if (!result) return false; return {object: o, asPolygon: result}; } @@ -2254,19 +2254,43 @@ OSDAnnotations.StateFreeFormTool = class extends OSDAnnotations.AnnotationState height: ffTool.radius * 2 + offset }, getObjectAsCandidateForIntersectionTest); + const polygonUtils = OSDAnnotations.PolygonUtilities; const active = this.context.canvas.getActiveObject(); - let max = 0, - result = candidates; // by default return the whole list if intersections are <= 0 - for (let i = 0; i < candidates.length; i++) { - let candidate = candidates[i]; - const intersection = OSDAnnotations.checkPolygonIntersect(brushPolygon, candidate.asPolygon); - if (intersection.length) { - if (active) { // prefer first encountered object if it is also the selection - return candidate; + let max = 0, result = candidates; // by default return the whole list if intersections are <= 0 + + for (let candidate of candidates) { + let outerPolygon; + let holes = null; + let notFullyInHoles = false; + let isMultipolygon = candidate.object.factoryID === "multipolygon"; + + if (isMultipolygon) { + outerPolygon = candidate.asPolygon[0]; + holes = candidate.asPolygon.slice(1); + } else { + outerPolygon = candidate.asPolygon; + } + + const intersection = OSDAnnotations.checkPolygonIntersect(brushPolygon, outerPolygon); + if (!intersection.length) continue; + + if (holes) { + notFullyInHoles = holes.every(hole => { + if (polygonUtils.intersectAABB(polygonUtils.getBoundingBox(brushPolygon), polygonUtils.getBoundingBox(hole))) { + const preciseIntersection = OSDAnnotations.checkPolygonIntersect(brushPolygon, hole); + return !(JSON.stringify(preciseIntersection) === JSON.stringify(brushPolygon)); + } + return true; + }); + } + + if (!isMultipolygon || notFullyInHoles) { + if (active) { // prefer first encounhtered object if it is also the selection + return candidate.object; } if (intersection.length > max) { max = intersection.length; - result = candidate; + result = candidate.object; } } } @@ -2327,6 +2351,7 @@ OSDAnnotations.StateFreeFormToolAdd = class extends OSDAnnotations.StateFreeForm } let created = false; const ffTool = this.context.freeFormTool; + ffTool.zoom = this.context.canvas.getZoom(); ffTool.recomputeRadius(); const newPolygonPoints = ffTool.getCircleShape(point); let targetIntersection = this.fftFindTarget(point, ffTool, newPolygonPoints, 0); @@ -2394,6 +2419,7 @@ OSDAnnotations.StateFreeFormToolRemove = class extends OSDAnnotations.StateFreeF } const ffTool = this.context.freeFormTool; + ffTool.zoom = this.context.canvas.getZoom(); ffTool.recomputeRadius(); const newPolygonPoints = ffTool.getCircleShape(point); let candidates = this.fftFindTarget(point, ffTool, newPolygonPoints, 50); @@ -2577,6 +2603,7 @@ OSDAnnotations.StateCorrectionTool = class extends OSDAnnotations.StateFreeFormT const ffTool = this.context.freeFormTool, newPolygonPoints = ffTool.getCircleShape(point); + ffTool.zoom = this.context.canvas.getZoom(); let candidates = this.fftFindTarget(point, ffTool, newPolygonPoints, 50); if (this.fftFoundIntersection(candidates)) { diff --git a/modules/annotations/freeFormTool.js b/modules/annotations/freeFormTool.js index ead6e7b..6b20074 100644 --- a/modules/annotations/freeFormTool.js +++ b/modules/annotations/freeFormTool.js @@ -13,6 +13,7 @@ OSDAnnotations.FreeFormTool = class { this.radius = 20; this.mousePos = null; this.SQRT3DIV2 = 0.866025403784; + this.zoom = null; this._context = context; this._update = null; this._created = false; @@ -358,63 +359,90 @@ OSDAnnotations.FreeFormTool = class { } } - _union (nextMousePos) { - if (!this.polygon || this._toDistancePointsAsObjects(this.mousePos, nextMousePos) < this.radius / 3) return false; + _getValidContours(contours) { + const polygonUtils = OSDAnnotations.PolygonUtilities; + let innerContours = []; + let falseOuterContours = []; + let maxArea = -1; + let outerContour = null; - this.mousePos = nextMousePos; - this._ctx2d.fillStyle = 'white'; + for (let i = 0; i < contours.length; i++) { + const size = polygonUtils.approximatePolygonArea(contours[i].points); + const area = size.diffX * size.diffY; - let contours = this._getContours(); + if (contours[i].inner) { + //if (area < this.zoom ) continue; // deleting too small holes + innerContours.push(contours[i]); - // go through contours and delete too small inner polygons + } else if (area > maxArea) { + if (outerContour) falseOuterContours.push(outerContour); + maxArea = area; + outerContour = contours[i]; - if (contours.length === 0 || (contours.length > 1 && (contours[0].inner || !contours[1].inner))) return false; - - if (contours.length === 1) { // polygon - if (contours[0].inner) return false; - if (this.initial.factoryID !== "multipolygon") { - this.polygon.set({points: contours[0].points}); - } else { - this._changeFactory(this._context.polygonFactory, contours[0].points); - } - - } else { // multipolygon - let contourPoints = contours.map(contour => contour.points); - - if (this.initial.factoryID === "multipolygon") { - this.polygon = this._context.objectFactories.multipolygon.swapHoles(this.polygon, contourPoints); } else { - this._changeFactory(this._context.objectFactories.multipolygon, contourPoints); + falseOuterContours.push(contours[i]); } } - return true; + + if (!outerContour) return innerContours; + + if (falseOuterContours.length !== 0) { + innerContours = innerContours.filter(inner => { + return falseOuterContours.some(outer => { + if (polygonUtils.intersectAABB(polygonUtils.getBoundingBox(outer.points), polygonUtils.getBoundingBox(inner.points))) { + const intersections = OSDAnnotations.checkPolygonIntersect(inner.points, outer.points); + return intersections.length === 0 || JSON.stringify(intersections) === JSON.stringify(outer.points); + } + return true; + }); + }); + } + + return [outerContour, ...innerContours]; } - _subtract (nextMousePos) { // to do + _processContours(nextMousePos, fillColor) { if (!this.polygon || this._toDistancePointsAsObjects(this.mousePos, nextMousePos) < this.radius / 3) return false; - + this.mousePos = nextMousePos; - this._ctx2d.fillStyle = 'black'; - + this._ctx2d.fillStyle = fillColor; + let contours = this._getContours(); - - if (contours.length === 0){ // deletion - this.finish(true); + contours = this._getValidContours(contours); + + if (contours.length >= 1 && contours[0].inner) return false; + + if (contours.length === 0) return this.finish(true); // deletion in subtract mode + + if (contours.length === 1) { // polygon + if (this.initial.factoryID !== "multipolygon") { + this.polygon.set({ points: contours[0].points }); + } else { + this._changeFactory(this._context.polygonFactory, contours[0].points); + } return true; } - - if (contours.length === 1 ) { //polygon - this.polygon.set({points: contours[0].points}); + + // multipolygon + let contourPoints = contours.map(contour => contour.points); + + if (this.initial.factoryID === "multipolygon") { + this.polygon = this._context.objectFactories.multipolygon.swapHoles(this.polygon, contourPoints); } else { - - let calcSize = OSDAnnotations.PolygonUtilities.approximatePolygonArea; - let points = calcSize(contours[0].points) > calcSize(contours[1].points) ? contours[0].points : contours[1].points; - this.polygon.set({points: points}); + this._changeFactory(this._context.objectFactories.multipolygon, contourPoints); } - + return true; } + _union (nextMousePos) { + return this._processContours(nextMousePos, 'white'); + } + + _subtract (nextMousePos) { + return this._processContours(nextMousePos, 'black'); + } + _getContours() { this._rasterizePolygons(this.getCircleShape(this.mousePos), true); @@ -423,7 +451,7 @@ OSDAnnotations.FreeFormTool = class { if (!mask.bounds) return []; let contours = this.MagicWand.traceContours(mask); - contours = this.MagicWand.simplifyContours(contours, 1, 30); + contours = this.MagicWand.simplifyContours(contours, 0, 30); const imageContours = contours.map(contour => ({ ...contour, diff --git a/modules/annotations/objectGenericFactories.js b/modules/annotations/objectGenericFactories.js index d7d5cac..ba3970e 100644 --- a/modules/annotations/objectGenericFactories.js +++ b/modules/annotations/objectGenericFactories.js @@ -1620,14 +1620,14 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto _createPolygons(parameters, options) { // to do - //let outerPolygon = this._polygonFactory.create(parameters[0], options); // { ...options, globalCompositeOperation: "destination-out" }); //source-out , destination-out, xor + //console.log(options); + //let outerPolygon = this._polygonFactory.create(parameters[0], options); // { ...options, globalCompositeOperation: "destination-out" }); //let holes = parameters.slice(1).map(param => { - // let customOptions = { ...options , fill: "transparent", globalCompositeOperation: ' destination-out' }; + // let customOptions = { ...options, globalCompositeOperation: "destination-out" }; // return this._polygonFactory.create(param, customOptions); - //}); - - //clipPath - //return [...holes, outerPolygon]; + //}) + // + //return [outerPolygon, ...holes]; return parameters.map(param => this._polygonFactory.create(param, options)); } @@ -1636,6 +1636,7 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto multipolygon = this.configure(multipolygon, options); this._adjustPointsRelativeToGroup(multipolygon); + multipolygon.globalCompositeOperation = "source-over"; return multipolygon; } @@ -1673,8 +1674,11 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto let newPolygons = this._createPolygons(newParameters, props); group._objects = newPolygons; + + group._calcBounds(); + group.setCoords(); - this._adjustPointsRelativeToGroup(group); + this._adjustPointsRelativeToGroup(group); return group; } @@ -1725,12 +1729,23 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto } updateRendering(ofObject, preset, visualProperties, defaultVisualProperties) { - ofObject._objects.forEach(o => { + console.log(preset); + this._polygonFactory.updateRendering(ofObject._objects[0], preset, visualProperties, defaultVisualProperties); + + //if (visualProperties.modeOutline) { + // visualProperties.globalCompositeOperation = "source-over"; + //} else { + // visualProperties.globalCompositeOperation = "destination-out"; + //} + + ofObject._objects.slice(1).forEach(o => { this._polygonFactory.updateRendering(o, preset, visualProperties, defaultVisualProperties); }); } - toPointArray(obj, converter, digits=undefined, quality=1) { - return undefined; + getMultipolygonPoints(multipolygon) { + return multipolygon._objects.map(polygon => + this.restoreOriginalCoords(polygon.points, multipolygon) + ); } }; diff --git a/modules/annotations/objects.js b/modules/annotations/objects.js index 07a8fe7..73c9074 100644 --- a/modules/annotations/objects.js +++ b/modules/annotations/objects.js @@ -597,6 +597,27 @@ OSDAnnotations.PolygonUtilities = { return { diffX: maxX - minX, diffY: maxY - minY }; }, + getBoundingBox: function (polygonPoints) { + if (!polygonPoints || polygonPoints.length === 0) return null; + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for (let i = 0; i < polygonPoints.length; i++) { + const point = polygonPoints[i]; + minX = Math.min(minX, point.x); + minY = Math.min(minY, point.y); + maxX = Math.max(maxX, point.x); + maxY = Math.max(maxY, point.y); + } + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + }; + }, + /** * https://gist.github.com/cwleonard/e124d63238bda7a3cbfa * To detect intersection with another Polygon object, this From 94e8a30f27129790535d3c11ff1b9a70ec37e933 Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Sun, 8 Dec 2024 23:23:21 +0100 Subject: [PATCH 3/7] fix: changed multipolygon factory representation to fabric.Path --- modules/annotations/annotations.js | 4 +- modules/annotations/freeFormTool.js | 30 +--- modules/annotations/objectGenericFactories.js | 149 +++++------------- modules/annotations/objects.js | 25 ++- 4 files changed, 59 insertions(+), 149 deletions(-) diff --git a/modules/annotations/annotations.js b/modules/annotations/annotations.js index 8a0dd6a..a516b8d 100644 --- a/modules/annotations/annotations.js +++ b/modules/annotations/annotations.js @@ -2232,9 +2232,7 @@ OSDAnnotations.StateFreeFormTool = class extends OSDAnnotations.AnnotationState if (!factory.isEditable()) return false; const result = factory.isImplicit() ? factory.toPointArray(o, OSDAnnotations.AnnotationObjectFactory.withObjectPoint) - : factory.factoryID !== "multipolygon" - ? o.points - : factory.getMultipolygonPoints(o); + : o.points; if (!result) return false; return {object: o, asPolygon: result}; } diff --git a/modules/annotations/freeFormTool.js b/modules/annotations/freeFormTool.js index 6b20074..2ac52bb 100644 --- a/modules/annotations/freeFormTool.js +++ b/modules/annotations/freeFormTool.js @@ -27,7 +27,6 @@ OSDAnnotations.FreeFormTool = class { this._offscreenCanvas.width = this._context.overlay._containerWidth; this._offscreenCanvas.height = this._context.overlay._containerHeight; this._ctx2d = this._offscreenCanvas.getContext('2d', { willReadFrequently: true }); - //this._ctx2d.imageSmoothingEnabled = false; this.MagicWand = OSDAnnotations.makeMagicWand(); this.ref = VIEWER.scalebar.getReferencedTiledImage(); @@ -212,18 +211,7 @@ OSDAnnotations.FreeFormTool = class { try { this._updatePerformed = this._update(point) || this._updatePerformed; - - if (this.polygon) { - if (this.polygon.factoryID === "multipolygon") { - this.polygon._objects.forEach(obj => { - obj._setPositionDimensions({}); - }); - } else { - this.polygon._setPositionDimensions({}); - } - - this._context.canvas.renderAll(); - } + this._context.canvas.renderAll(); } catch (e) { console.warn("FreeFormTool: something went wrong, ignoring...", e); } @@ -322,17 +310,8 @@ OSDAnnotations.FreeFormTool = class { this._context.addHelperAnnotation(polyObject); } - if (polyObject.factoryID === "polygon") { - this._rasterizePolygons(polyObject.points, true); - } else { - const points = polyObject._objects.map(subPolygon => - this._context.objectFactories.multipolygon.restoreOriginalCoords( - subPolygon.points, - polyObject - ) - ); - this._rasterizePolygons(points, false) - } + const isPolygon = polyObject.factoryID === "polygon"; + this._rasterizePolygons(polyObject.points, isPolygon); polyObject.moveCursor = 'crosshair'; } @@ -417,6 +396,7 @@ OSDAnnotations.FreeFormTool = class { if (contours.length === 1) { // polygon if (this.initial.factoryID !== "multipolygon") { this.polygon.set({ points: contours[0].points }); + this.polygon._setPositionDimensions({}); } else { this._changeFactory(this._context.polygonFactory, contours[0].points); } @@ -427,7 +407,7 @@ OSDAnnotations.FreeFormTool = class { let contourPoints = contours.map(contour => contour.points); if (this.initial.factoryID === "multipolygon") { - this.polygon = this._context.objectFactories.multipolygon.swapHoles(this.polygon, contourPoints); + this.polygon = this._context.objectFactories.multipolygon.setPoints(this.polygon, contourPoints); } else { this._changeFactory(this._context.objectFactories.multipolygon, contourPoints); } diff --git a/modules/annotations/objectGenericFactories.js b/modules/annotations/objectGenericFactories.js index ba3970e..4361742 100644 --- a/modules/annotations/objectGenericFactories.js +++ b/modules/annotations/objectGenericFactories.js @@ -1585,9 +1585,8 @@ OSDAnnotations.Group = class extends OSDAnnotations.AnnotationObjectFactory { OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFactory { constructor(context, autoCreationStrategy, presetManager) { - super(context, autoCreationStrategy, presetManager, "multipolygon", "group"); + super(context, autoCreationStrategy, presetManager, "multipolygon", "path"); this._current = null; - this._polygonFactory = new OSDAnnotations.Polygon(context, autoCreationStrategy, presetManager); } @@ -1611,75 +1610,50 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto return "Multipolygon"; } - create(parameters, options) { - let polygons = this._createPolygons(parameters, options); - let multipolygon = this._createGroup(polygons, options); - - return multipolygon; + exportsGeometry() { + return [["points"]]; } - - _createPolygons(parameters, options) { - // to do - //console.log(options); - //let outerPolygon = this._polygonFactory.create(parameters[0], options); // { ...options, globalCompositeOperation: "destination-out" }); - //let holes = parameters.slice(1).map(param => { - // let customOptions = { ...options, globalCompositeOperation: "destination-out" }; - // return this._polygonFactory.create(param, customOptions); - //}) - // - //return [outerPolygon, ...holes]; - return parameters.map(param => this._polygonFactory.create(param, options)); + + isImplicit() { + return false; } - - _createGroup(polygons, options) { - let multipolygon = new fabric.Group(polygons); + + create(parameters, options) { + const path = this._createPathFromPoints(parameters); + let multipolygon = new fabric.Path(path); + multipolygon = this.configure(multipolygon, options); - - this._adjustPointsRelativeToGroup(multipolygon); - multipolygon.globalCompositeOperation = "source-over"; + multipolygon.points = parameters; return multipolygon; } - _adjustPointsRelativeToGroup(group) { - group._objects.forEach(obj => { - obj.points = obj.points.map(p => ({ - x: p.x - (group.left + group.width / 2), - y: p.y - (group.top + group.height / 2) - })); - obj.setCoords(); - }); - } - configure(object, options) { const instance = super.configure(object, options); - + instance.fillRule = "evenodd"; delete instance.points; + return instance } - restoreOriginalCoords(points, multipolygon) { - return points.map(p => ({ - x: p.x + (multipolygon.left + multipolygon.width / 2), - y: p.y + (multipolygon.top + multipolygon.height / 2) - })); - } + _createPathFromPoints(multiPoints) { + if (multiPoints.length === 0) return; + let pathString = ''; - swapHoles(group, newParameters) { - const props = this.copyProperties(group); - delete props.left; - delete props.top; - delete props.width; - delete props.height; - delete props.points; + for (let i = 0; i < multiPoints.length; i++) { + const points = multiPoints[i]; - let newPolygons = this._createPolygons(newParameters, props); - group._objects = newPolygons; + if (i !== 0) pathString += ' '; + pathString += `M ${points[0].x} ${points[0].y}`; - group._calcBounds(); - group.setCoords(); - - this._adjustPointsRelativeToGroup(group); - return group; + points.forEach(point => { + pathString += ` L ${point.x} ${point.y}`; + }); + + pathString += ' z'; + if (i !== multiPoints.length) pathString += ' '; + } + + return pathString; } copy(ofObject, parameters=undefined) { @@ -1690,62 +1664,25 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto delete props.height; delete props.points; - if (!Array.isArray(parameters)) { - parameters = ofObject._objects.map(obj => { - return this.restoreOriginalCoords(obj.points, ofObject); - }); - } - return this.create(parameters, props);; - } - - exportsGeometry() { - return [["points"]]; + if (!parameters) parameters = ofObject.points; + return this.create(parameters, props); } - // to do - //edit(theObject) { - //} - //recalculate(theObject) { - //} + setPoints(object, points) { + object.points = points; + const newPathString = this._createPathFromPoints(points); + object._setPath(fabric.util.parsePath(newPathString)); + object.setCoords(); + return object; + } + getArea(theObject) { - let area = this._polygonFactory.getArea(theObject._objects[0]); + let area = this._polygonFactory.getArea({points: theObject.points[0]}); - for (let i = 1; i < theObject._objects.length; i++) { - area -= this._polygonFactory.getArea(theObject._objects[i]); + for (let i = 1; i < theObject.points.length; i++) { + area -= this._polygonFactory.getArea({points: theObject.points[i]}); } - return area; } - - onZoom(ofObject, graphicZoom, realZoom) { - ofObject._objects.forEach(o => { - o.set({strokeWidth: ofObject.originalStrokeWidth/graphicZoom}); - }); - } - - isImplicit() { - return false; - } - - updateRendering(ofObject, preset, visualProperties, defaultVisualProperties) { - console.log(preset); - this._polygonFactory.updateRendering(ofObject._objects[0], preset, visualProperties, defaultVisualProperties); - - //if (visualProperties.modeOutline) { - // visualProperties.globalCompositeOperation = "source-over"; - //} else { - // visualProperties.globalCompositeOperation = "destination-out"; - //} - - ofObject._objects.slice(1).forEach(o => { - this._polygonFactory.updateRendering(o, preset, visualProperties, defaultVisualProperties); - }); - } - - getMultipolygonPoints(multipolygon) { - return multipolygon._objects.map(polygon => - this.restoreOriginalCoords(polygon.points, multipolygon) - ); - } }; diff --git a/modules/annotations/objects.js b/modules/annotations/objects.js index 73c9074..47f9030 100644 --- a/modules/annotations/objects.js +++ b/modules/annotations/objects.js @@ -586,30 +586,25 @@ OSDAnnotations.PolygonUtilities = { }, approximatePolygonArea: function (points) { - if (points.length < 3) return { diffX: 0, diffY: 0 }; - let maxX = points[0].x, minX = points[0].x, maxY = points[0].y, minY = points[0].y; - for (let i = 1; i < points.length; i++) { - maxX = Math.max(maxX, points[i].x); - maxY = Math.max(maxY, points[i].y); - minX = Math.min(minX, points[i].x); - minY = Math.min(minY, points[i].y); - } - return { diffX: maxX - minX, diffY: maxY - minY }; + if (!points || points.length < 3) return { diffX: 0, diffY: 0 }; + const bbox = this.getBoundingBox(points); + + return { diffX: bbox.width, diffY: bbox.height }; }, - getBoundingBox: function (polygonPoints) { - if (!polygonPoints || polygonPoints.length === 0) return null; + getBoundingBox: function (points) { + if (!points || points.length === 0) return null; - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let maxX = points[0].x, minX = points[0].x, maxY = points[0].y, minY = points[0].y; - for (let i = 0; i < polygonPoints.length; i++) { - const point = polygonPoints[i]; + for (let i = 0; i < points.length; i++) { + const point = points[i]; minX = Math.min(minX, point.x); minY = Math.min(minY, point.y); maxX = Math.max(maxX, point.x); maxY = Math.max(maxY, point.y); } - + return { x: minX, y: minY, From 7ede03469f3d84add40a291adf71fa494b78b95a Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Tue, 10 Dec 2024 00:40:03 +0100 Subject: [PATCH 4/7] fix: multipolygon export issue, unwanted contour removal in fftoolremove --- modules/annotations/freeFormTool.js | 60 ++++++++++--------- modules/annotations/objectGenericFactories.js | 9 ++- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/modules/annotations/freeFormTool.js b/modules/annotations/freeFormTool.js index 2ac52bb..3445381 100644 --- a/modules/annotations/freeFormTool.js +++ b/modules/annotations/freeFormTool.js @@ -270,22 +270,18 @@ OSDAnnotations.FreeFormTool = class { this._ctx2d.closePath(); } - _rasterizePolygons(originalPoints, isPolygon) { - let points = []; - let firstPolygon; - - if (isPolygon) { - points = originalPoints.map(point => this.ref.imageToWindowCoordinates(new OpenSeadragon.Point(point.x, point.y))); - firstPolygon = points; - } else { - points = originalPoints.map(subPolygonPoints => { - return subPolygonPoints.map(point => - this.ref.imageToWindowCoordinates(new OpenSeadragon.Point(point.x, point.y)) - ); - }); - - firstPolygon = points[0]; + _rasterizePolygons(originalPoints, isPolygon, needsConversion=true) { + const convertPoints = (points) => + points.map(point => this.ref.imageToWindowCoordinates(new OpenSeadragon.Point(point.x, point.y))); + + if (needsConversion) { + originalPoints = isPolygon + ? convertPoints(originalPoints) + : originalPoints.map(convertPoints); } + + const points = originalPoints; + const firstPolygon = isPolygon ? points : points[0]; this._ctx2d.beginPath(); this._drawPolygon(firstPolygon); @@ -368,45 +364,54 @@ OSDAnnotations.FreeFormTool = class { if (falseOuterContours.length !== 0) { innerContours = innerContours.filter(inner => { return falseOuterContours.some(outer => { - if (polygonUtils.intersectAABB(polygonUtils.getBoundingBox(outer.points), polygonUtils.getBoundingBox(inner.points))) { + + const innerBbox = polygonUtils.getBoundingBox(inner.points); + const outerBbox = polygonUtils.getBoundingBox(outer.points); + + if (polygonUtils.intersectAABB(innerBbox, outerBbox)) { const intersections = OSDAnnotations.checkPolygonIntersect(inner.points, outer.points); return intersections.length === 0 || JSON.stringify(intersections) === JSON.stringify(outer.points); } return true; }); }); - } + + this._ctx2d.fillStyle = 'white'; + this._ctx2d.clearRect(0, 0, this._ctx2d.canvas.width, this._ctx2d.canvas.height); + + const newContours = [outerContour, ...innerContours].map(contour => contour.points); + this._rasterizePolygons(newContours, false, false); + } return [outerContour, ...innerContours]; } _processContours(nextMousePos, fillColor) { if (!this.polygon || this._toDistancePointsAsObjects(this.mousePos, nextMousePos) < this.radius / 3) return false; - + this.mousePos = nextMousePos; this._ctx2d.fillStyle = fillColor; - - let contours = this._getContours(); - contours = this._getValidContours(contours); - + let contours = this._getContours(); + if (contours.length >= 1 && contours[0].inner) return false; if (contours.length === 0) return this.finish(true); // deletion in subtract mode if (contours.length === 1) { // polygon - if (this.initial.factoryID !== "multipolygon") { + if (this.polygon.factoryID !== "multipolygon") { this.polygon.set({ points: contours[0].points }); - this.polygon._setPositionDimensions({}); } else { this._changeFactory(this._context.polygonFactory, contours[0].points); } + + this.polygon._setPositionDimensions({}); return true; } // multipolygon let contourPoints = contours.map(contour => contour.points); - if (this.initial.factoryID === "multipolygon") { + if (this.polygon.factoryID === "multipolygon") { this.polygon = this._context.objectFactories.multipolygon.setPoints(this.polygon, contourPoints); } else { this._changeFactory(this._context.objectFactories.multipolygon, contourPoints); @@ -431,13 +436,12 @@ OSDAnnotations.FreeFormTool = class { if (!mask.bounds) return []; let contours = this.MagicWand.traceContours(mask); + contours = this._getValidContours(contours); contours = this.MagicWand.simplifyContours(contours, 0, 30); const imageContours = contours.map(contour => ({ ...contour, - points: contour.points.map(point => - this.ref.windowToImageCoordinates(new OpenSeadragon.Point(point.x, point.y)) - ) + points: contour.points.map(point => this.ref.windowToImageCoordinates(new OpenSeadragon.Point(point.x, point.y))) })); return imageContours; diff --git a/modules/annotations/objectGenericFactories.js b/modules/annotations/objectGenericFactories.js index 4361742..4793975 100644 --- a/modules/annotations/objectGenericFactories.js +++ b/modules/annotations/objectGenericFactories.js @@ -1595,7 +1595,7 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto } fabricStructure() { - return ["polygon"]; + return "path"; } getDescription(ofObject) { @@ -1611,7 +1611,11 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto } exportsGeometry() { - return [["points"]]; + return ["path"]; + } + + exports() { + return ["points"]; } isImplicit() { @@ -1630,7 +1634,6 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto configure(object, options) { const instance = super.configure(object, options); instance.fillRule = "evenodd"; - delete instance.points; return instance } From ebabfdab24877872fe7088f5705d058e956317a0 Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Tue, 10 Dec 2024 18:50:15 +0100 Subject: [PATCH 5/7] fixed small typos and copy function in multipolygon factory --- modules/annotations/annotations.js | 6 +++++- modules/annotations/freeFormTool.js | 3 ++- modules/annotations/objectGenericFactories.js | 9 +++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/annotations/annotations.js b/modules/annotations/annotations.js index a516b8d..d1d7789 100644 --- a/modules/annotations/annotations.js +++ b/modules/annotations/annotations.js @@ -2274,7 +2274,11 @@ OSDAnnotations.StateFreeFormTool = class extends OSDAnnotations.AnnotationState if (holes) { notFullyInHoles = holes.every(hole => { - if (polygonUtils.intersectAABB(polygonUtils.getBoundingBox(brushPolygon), polygonUtils.getBoundingBox(hole))) { + + const bboxBrush = polygonUtils.getBoundingBox(brushPolygon); + const bboxHole = polygonUtils.getBoundingBox(hole); + + if (polygonUtils.intersectAABB(bboxBrush, bboxHole)) { const preciseIntersection = OSDAnnotations.checkPolygonIntersect(brushPolygon, hole); return !(JSON.stringify(preciseIntersection) === JSON.stringify(brushPolygon)); } diff --git a/modules/annotations/freeFormTool.js b/modules/annotations/freeFormTool.js index 3445381..3ecae1b 100644 --- a/modules/annotations/freeFormTool.js +++ b/modules/annotations/freeFormTool.js @@ -338,7 +338,7 @@ OSDAnnotations.FreeFormTool = class { const polygonUtils = OSDAnnotations.PolygonUtilities; let innerContours = []; let falseOuterContours = []; - let maxArea = -1; + let maxArea = 0; let outerContour = null; for (let i = 0; i < contours.length; i++) { @@ -361,6 +361,7 @@ OSDAnnotations.FreeFormTool = class { if (!outerContour) return innerContours; + // deleting inner contours (holes) which are found inside deleted outer contours if (falseOuterContours.length !== 0) { innerContours = innerContours.filter(inner => { return falseOuterContours.some(outer => { diff --git a/modules/annotations/objectGenericFactories.js b/modules/annotations/objectGenericFactories.js index 4793975..8fc85f4 100644 --- a/modules/annotations/objectGenericFactories.js +++ b/modules/annotations/objectGenericFactories.js @@ -1586,7 +1586,6 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto constructor(context, autoCreationStrategy, presetManager) { super(context, autoCreationStrategy, presetManager, "multipolygon", "path"); - this._current = null; this._polygonFactory = new OSDAnnotations.Polygon(context, autoCreationStrategy, presetManager); } @@ -1602,10 +1601,6 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto return `Multipolygon [${Math.round(ofObject.left)}, ${Math.round(ofObject.top)}]`; } - getCurrentObject() { - return this._current; - } - title() { return "Multipolygon"; } @@ -1668,7 +1663,9 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto delete props.points; if (!parameters) parameters = ofObject.points; - return this.create(parameters, props); + props.points = parameters; + + return new fabric.Path(this._createPathFromPoints(parameters), props); } setPoints(object, points) { From b35da5bab70c1ce8c3ddb15fe86e8c29cb4b543d Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Sat, 21 Dec 2024 22:54:01 +0100 Subject: [PATCH 6/7] fixed configure in generic object factories, deleted some comments --- modules/annotations/objectGenericFactories.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/modules/annotations/objectGenericFactories.js b/modules/annotations/objectGenericFactories.js index 8fc85f4..d4831b1 100644 --- a/modules/annotations/objectGenericFactories.js +++ b/modules/annotations/objectGenericFactories.js @@ -715,7 +715,7 @@ OSDAnnotations.Point = class extends OSDAnnotations.Ellipse { } }; -OSDAnnotations.ExplicitPointsObjectFactory = class extends OSDAnnotations.AnnotationObjectFactory { /// +OSDAnnotations.ExplicitPointsObjectFactory = class extends OSDAnnotations.AnnotationObjectFactory { constructor(context, autoCreationStrategy, presetManager, factoryID, type, fabricClass, withHelperPoints=true) { super(context, autoCreationStrategy, presetManager, factoryID, type); @@ -1337,7 +1337,7 @@ OSDAnnotations.Line = class extends OSDAnnotations.AnnotationObjectFactory { } }; -OSDAnnotations.Polygon = class extends OSDAnnotations.ExplicitPointsObjectFactory { /// +OSDAnnotations.Polygon = class extends OSDAnnotations.ExplicitPointsObjectFactory { constructor(context, autoCreationStrategy, presetManager) { super(context, autoCreationStrategy, presetManager, "polygon", "polygon", fabric.Polygon, true); } @@ -1420,7 +1420,7 @@ OSDAnnotations.Group = class extends OSDAnnotations.AnnotationObjectFactory { * @param {Array} parameters array of objects * @param {Object} options see parent class */ - create(parameters, options) { ///// + create(parameters, options) { return this.configure(new fabric.Group(parameters), options); } @@ -1429,13 +1429,11 @@ OSDAnnotations.Group = class extends OSDAnnotations.AnnotationObjectFactory { //todo use factory instead object.forEachObject(o => { - //o.fill = options.fill; + o.fill = options.fill; o.stroke = options.stroke; o.color = options.color; o.originalStrokeWidth = options.originalStrokeWidth; }); - - return object; } _eachChildAndFactory(ofObject, executor, method="map") { @@ -1621,16 +1619,14 @@ OSDAnnotations.Multipolygon = class extends OSDAnnotations.AnnotationObjectFacto const path = this._createPathFromPoints(parameters); let multipolygon = new fabric.Path(path); - multipolygon = this.configure(multipolygon, options); + this.configure(multipolygon, options); multipolygon.points = parameters; return multipolygon; } configure(object, options) { - const instance = super.configure(object, options); - instance.fillRule = "evenodd"; - - return instance + super.configure(object, options); + object.fillRule = "evenodd"; } _createPathFromPoints(multiPoints) { From 1827c078d6dade005e1935117f27cd6ce59a0e8c Mon Sep 17 00:00:00 2001 From: Simona Sarvasova Date: Mon, 23 Dec 2024 17:42:39 +0100 Subject: [PATCH 7/7] added test for intersection while moving mouse --- modules/annotations/freeFormTool.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/annotations/freeFormTool.js b/modules/annotations/freeFormTool.js index 3ecae1b..e1a5d8a 100644 --- a/modules/annotations/freeFormTool.js +++ b/modules/annotations/freeFormTool.js @@ -210,8 +210,15 @@ OSDAnnotations.FreeFormTool = class { } try { - this._updatePerformed = this._update(point) || this._updatePerformed; - this._context.canvas.renderAll(); + const cursorPolygon = this.getCircleShape(point); + const polygon = this.polygon.factoryID === "multipolygon" ? this.polygon.points[0] : this.polygon.points; + + const intersect = OSDAnnotations.checkPolygonIntersect(cursorPolygon, polygon); + if (intersect.length !== 0) { + this._updatePerformed = this._update(point) || this._updatePerformed; + this._context.canvas.renderAll(); + } + } catch (e) { console.warn("FreeFormTool: something went wrong, ignoring...", e); }