fix: annotation behavioral fixes and IO stability#175
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request improves annotation handling and conversion across ASAP-XML, GeoJSON, and QuPath formats, specifically adding better support for 'line' and 'ruler' types, and fixing coordinate calculations for Ruler and Line annotations. The review feedback highlights several critical safety improvements, including avoiding passing the entire fabric object to factory.configure (which risks circular reference copying), filtering out sparse array slots during ASAP-XML decoding, adding null-checks for missing factories in GeoJSON converters, and filtering out undefined presets in the GUI.
| const factory = self.getAnnotationObjectFactory(obj.factoryID); | ||
| factory?.configure?.(obj, obj); |
There was a problem hiding this comment.
Passing the entire fabric object obj as the options parameter to factory.configure is highly inefficient and dangerous. In Ruler.configure, $.extend({}, options, ...) and Object.assign({}, options) are called, which will attempt to clone/copy all properties of the fabric instance (including circular references, canvas references, etc.). This can lead to severe performance degradation, memory bloat, or runtime errors.
Instead, extract and pass only the necessary properties as a plain object.
const factory = self.getAnnotationObjectFactory(obj.factoryID);
factory?.configure?.(obj, {
color: obj.color,
presetID: obj.presetID,
zoomAtCreation: obj.zoomAtCreation,
layerID: obj.layerID
});| if (factory && typeof factory.fromPointArray === "function" && | ||
| typeof factory.create === "function") { | ||
| try { | ||
| const arrPts = pointArray.map(p => [p.x, p.y]); |
There was a problem hiding this comment.
If pointArray is sparse (e.g., due to missing or non-sequential Order indices in the XML), pointArray.map will preserve the empty slots as undefined. When passed to fromPointArray, destructuring undefined will throw a TypeError and crash the import process.
Filter out empty/undefined slots to ensure a dense array of coordinates is passed to the factory.
| const arrPts = pointArray.map(p => [p.x, p.y]); | |
| const arrPts = pointArray.filter(p => p).map(p => [p.x, p.y]); |
| const factory = this.context.getAnnotationObjectFactory(object.factoryID); | ||
| const converter = OSDAnnotations.AnnotationObjectFactory.withArrayPoint; | ||
| object._objects = object.objects; // todo ugly, factory works with live objects | ||
| const props = factory.copyNecessaryProperties(object, [], true); |
There was a problem hiding this comment.
If factory is undefined, calling factory.copyNecessaryProperties will throw a TypeError. Add a null check to prevent potential crashes if the ruler factory is not registered.
| const props = factory.copyNecessaryProperties(object, [], true); | |
| const props = factory ? factory.copyNecessaryProperties(object, [], true) : {}; |
| const factory = this.context.getAnnotationObjectFactory("line"); | ||
| const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y })); | ||
| const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties); | ||
| return factory.create(params, opts); |
There was a problem hiding this comment.
If the line factory is not registered or found, calling factory.fromPointArray will throw a TypeError. Add a guard to safely handle cases where the factory is missing.
const factory = this.context.getAnnotationObjectFactory("line");
if (!factory) return null;
const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y }));
const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties);
return factory.create(params, opts);| const factory = this.context.getAnnotationObjectFactory("ruler"); | ||
| const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y })); | ||
| const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties); | ||
| const obj = factory.create(params, opts); |
There was a problem hiding this comment.
If the ruler factory is not registered or found, calling factory.fromPointArray will throw a TypeError. Add a guard to safely handle cases where the factory is missing.
const factory = this.context.getAnnotationObjectFactory("ruler");
if (!factory) return null;
const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y }));
const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties);
const obj = factory.create(params, opts);| groups.push({ | ||
| name: "Unknown / Unsorted", | ||
| presets: unknown | ||
| presets: unknown.map(pId => presetManager.get(pId)) |
There was a problem hiding this comment.
If presetManager.get(pId) returns undefined for any of the unknown preset IDs, the presets array will contain undefined values, which can cause rendering issues or crashes later. Filter out any falsy/undefined values to ensure a clean array of presets.
| presets: unknown.map(pId => presetManager.get(pId)) | |
| presets: unknown.map(pId => presetManager.get(pId)).filter(Boolean) |
No description provided.