Skip to content

Commit 92ea3be

Browse files
authored
Merge pull request #5 from bbsaclay/master
Fixing brush feature
2 parents e28c5c3 + 1683c14 commit 92ea3be

File tree

6 files changed

+91
-80
lines changed

6 files changed

+91
-80
lines changed

demos/segmentation-interactive/my-demo.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,11 @@ class MyDemo extends LitElement {
101101
<p class="icon" title="Add instance" @click=${() => this.element.mode = 'create'}>${createPencil}</p>
102102
<p class="icon" title="Smart instance" @click=${() => this.element.mode = 'smart-create'}>${borderOuter}</p>
103103
<p class="icon" title="Select" @click=${() => this.element.mode = 'select'}>${magicSelect}</p>
104-
<p class="icon" title="Subtract" @click=${() => this.element.mode = 'edit-remove'}>${subtract}</p>
105-
<p class="icon" title="Union" @click=${() => this.element.mode = 'edit-add'}>${union}</p>
104+
<p class="icon" title="Subtract (Ctrl)" @click=${() => this.element.mode = 'edit-remove'}>${subtract}</p>
105+
<p class="icon" title="Union (Shift)" @click=${() => this.element.mode = 'edit-add'}>${union}</p>
106106
<p class="icon" title="Lock" @click=${() => this.element.mode = 'lock'}>${lock}</p>
107-
<p class="icon" title="Zoom in" @click=${() => this.element.viewControls.zoomIn()}>${zoomIn}</p>
108-
<p class="icon" title="Zoom out" @click=${() => this.element.viewControls.zoomOut()}>${zoomOut}</p>
107+
<p class="icon" title="Zoom in (scroll)" @click=${() => this.element.viewControls.zoomIn()}>${zoomIn}</p>
108+
<p class="icon" title="Zoom out (scroll)" @click=${() => this.element.viewControls.zoomOut()}>${zoomOut}</p>
109109
</div>
110110
</div>
111111
`;

demos/segmentation/my-demo.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ class MyDemo extends LitElement {
106106
<p class="icon" title="Add instance" @click=${() => this.element.mode = 'create'}>${createPencil}</p>
107107
<p class="icon" title="Add instance" @click=${() => this.element.mode = 'create-brush'}>${paintBrush}</p>
108108
<p class="icon" title="Select" @click=${() => this.element.mode = 'select'}>${magicSelect}</p>
109-
<p class="icon" title="Subtract" @click=${() => this.element.mode = 'edit-remove'}>${subtract}</p>
110-
<p class="icon" title="Union" @click=${() => this.element.mode = 'edit-add'}>${union}</p>
109+
<p class="icon" title="Subtract (Ctrl)" @click=${() => this.element.mode = 'edit-remove'}>${subtract}</p>
110+
<p class="icon" title="Union (Shift)" @click=${() => this.element.mode = 'edit-add'}>${union}</p>
111111
<p class="icon" title="Lock" @click=${() => this.element.mode = 'lock'}>${lock}</p>
112-
<p class="icon" title="Zoom in" @click=${() => this.element.viewControls.zoomIn()}>${zoomIn}</p>
113-
<p class="icon" title="Zoom out" @click=${() => this.element.viewControls.zoomOut()}>${zoomOut}</p>
112+
<p class="icon" title="Zoom in (scroll)" @click=${() => this.element.viewControls.zoomIn()}>${zoomIn}</p>
113+
<p class="icon" title="Zoom out (scroll)" @click=${() => this.element.viewControls.zoomOut()}>${zoomOut}</p>
114114
</div>
115115
</div>
116116
`;

packages/graphics-2d/src/controller-mask.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,9 @@ export class CreateBrushController extends Controller {
225225
this.renderer.stage.removeListener('pointermove', this.onPointerMoveBrush);
226226
this.renderer.stage.removeListener('pointerupoutside', this.onPointerUpBrush);
227227
window.removeEventListener('keydown', this.onKeyDown, false);
228+
this.roi.cacheAsBitmap = false;
228229
this.roi.clear();
230+
this.roi.cacheAsBitmap = true;
229231
this.contours.visible = false;
230232
}
231233

@@ -285,10 +287,13 @@ export class CreateBrushController extends Controller {
285287
* @param evt PIXIInteractionEvent
286288
*/
287289
onPointerDownBrush(evt: PIXIInteractionEvent) {
290+
if (evt.data.button == 1) return;//middle button : nothing to do
291+
288292
this.isActive = true;
289293
this.roi.x = this.renderer.mouse.x;
290294
this.roi.y = this.renderer.mouse.y;
291295

296+
292297
if (evt.data.button === 0 && !evt.data.originalEvent.ctrlKey && !evt.data.originalEvent.shiftKey) {
293298
// create
294299
this.editionMode = EditionMode.NEW_INSTANCE;
@@ -299,6 +304,7 @@ export class CreateBrushController extends Controller {
299304
// remove
300305
this.editionMode = EditionMode.REMOVE_FROM_INSTANCE;
301306
}
307+
302308
const fillType = (this.editionMode === EditionMode.REMOVE_FROM_INSTANCE) ? 'remove' : 'add';
303309
this.gmask.updateByMaskInRoi(this.roiMatrix,
304310
[this.roi.x - this.roiRadius, this.roi.y - this.roiRadius, this.roi.x + this.roiRadius, this.roi.y + this.roiRadius],
@@ -312,15 +318,24 @@ export class CreateBrushController extends Controller {
312318
*/
313319
onPointerMoveBrush(evt: PIXIInteractionEvent) {
314320
const newPos = this.renderer.getPosition(evt.data);
315-
this.roi.x = newPos.x;
316-
this.roi.y = newPos.y;
317321
if (this.isActive) {
318322
const fillType = (this.editionMode === EditionMode.REMOVE_FROM_INSTANCE) ? 'remove' : 'add';
319323
this.gmask.updateByMaskInRoi(this.roiMatrix,
320-
[this.roi.x - this.roiRadius, this.roi.y - this.roiRadius, this.roi.x + this.roiRadius, this.roi.y + this.roiRadius],
324+
[newPos.x - this.roiRadius, newPos.y - this.roiRadius, newPos.x + this.roiRadius, newPos.y + this.roiRadius],
325+
this.getTargetValue(), fillType
326+
);
327+
// filling space between successive mousepoints to enable easy surface painting
328+
//... assuming roiMatrix is a circle
329+
//... filling by a strait line even if the user describes a curve
330+
const alpha = Math.atan2((newPos.y-this.roi.y),(newPos.x-this.roi.x));
331+
const dy = Math.trunc(-Math.cos(alpha)*this.roiRadius);
332+
const dx = Math.trunc(Math.sin(alpha)*this.roiRadius);
333+
this.gmask.updateByPolygon([new Point(this.roi.x+dx,this.roi.y+dy),new Point(this.roi.x-dx,this.roi.y-dy),new Point(newPos.x-dx,newPos.y-dy),new Point(newPos.x+dx,newPos.y+dy)],
321334
this.getTargetValue(), fillType
322335
);
323336
}
337+
this.roi.x = newPos.x;
338+
this.roi.y = newPos.y;
324339
}
325340

326341
/**
@@ -599,6 +614,7 @@ export class MaskManager extends EventTarget {
599614
this.gmask = gmask;
600615
this.selectedId = { value: selectedId };
601616
this.renderer.stage.addChild(this.contour);
617+
// creating default controler
602618
this.modes = {
603619
'create': new CreatePolygonController({...this} as any),
604620
'create-brush': new CreateBrushController({...this} as any),
@@ -610,6 +626,11 @@ export class MaskManager extends EventTarget {
610626
this.modes[this.mode].activate();
611627
}
612628

629+
/**
630+
* Change the controler linked to the mode:
631+
* @param mode string, the mode whome controler has to be changed
632+
* @param controller the new controler
633+
*/
613634
public setController(mode: string, controller: Controller) {
614635
if (mode === this.mode && this.modes[mode]) {
615636
// remove active base controller

packages/graphics-2d/src/graphic-mask.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -297,22 +297,33 @@ export class GraphicMask extends PIXIContainer {
297297
* Update panoptic mask with a localised submask and a panoptic value
298298
* @param mask binary flatten array
299299
* @param box [l,t,r,b] search area
300-
* @param newVal
300+
* @param newVal new pixel/class value (corresponds to color for rendering)
301301
*/
302302
public updateByMaskInRoi(mask: Float32Array, box: [number, number, number, number],
303303
newVal: [number, number, number], fillType: 'add' | 'remove' ='add') {
304304
const pixels = this.ctx.getImageData(0,0,this.canvas.width, this.canvas.height);
305+
306+
//respect image boundaries
305307
const width = box[2]-box[0];
306-
const height = box[3]-box[1];
308+
const roi = [(box[0]>0) ? box[0] : 0,
309+
(box[1]>0) ? box[1] : 0,
310+
(box[2]<this.canvas.width) ? box[2] : this.canvas.width,
311+
(box[3]<this.canvas.height) ? box[3] : this.canvas.height];
312+
const widthroi = roi[2]-roi[0];
313+
const heightroi = roi[3]-roi[1];
314+
const decx = (box[0]<0) ? -box[0] : 0;
315+
const decy = (box[1]<0) ? -box[1] : 0;
316+
317+
307318
const color = this.pixelToColor(...newVal);
308319
const alpha = (Math.max(...color) === 0) ? 0 : MASK_ALPHA_VALUE;
309320
const [id1, id2, cls] = newVal;
310321
if (fillType === 'add') {
311-
for (let x = 0; x < width; x++) {
312-
for (let y = 0; y < height; y++) {
313-
const idx = (x + box[0] + (y + box[1]) * this.canvas.width);
322+
for (let x = 0; x < widthroi; x++) {
323+
for (let y = 0; y < heightroi; y++) {
324+
const idx = (x + roi[0] + (y + roi[1]) * this.canvas.width);
314325
const pixId = this.pixelId(idx);
315-
if (mask[y * width + x] === 1 && !this.lockedInstances.has(fuseId(pixId))) {
326+
if (mask[(y+decy)*width + (x+decx)] === 1 && !this.lockedInstances.has(fuseId(pixId))) {
316327
this.orig!.data[4 * idx] = id1;
317328
this.orig!.data[4 * idx + 1] = id2;
318329
this.orig!.data[4 * idx + 2] = cls;
@@ -326,11 +337,11 @@ export class GraphicMask extends PIXIContainer {
326337
}
327338
} else if (fillType === 'remove') {
328339
const fusedVal = fuseId(newVal);
329-
for (let x = 0; x < width; x++) {
330-
for (let y = 0; y < height; y++) {
331-
const idx = (x + box[0] + (y + box[1]) * this.canvas.width);
340+
for (let x = 0; x < widthroi; x++) {
341+
for (let y = 0; y < heightroi; y++) {
342+
const idx = (x + roi[0] + (y + roi[1]) * this.canvas.width);
332343
const pixId = this.pixelId(idx);
333-
if (mask[y * width + x] === 1 && !this.lockedInstances.has(fuseId(pixId))
344+
if (mask[(y+decy)*width + (x+decx)] === 1 && !this.lockedInstances.has(fuseId(pixId))
334345
&& fuseId(pixId) == fusedVal) {
335346
this.orig!.data[4 * idx] = 0;
336347
this.orig!.data[4 * idx + 1] = 0;
@@ -350,13 +361,19 @@ export class GraphicMask extends PIXIContainer {
350361

351362
/**
352363
* Update panoptic mask with a polygon to be filled with given panoptic value
353-
* @param polygon
354-
* @param id
355-
* @param fillType
364+
* @param polygon an array of points (points must be represented by integers)
365+
* @param id new pixel/class value (corresponds to color for rendering)
366+
* @param fillType 'add' or 'remove'
356367
*/
357-
public updateByPolygon(polygon: Point[], id: [number, number, number], fillType='add') {
368+
public updateByPolygon(polygon: Point[], id: [number, number, number], fillType: 'add' | 'remove' ='add') {
358369
const pixels = this.ctx.getImageData(0,0,this.canvas.width, this.canvas.height);
359-
const [xMin, yMin, xMax, yMax] = getPolygonExtrema(polygon);
370+
let [xMin, yMin, xMax, yMax] = getPolygonExtrema(polygon);
371+
//respect image boundaries
372+
if (xMin<0) xMin=0;
373+
if (yMin<0) yMin=0;
374+
if (xMax>this.canvas.width) xMax=this.canvas.width-1;
375+
if (yMax>this.canvas.height) yMax=this.canvas.height-1;
376+
360377
if (this.lockedInstances.has(fuseId(id))) {
361378
// do not update locked instances
362379
return;

packages/graphics-2d/src/pxn-canvas-2d.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -282,18 +282,6 @@ export class Canvas2d extends Canvas {
282282
}
283283
}
284284

285-
/**
286-
* Snackbar temporary appearance
287-
* To display mode instructions.
288-
* @param text
289-
*/
290-
protected showTooltip(text: string) {
291-
const x = this.shadowRoot!.getElementById("snackbar")!;
292-
x.className = "show";
293-
x.innerHTML = text;
294-
setTimeout(() => { x.className = x.className.replace("show", ""); }, 3000);
295-
}
296-
297285
/**
298286
* Called on every property change
299287
* @param changedProperty

packages/graphics-2d/src/view-controls.ts

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -84,58 +84,43 @@ export class ViewControls extends EventTarget {
8484
* @param evt
8585
*/
8686
public onWheel(evt: WheelEvent) {
87-
if (evt.ctrlKey) {
88-
evt.preventDefault();
89-
}
90-
// Manipulate the scale based on direction
91-
const distance = this.wheelDistance(evt) / 5 * this.viewer.s;
92-
// if (this.s == this.smin && distance <= 0) {
93-
// return;
94-
// }
95-
const so = this.viewer.s;
96-
this.viewer.s += distance;
97-
const x = evt.offsetX;
98-
const y = evt.offsetY;
99-
100-
// Check to see that the scale is not outside of the specified bounds
101-
if (this.viewer.s > this.viewer.smax) {
102-
this.viewer.s = this.viewer.smax;
103-
} else if (this.viewer.s < this.viewer.smin) {
104-
// center placeholder if zoom is minimal
105-
this.viewer.s = this.viewer.smin;
106-
this.viewer.computeDrawableArea(this.viewer.canvasWidth, this.viewer.canvasHeight,
107-
this.viewer.imageWidth, this.viewer.imageHeight, true);
108-
this.viewer.sx = 0.5 * (1 - this.viewer.s) * this.viewer.rw;
109-
this.viewer.sy = 0.5 * (1 - this.viewer.s) * this.viewer.rh;
110-
}
111-
this.viewer.sx = (this.viewer.sx - x) * (this.viewer.s / so) + x;
112-
this.viewer.sy = (this.viewer.sy - y) * (this.viewer.s / so) + y;
113-
this.viewer.stage.scale.set(this.viewer.s * this.viewer.rw / this.viewer.imageWidth,
114-
this.viewer.s * this.viewer.rh / this.viewer.imageHeight);
115-
this.viewer.stage.position.set(this.viewer.rx * this.viewer.s + this.viewer.sx, this.viewer.ry * this.viewer.s + this.viewer.sy);
116-
this.triggerOnZoom();
117-
this.computeHitArea();
87+
if (evt.ctrlKey) evt.preventDefault();
88+
const scaleFactor = 1.0+Math.sign(this.wheelDistance(evt))*0.1;
89+
this.zoomFct(scaleFactor, [evt.x, evt.y]);
11890
}
11991

12092
public zoomIn() {
121-
this.viewer.s *= 1.1;
122-
this.viewer.stage.scale.set(this.viewer.s * this.viewer.rw / this.viewer.imageWidth,
123-
this.viewer.s * this.viewer.rh / this.viewer.imageHeight);
124-
this.viewer.stage.position.set(this.viewer.rx * this.viewer.s + this.viewer.sx, this.viewer.ry * this.viewer.s + this.viewer.sy);
125-
this.triggerOnZoom();
126-
this.computeHitArea();
93+
this.zoomFct(1.1, [this.viewer.rw/2, this.viewer.rh/2]);
12794
}
12895

12996
public zoomOut() {
130-
this.viewer.s *= 0.9;
131-
if (this.viewer.s < this.viewer.smin) {
132-
// center placeholder if zoom is minimal
97+
this.zoomFct(0.9, [this.viewer.rw/2, this.viewer.rh/2]);
98+
}
99+
100+
/**
101+
* Generalized zoom function
102+
* @param scaleFactor scale factor to be applied
103+
* @param center point where the zoom will be centered
104+
*/
105+
public zoomFct(scaleFactor: number, center: [number, number]) {
106+
// apply new scale
107+
const oldscale = this.viewer.s;
108+
this.viewer.s *= scaleFactor;
109+
// Check to see that the scale is not outside of the specified bounds
110+
if (this.viewer.s >= this.viewer.smax) {
111+
this.viewer.s = this.viewer.smax;
112+
} else if (this.viewer.s <= this.viewer.smin) {
133113
this.viewer.s = this.viewer.smin;
114+
// center placeholder if zoom is minimal
134115
this.viewer.sx = 0.5 * (1 - this.viewer.s) * this.viewer.rw;
135116
this.viewer.sy = 0.5 * (1 - this.viewer.s) * this.viewer.rh;
117+
} else {
118+
// apply zoom center (sx and sy are offsets)
119+
this.viewer.sx = (this.viewer.sx - center[0]) * (this.viewer.s / oldscale) + center[0];
120+
this.viewer.sy = (this.viewer.sy - center[1]) * (this.viewer.s / oldscale) + center[1];
136121
}
137-
this.viewer.stage.scale.set(this.viewer.s * this.viewer.rw / this.viewer.imageWidth,
138-
this.viewer.s * this.viewer.rh / this.viewer.imageHeight);
122+
// apply changes
123+
this.viewer.stage.scale.set(this.viewer.s * this.viewer.rw / this.viewer.imageWidth, this.viewer.s * this.viewer.rh / this.viewer.imageHeight);
139124
this.viewer.stage.position.set(this.viewer.rx * this.viewer.s + this.viewer.sx, this.viewer.ry * this.viewer.s + this.viewer.sy);
140125
this.triggerOnZoom();
141126
this.computeHitArea();

0 commit comments

Comments
 (0)