Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inset feature #549

Merged
merged 21 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
035f86c
Initial working inset button
will-moore Mar 23, 2024
838e0ef
Add roi ID handling to paste_panels
will-moore Mar 24, 2024
d3bcd96
Fix default page size to A4
will-moore Apr 8, 2024
efabd1b
handle inset ROI change to update viewport
will-moore Apr 8, 2024
a703ef7
Update insetRoiId logic when copy/paste panels
will-moore Apr 8, 2024
05cb9d9
Merge remote-tracking branch 'origin/master' into inset_feature
will-moore Apr 9, 2024
529d4ed
Tweak 'Create Inset' button
will-moore Apr 9, 2024
f137dd7
tidy console.log and docstrings
will-moore Apr 10, 2024
9f0215a
comment out FillOpacity/FillColor from shapeEditorTest.js
will-moore Apr 10, 2024
ed608c9
Add rotation support to shapeEditor Rectangle
will-moore Apr 10, 2024
c00b40a
Add rotation support to inset
will-moore Apr 10, 2024
f44ffd6
Merge remote-tracking branch 'origin/master' into inset_feature
will-moore Apr 29, 2024
fb7d54c
Try to handle Rectangle rotation when resizing
will-moore May 1, 2024
6b34dcc
Make sure inset panel is square, even if parent panel isn't
will-moore May 7, 2024
8ec99c2
Fix offset of new panel when parent isn't square
will-moore Jul 2, 2024
f7d26b9
Add tooltip to Create Inset button
will-moore Jul 2, 2024
df2733a
Add widgets for inset position, color, width
will-moore Aug 23, 2024
c7743ae
Fix line-width dropdown with .dropdown_icon
will-moore Aug 29, 2024
09ffee7
Fix Rectangle rotation on Inset creation
will-moore Aug 30, 2024
d5abf96
remove console.log, add code comments
will-moore Sep 4, 2024
65b0d53
Deleting inset panel also deletes inset ROI
will-moore Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/js/models/figure_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
initialize: function() {
this.panels = new PanelList(); //this.get("shapes"));

// listen for new Panels added so we can pass in a reference
// to this figureModel...
this.panels.on('add', (panel) => {
panel.setFigureModel(this);
});

// wrap selection notification in a 'debounce', so that many rapid
// selection changes only trigger a single re-rendering
this.notifySelectionChange = _.debounce( this.notifySelectionChange, 10);
Expand Down Expand Up @@ -81,7 +87,7 @@
'paper_height': data.paper_height,
'width_mm': data.width_mm,
'height_mm': data.height_mm,
'page_size': data.page_size || 'letter',
'page_size': data.page_size || 'A4',
'page_count': data.page_count,
'paper_spacing': data.paper_spacing,
'page_col_count': data.page_col_count,
Expand Down
84 changes: 84 additions & 0 deletions src/js/models/panel_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
// Attributes can be added as we need them.
export var Panel = Backbone.Model.extend({

// see setFigureModel() below
figureModel: undefined,

defaults: {
x: 100, // coordinates on the 'paper'
y: 100,
Expand All @@ -35,7 +38,79 @@
},

initialize: function() {
// listen for changes to the viewport...
this.on("change:zoom change:dx change:dy change:width change:height change:rotation", (panel) => {
// if this panel has 'insetRoiId' it is an "inset" panel so we need to
// notify other panels that contain the corresponding ROI (rectangle)
if (this.get('insetRoiId') && this.figureModel) {
if (!this.silenceTriggers) {
this.figureModel.trigger('zoomPanUpdated', panel);
}
}
});
// listen for changes to shapes...
this.on("change:shapes", (panel) => {
// notify all other panels via the figureModel...
console.log("trigger shapesUpdated...");
if (!this.silenceTriggers && this.figureModel) {
this.figureModel.trigger('shapesUpdated', panel);
}
});
},

setFigureModel(figureModel) {
this.figureModel = figureModel;
// listen for *other* panels zooming - might need to update "inset" ROI...
this.listenTo(figureModel, 'zoomPanUpdated', this.handleZoomPanChange);
// listen for *other* panels shape updates - might need to update zoom/pan...
this.listenTo(figureModel, 'shapesUpdated', this.handleShapesChange);
},

handleShapesChange(panel) {
// The ROI rectangle has been updated on another panel - update viewport to match...
// Update zoom/panel but don't trigger zoomPanUpdated or we could get
// recursive feedback
var insetRoiId = this.get('insetRoiId');
if (!insetRoiId) return;

this.silenceTriggers = true;

// find Rectangle from panel that corresponds to this panel
// TODO: use shape.insetRoiId for storing the ID, instead of the shape's actual id
let rect = (panel.get("shapes") || []).find(shape => shape.id == insetRoiId);
if (rect) {
this.cropToRoi(rect);
}

this.silenceTriggers = false;
},

handleZoomPanChange(panel) {
// An inset panel has zoomed/panned. If we have corresponding inset Rectangle
// then update it accordingly...
var insetRoiId = panel.get('insetRoiId');
if (!insetRoiId) return;
// find Rectangles that have insetRoiId...
let insetShapes = this.get('shapes');
if (insetShapes) {
insetShapes = insetShapes.filter(sh => sh.type == 'Rectangle');

if (insetShapes.length > 0) {
this.silenceTriggers = true;

let rect = panel.getViewportAsRect();
// update the "inset" Rectangle
let updated = this.get('shapes').map(shape => {
if (shape.type == 'Rectangle' && shape.id == insetRoiId) {
return {...shape, ...rect}
}
return shape;
});
this.save('shapes', updated);

this.silenceTriggers = false;
}
}
},

// When we're creating a Panel, we process the data a little here:
Expand Down Expand Up @@ -614,7 +689,10 @@

var toSet = { 'width': newW, 'height': newH, 'dx': dx, 'dy': dy, 'zoom': zoom };
var rotation = coords.rotation || 0;
// if the rectangle/viewport is rotated clockwise, the image within the
// viewport is rotated anti-clockwise
if (!isNaN(rotation)) {
rotation = -(rotation - 360);
toSet.rotation = rotation;
}
this.save(toSet);
Expand All @@ -626,6 +704,12 @@
dx = dx !== undefined ? dx : this.get('dx');
dy = dy !== undefined ? dy : this.get('dy');
var rotation = this.get('rotation');
if (isNaN(rotation)) {
rotation = 0;
};
// if we have rotated the panel clockwise within the viewport
// it's as if the viewport rectangle has rotated anti-clockwise
rotation = 360 - rotation;

var width = this.get('width'),
height = this.get('height'),
Expand Down
7 changes: 7 additions & 0 deletions src/js/shapeEditorTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,19 @@ $(function() {
"strokeWidth": 4});

shapeManager.addShapeJson({"id": 1234,
"rotation": 45,
"type": "Rectangle",
"strokeColor": "#ff00ff",
"strokeWidth": 6,
"x": 200, "y": 150,
"width": 125, "height": 150});

shapeManager.addShapeJson({"type": "Rectangle",
"strokeColor": "#ffffff",
"strokeWidth": 3,
"x": 50, "y": 300,
"width": 50, "height": 100});

shapeManager.addShapeJson({"type": "Ellipse",
"x": 200, "y": 150,
"radiusX": 125, "radiusY": 50,
Expand Down
33 changes: 32 additions & 1 deletion src/js/shape_editor/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var Rect = function Rect(options) {
if (options.zoom) {
this._zoomFraction = options.zoom / 100;
}
this._rotation = options.rotation || 0;
this.handle_wh = 6;

this.element = this.paper.rect();
Expand Down Expand Up @@ -104,6 +105,7 @@ Rect.prototype.toJson = function toJson() {
'area': this._width * this._height,
strokeWidth: this._strokeWidth,
strokeColor: this._strokeColor,
rotation: this._rotation,
};
if (this._id) {
rv.id = this._id;
Expand Down Expand Up @@ -258,6 +260,8 @@ Rect.prototype.drawShape = function drawShape() {
"stroke-width": lineW,
});

this.element.transform("r" + this._rotation + "," + (x + (w/2)) + "," + (y + (h/2)));

if (this.isSelected()) {
this.handles.show().toFront();
} else {
Expand All @@ -273,6 +277,7 @@ Rect.prototype.drawShape = function drawShape() {
hx = handleIds[h_id][0];
hy = handleIds[h_id][1];
hnd.attr({ x: hx - this.handle_wh / 2, y: hy - this.handle_wh / 2 });
hnd.transform("r" + this._rotation + "," + (x + (w/2)) + "," + (y + (h/2)));
}
};

Expand All @@ -296,6 +301,22 @@ Rect.prototype.getHandleCoords = function getHandleCoords() {
return handleIds;
};

function correct_rotation(dx, dy, rotation) {
if (dx === 0 && dy === 0) {
return {x: dx, y: dy};
}
var length = Math.sqrt(dx * dx + dy * dy),
ang1 = Math.atan(dy/dx);
if (dx < 0) {
ang1 = Math.PI + ang1;
}
var angr = rotation * (Math.PI/180), // deg -> rad
ang2 = ang1 - angr;
dx = Math.cos(ang2) * length;
dy = Math.sin(ang2) * length;
return {x: dx, y: dy};
}

// ---- Create Handles -----
Rect.prototype.createHandles = function createHandles() {
var self = this,
Expand All @@ -315,6 +336,12 @@ Rect.prototype.createHandles = function createHandles() {
return function (dx, dy, mouseX, mouseY, event) {
dx = dx / self._zoomFraction;
dy = dy / self._zoomFraction;
// need to handle rotation...
if (self._rotation != 0) {
let xy = correct_rotation(dx, dy, self._rotation);
dx = xy.x;
dy = xy.y;
}

// If drag on corner handle, retain aspect ratio. dx/dy = aspect
var keep_ratio = self.fixed_ratio || event.shiftKey;
Expand Down Expand Up @@ -397,6 +424,9 @@ Rect.prototype.createHandles = function createHandles() {
// var _stop_event_propagation = function(e) {
// e.stopImmediatePropagation();
// }
let cx = handleIds['n'][0];
let cy = handleIds['e'][1];

for (var key in handleIds) {
var hx = handleIds[key][0];
var hy = handleIds[key][1];
Expand All @@ -407,7 +437,8 @@ Rect.prototype.createHandles = function createHandles() {
self.handle_wh,
self.handle_wh
)
.attr(handle_attrs);
.attr(handle_attrs)
.rotate(self._rotation, cx, cy);
handle.attr({ cursor: key + "-resize" }); // css, E.g. ne-resize
handle.h_id = key;
handle.rect = self;
Expand Down
1 change: 1 addition & 0 deletions src/js/shape_editor/shape_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ ShapeManager.prototype.createShapeJson = function createShapeJson(jsonShape) {
options.width = s.width;
options.height = s.height;
options.area = s.width * s.height;
options.rotation = s.rotation;
newShape = new Rect(options);
} else if (s.type === "Line") {
options.x1 = s.x1;
Expand Down
7 changes: 6 additions & 1 deletion src/js/views/figure_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
showExportAsJsonModal,
showModal,
hideModals,
hideModal} from "./util";
hideModal,
updateRoiIds} from "./util";

// This extends Backbone to support keyboardEvents
backboneMousetrap(_, Backbone, Mousetrap);
Expand Down Expand Up @@ -571,6 +572,8 @@
var cd = [];
s.forEach(function(m) {
var copy = m.toJSON();
// deep copy (e.g. includes shapes)
copy = JSON.parse(JSON.stringify(copy));
delete copy.id;
cd.push(copy);
});
Expand Down Expand Up @@ -644,6 +647,8 @@

// apply offset to clipboard data & paste
// NB: we are modifying the list that is in the clipboard
clipboard_panels = updateRoiIds(clipboard_panels);

_.each(clipboard_panels, function(m) {
m.x = m.x + offset_x;
m.y = m.y + offset_y;
Expand Down
74 changes: 72 additions & 2 deletions src/js/views/right_panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import _ from "underscore";
import $ from "jquery";

import {figureConfirmDialog, showModal, rotatePoint} from "./util";
import {figureConfirmDialog, showModal, rotatePoint, getRandomId} from "./util";
import FigureColorPicker from "../views/colorpicker";

import FigureModel from "../models/figure_model";
Expand Down Expand Up @@ -110,11 +110,77 @@
"click .copyROIs": "copyROIs",
"click .pasteROIs": "pasteROIs",
"click .deleteROIs": "deleteROIs",
"click .create_inset": "createInset",
// triggered by select_dropdown_option below
"change .shape-color": "changeROIColor",
"change .line-width": "changeLineWidth",
},

createInset: function() {
let selected = this.model.getSelected();

// get bounding box of selected panels...
let minX = selected.reduce((prev, panel) => Math.min(panel.get('x'), prev), Infinity);
let maxX = selected.reduce((prev, panel) => Math.max(panel.get('x') + panel.get('width'), prev), -Infinity);
let minY = selected.reduce((prev, panel) => Math.min(panel.get('y'), prev), Infinity);
let maxY = selected.reduce((prev, panel) => Math.max(panel.get('y') + panel.get('height'), prev), -Infinity);
let selWidth = maxX - minX;
let selHeight = maxY - minY;

selected.forEach(panel => {
let randomId = getRandomId();
// Add Rectangle (square) in centre of viewport
let vp = panel.getViewportAsRect();
let minSide = Math.min(vp.width, vp.height);
// Square is 1/3 size of the viewport
let rectSize = minSide / 3;
var color = $('button.shape-color span:first', this.$el).attr('data-color');
var strokeWidth = parseFloat($('button.line-width span:first', this.$el).attr('data-line-width'));
let rect = {
type: "Rectangle",
strokeWidth,
strokeColor: "#" + color,
x: vp.x + ((vp.width - rectSize) / 2),
y: vp.y + ((vp.height - rectSize) / 2),
width: rectSize,
height: rectSize,
id: randomId,
}
panel.add_shapes([rect]);

// Create duplicate panels
let panelJson = panel.toJSON();

// want to make sure new panel is square
let maxSide = Math.min(panelJson.width, panelJson.height);
panelJson.width = maxSide;
panelJson.height = maxSide;

if (selWidth > selHeight) {
panelJson.y = panelJson.y + 1.1 * panel.get("height");
} else {
panelJson.x = panelJson.x + 1.1 * panel.get("width");
}
// cropped to match new Rectangle
let orig_width = panelJson.orig_width;
let orig_height = panelJson.orig_height;
let targetCx = Math.round(rect.x + (rect.width/2));
let targetCy = Math.round(rect.y + (rect.height/2));

panelJson.dx = (orig_width/2) - targetCx;
panelJson.dy = (orig_height/2) - targetCy;
// zoom to correct percentage
var xPercent = orig_width / rect.width;
var yPercent = orig_height / rect.height;
panelJson.zoom = Math.min(xPercent, yPercent) * 100;
panelJson.selected = false;
panelJson.shapes = [];
panelJson.insetRoiId = randomId;

this.model.panels.create(panelJson);
});
},

changeLineWidth: function() {
var width = $('button.line-width span:first', this.$el).attr('data-line-width'),
sel = this.model.getSelected();
Expand Down Expand Up @@ -950,7 +1016,11 @@
var shapeJson = clipboard_data.SHAPES;
shapeJson.forEach(function(shape) {
if (!rect && shape.type === "Rectangle") {
rect = {x: shape.x, y: shape.y, width: shape.width, height: shape.height};
rect = {
x: shape.x, y: shape.y,
width: shape.width, height: shape.height,
rotation: shape.rotation
};
}
});
if (!rect) {
Expand Down
Loading