BioGTK  6.7.0
A .NET library & program for annotating, editing various microscopy imaging formats using Bioformats supported images. Including whole slide, pyramidal, and series.
Loading...
Searching...
No Matches
Tools.cs
1using AForge;
2using AForge.Imaging;
3using AForge.Imaging.Filters;
4using Bio;
5using Bio.Graphics;
6using Cairo;
7using CSScripting;
8using Gdk;
9using Gtk;
10using ScottPlot.Rendering;
11using System;
12using System.Collections.Generic;
13using System.Linq;
14using Color = Cairo.Color;
15using PointD = AForge.PointD;
16using Rectangle = AForge.Rectangle;
17namespace BioGTK
18{
22 public class Tools : Gtk.Window
23 {
24
25#pragma warning disable 649
26
27 [Builder.Object]
28 private Gtk.DrawingArea view;
29#pragma warning restore 649
30 private AForge.Color selectColor;
31 private Builder _builder;
36 public static Tools Create()
37 {
38 Builder builder = new Builder(new FileStream(System.IO.Path.GetDirectoryName(Environment.ProcessPath) + "/" + "Glade/Tools.glade", FileMode.Open));
39 return new Tools(builder, builder.GetObject("tools").Handle);
40 }
41 /* Initializing the tools. */
42 protected Tools(Builder builder, IntPtr handle) : base(handle)
43 {
44 selectColor = AForge.Color.FromArgb(0, 0, 150, 255);
45 AForge.Color white = AForge.Color.FromArgb(255, 255, 255, 255);
46 _builder = builder;
47 builder.Autoconnect(this);
49 Tool.Init();
50 ColorS col = new ColorS(ushort.MaxValue);
51 //We initialize the tools
52 currentTool = GetTool(Tool.Type.move.ToString());
53 floodFiller = new QueueLinearFloodFiller(floodFiller);
54 magicSelect = MagicSelect.Create(0);
55 App.ApplyStyles(this);
56 }
57 public static bool applyToStack = false;
58 public static ColorTool colorTool;
59 public static bool rEnabled = true;
60 public static bool gEnabled = true;
61 public static bool bEnabled = true;
62 public static ColorS drawColor = new ColorS(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
63 public static ColorS eraseColor = new ColorS(0, 0, 0);
64 public static ColorS tolerance = new ColorS(0, 0, 0);
65 private static MagicSelect magicSelect;
66 private static int width = 5;
67 private static ROI roi;
68 public static ROI selectedROI
69 {
70 get
71 {
72 List<ROI> rs = new List<ROI>();
73 foreach (var item in GetSelectableRois(ImageView.SelectedImage))
74 if (item != null && item.Selected)
75 rs.Add(item);
76 if (rs.Count > 0)
77 roi = rs[0];
78 return roi;
79 }
80 set
81 {
82 roi = value;
83 }
84 }
85 public static Rectangle selectionRectangle;
86 public static Dictionary<string, Tool> tools = new Dictionary<string, Tool>();
87 private static AbstractFloodFiller floodFiller = null;
88 static List<ROI> rois = new List<ROI>();
89 /* Creating a new ROI object called selectedROI. */
90 public static List<ROI> selectedROIs
91 {
92 get
93 {
94 rois.Clear();
95 for (int i = 0; i < Images.images.Count; i++)
96 {
97 foreach (var item in GetSelectableRois(Images.images[i]))
98 if (item != null && item.Selected)
99 rois.Add(item);
100 }
101 return rois;
102 }
103 set
104 {
105 rois.Clear();
106 rois.AddRange(value);
107 }
108 }
109 public static bool HitTestMask(ROI ann, PointD worldPoint)
110 {
111 if (ann == null || ann.type != ROI.Type.Mask || ann.roiMask == null)
112 return false;
113
114 double x = ann.X;
115 double y = ann.Y;
116 double w = ann.W;
117 double h = ann.H;
118 if (worldPoint.X < x || worldPoint.Y < y ||
119 worldPoint.X > x + w || worldPoint.Y > y + h)
120 return false;
121
122 if (ann.roiMask.PhysicalSizeX == 0 || ann.roiMask.PhysicalSizeY == 0)
123 return false;
124
125 int mx = (int)Math.Floor((worldPoint.X - x) / ann.roiMask.PhysicalSizeX);
126 int my = (int)Math.Floor((worldPoint.Y - y) / ann.roiMask.PhysicalSizeY);
127 if (mx < 0 || my < 0 || mx >= ann.roiMask.Width || my >= ann.roiMask.Height)
128 return false;
129
130 return ann.roiMask.IsSelected(mx, my);
131 }
132 void AddROI(ROI roi)
133 {
134 if (ImageView.SelectedImage.isPyramidal)
135 roi.serie = App.viewer.Level;
136 else
137 roi.serie = ImageView.SelectedImage.series;
138 roi.coord = App.viewer.GetCoordinate();
139 ImageView.SelectedImage.Annotations.Add(roi);
140 }
143 protected void SetupHandlers()
144 {
145 this.ButtonPressEvent += Tools_ButtonPressEvent;
146 ti = TextInput.Create();
147 view.Drawn += View_Drawn;
148 view.DeleteEvent += View_DeleteEvent;
149 }
150 private void View_DeleteEvent(object o, DeleteEventArgs args)
151 {
152 this.Hide();
153 args.RetVal = true;
154 }
155
156 static int gridW = 2;
157 static int gridH = 11;
158
159 static List<Rectangle> rects = new List<Rectangle>();
164 private void View_Drawn(object o, DrawnArgs e)
165 {
166 int w = 33;
167 int h = 33;
168 int x = 0;
169 int y = 0;
170 foreach (Tool item in tools.Values)
171 {
172 if (item.image != null)
173 {
174 e.Cr.LineWidth = 2;
175 Pixbuf pf = item.image.ScaleSimple(w, h, InterpType.Bilinear);
176 if (item.selected)
177 {
178 e.Cr.SetSourceRGB(item.color.Rf, item.color.Gf, item.color.Bf);
179 e.Cr.Rectangle(x * w, y * h, w, h);
180 e.Cr.Stroke();
181 }
182 Gdk.CairoHelper.SetSourcePixbuf(e.Cr, pf, w * x, h * y);
183 e.Cr.Paint();
184 e.Cr.Stroke();
185
186 }
187 else
188 {
189 e.Cr.SetSourceRGB(item.color.Rf, item.color.Gf, item.color.Bf);
190 e.Cr.Rectangle(x * w, y * h, w, h);
191 e.Cr.Fill();
192 e.Cr.Stroke();
193 }
194 if (x < gridW - 1)
195 {
196 x += 1;
197 }
198 else
199 {
200 y += 1;
201 x = 0;
202 }
203 }
204
205 }
206
212 private void Tools_ButtonPressEvent(object o, ButtonPressEventArgs args)
213 {
214 Gdk.Rectangle r = new Gdk.Rectangle((int)args.Event.X, (int)args.Event.Y, 1, 1);
215 Tool tool = null;
216 foreach (Tool t in tools.Values)
217 {
218 if (t.bounds.IntersectsWith(new AForge.PointD(args.Event.X, args.Event.Y)))
219 {
220 tool = t;
221 //if (selectedROI != null)
222 // selectedROI.Selected = false;
223 t.selected = true;
224 }
225 else
226 t.selected = false;
227 }
228 if (tool == null)
229 return;
230 currentTool = tool;
231 if (tool.type == Tool.Type.color1)
232 Color1Click();
233 if (tool.type == Tool.Type.color2)
234 Color2Click();
235 if (tool.type == Tool.Type.switchColors)
236 SwitchClick();
237 view.QueueDraw();
238 }
239 public static bool colorOne = true;
243 private void Color1Click()
244 {
245 if (App.color != null)
246 if (App.color.Visible)
247 return;
248 colorOne = true;
249 App.color = ColorTool.Create(false);
250 App.color.Show();
251 }
253 private void Color2Click()
254 {
255 if (App.color != null)
256 if (App.color.Visible)
257 return;
258 colorOne = false;
259 App.color = ColorTool.Create(true);
260 App.color.Show();
261 }
263 private void SwitchClick()
264 {
265 ColorS s = DrawColor;
266 DrawColor = EraseColor;
267 EraseColor = s;
268 }
269 private void UpdateView()
270 {
271 App.viewer.UpdateView();
272 }
273
274 // Normalize bounding box (makes W/H >= 0) and update ROI.Points to match corners
275 private void SyncPointsFromBoundingBox(ROI roi)
276 {
277 if (roi == null) return;
278 RectangleD r = roi.BoundingBox;
279
280 double left = Math.Min(r.X, r.X + r.W);
281 double top = Math.Min(r.Y, r.Y + r.H);
282 double right = Math.Max(r.X, r.X + r.W);
283 double bottom = Math.Max(r.Y, r.Y + r.H);
284
285 // update normalized bounding box
286 roi.BoundingBox = new RectangleD(left, top, right - left, bottom - top);
287
288 // Ensure Points list exists & has 4 corner points for rectangle/ellipse
289 if (roi.Points == null) roi.Points = new List<PointD>();
290
291 if (roi.Points.Count < 4)
292 {
293 roi.Points.Clear();
294 roi.Points.Add(new PointD(left, top)); // 0: TL
295 roi.Points.Add(new PointD(right, top)); // 1: TR
296 roi.Points.Add(new PointD(right, bottom)); // 2: BR
297 roi.Points.Add(new PointD(left, bottom)); // 3: BL
298 }
299 else
300 {
301 // Keep the index mapping consistent
302 roi.UpdatePoint(new PointD(left, top), 0);
303 roi.UpdatePoint(new PointD(right, top), 1);
304 roi.UpdatePoint(new PointD(right, bottom), 2);
305 roi.UpdatePoint(new PointD(left, bottom), 3);
306 }
307 }
308
309 // Recompute bounding box from the ROI points (useful after moving points)
310 private void SyncBoundingBoxFromPoints(ROI roi)
311 {
312 if (roi == null || roi.Points == null || roi.Points.Count == 0) return;
313
314 double minx = double.MaxValue, miny = double.MaxValue;
315 double maxx = double.MinValue, maxy = double.MinValue;
316
317 foreach (var p in roi.Points)
318 {
319 if (p.X < minx) minx = p.X;
320 if (p.Y < miny) miny = p.Y;
321 if (p.X > maxx) maxx = p.X;
322 if (p.Y > maxy) maxy = p.Y;
323 }
324
325 roi.BoundingBox = new RectangleD(minx, miny, maxx - minx, maxy - miny);
326 }
327
328
329 // -----------------------------
330 // Fixed tool implementations
331 // -----------------------------
332 public void ToolDown(PointD e, ButtonPressEventArgs buts)
333 {
334 if (App.viewer == null || currentTool == null || ImageView.SelectedImage == null)
335 return;
336
337 Plugins.MouseDown(ImageView.SelectedImage, e, buts);
338 Scripting.UpdateState(Scripting.State.GetDown(e, buts.Event.Button));
339
340 switch (currentTool.type)
341 {
342 case Tool.Type.pan:
343 ToolDown_Pan(e, buts);
344 break;
345 case Tool.Type.magic:
346 ToolDown_Magic(e, buts);
347 break;
348 case Tool.Type.point:
349 ToolDown_Point(e, buts);
350 break;
351 case Tool.Type.move:
352 ToolDown_Move(e, buts);
353 break;
354 case Tool.Type.line:
355 ToolDown_Line(e, buts);
356 break;
357 case Tool.Type.rect:
358 ToolDown_Rectangle(e, buts);
359 break;
360 case Tool.Type.ellipse:
361 ToolDown_Ellipse(e, buts);
362 break;
363 case Tool.Type.polygon:
364 ToolDown_Polygon(e, buts);
365 break;
366 case Tool.Type.text:
367 ToolDown_Text(e, buts);
368 break;
369 case Tool.Type.delete:
370 ToolDown_Delete(e, buts);
371 break;
372 case Tool.Type.freeform:
373 ToolDown_Freeform(e, buts);
374 break;
375 default:
376 break;
377 }
378 }
379
380 public void ToolUp(PointD e, ButtonReleaseEventArgs buts)
381 {
382 if (App.viewer == null || currentTool == null || ImageView.SelectedImage == null)
383 return;
384
385 Plugins.MouseUp(ImageView.SelectedImage, e, buts);
386 Scripting.UpdateState(Scripting.State.GetUp(e, buts.Event.Button));
387
388 switch (currentTool.type)
389 {
390 case Tool.Type.pan:
391 ToolUp_Pan(e, buts);
392 break;
393 case Tool.Type.move:
394 ToolUp_Move(e, buts);
395 break;
396 case Tool.Type.magic:
397 ToolUp_Magic(e, buts);
398 break;
399 case Tool.Type.line:
400 ToolUp_Line(e, buts);
401 break;
402 case Tool.Type.rect:
403 ToolUp_Rectangle(e, buts);
404 break;
405 case Tool.Type.ellipse:
406 ToolUp_Ellipse(e, buts);
407 break;
408 case Tool.Type.polygon:
409 ToolUp_Polygon(e, buts);
410 break;
411 case Tool.Type.text:
412 //ToolUp_Text(e, buts);
413 break;
414 case Tool.Type.delete:
415 //
416 break;
417 case Tool.Type.freeform:
418 ToolUp_Freeform(e, buts);
419 break;
420 case Tool.Type.bucket:
421 ToolUp_Bucket(e, buts);
422 break;
423 case Tool.Type.dropper:
424 ToolUp_Dropper(e, buts);
425 break;
426 default:
427 break;
428 }
429 }
430
431 public void ToolMove(PointD e, MotionNotifyEventArgs buts)
432 {
433 if (App.viewer == null)
434 return;
435
436 Plugins.MouseMove(ImageView.SelectedImage, e, buts);
437
438 // Update scripting state using available button masks
439 if (buts.Event.State.HasFlag(ModifierType.Button1Mask))
440 Scripting.UpdateState(Scripting.State.GetMove(e, 1));
441 else if (buts.Event.State.HasFlag(ModifierType.Button2Mask))
442 Scripting.UpdateState(Scripting.State.GetMove(e, 2));
443 else if (buts.Event.State.HasFlag(ModifierType.Button3Mask))
444 Scripting.UpdateState(Scripting.State.GetMove(e, 3));
445 else if (buts.Event.State.HasFlag(ModifierType.Button4Mask))
446 Scripting.UpdateState(Scripting.State.GetMove(e, 4));
447 else if (buts.Event.State.HasFlag(ModifierType.Button5Mask))
448 Scripting.UpdateState(Scripting.State.GetMove(e, 5));
449 else
450 Scripting.UpdateState(Scripting.State.GetMove(e, 0));
451
452 switch (currentTool.type)
453 {
454 case Tool.Type.pan:
455 ToolMove_Pan(e, buts);
456 break;
457 case Tool.Type.move:
458 ToolMove_Move(e, buts);
459 break;
460 case Tool.Type.magic:
461 ToolMove_Magic(e, buts);
462 break;
463 case Tool.Type.line:
464 ToolMove_Line(e, buts);
465 break;
466 case Tool.Type.rect:
467 ToolMove_Rectangle(e, buts);
468 break;
469 case Tool.Type.ellipse:
470 ToolMove_Ellipse(e, buts);
471 break;
472 case Tool.Type.polygon:
473 ToolMove_Polygon(e, buts);
474 break;
475 case Tool.Type.text:
476 //ToolMove_Text(e, buts);
477 break;
478 case Tool.Type.freeform:
479 ToolMove_Freeform(e, buts);
480 break;
481 case Tool.Type.brush:
482 ToolMove_Brush(e, buts);
483 break;
484 case Tool.Type.eraser:
485 ToolMove_Eraser(e, buts);
486 break;
487 default:
488 break;
489 }
490 }
491
492 // ============================================================================
493 // 1. MOVE TOOL - Selection and Dragging (fixed)
494 // ============================================================================
495 public void ToolDown_Move(PointD e, ButtonPressEventArgs buts)
496 {
497 if (currentTool.type != Tool.Type.move || buts.Event.Button != 1)
498 return;
499
500 App.viewer.MouseDown = e;
501 bool ctrl = buts.Event.State.HasFlag(ModifierType.ControlMask);
502 var selectableRois = GetSelectableRois(ImageView.SelectedImage).ToList();
503 var selectedRois = selectableRois.Where(r => r != null && r.Selected).Reverse().ToList();
504 var unselectedRois = selectableRois.Where(r => r != null && !r.Selected).Reverse().ToList();
505
506 selectedROI = null;
507
508 // 1 — Vertex hit test on selected ROIs
509 foreach (var ann in selectedRois)
510 {
511 if (ann.type == ROI.Type.Mask)
512 continue;
513
514 var boxes = ann.GetSelectBoxes(ROI.selectBoxSize * ImageView.SelectedImage.PhysicalSizeX);
515 if (boxes == null) continue;
516
517 for (int i = 0; i < boxes.Length; i++)
518 {
519 if (boxes[i].Contains(ann.coord, e))
520 {
521 selectedROI = ann;
522 if (ctrl)
523 {
524 if (ann.selectedPoints.Contains(i)) ann.selectedPoints.Remove(i);
525 else ann.selectedPoints.Add(i);
526 }
527 else
528 {
529 ann.selectedPoints.Clear();
530 ann.selectedPoints.Add(i);
531 }
532 UpdateView();
533 return;
534 }
535 }
536 }
537
538 // 2 — Click on body of already-selected ROI
539 foreach (var ann in selectedRois)
540 {
541 if (ann == null)
542 continue;
543
544 if (ann.type == ROI.Type.Mask)
545 {
546 if (HitTestMask(ann, e))
547 {
548 selectedROI = ann;
549 UpdateView();
550 return;
551 }
552 continue;
553 }
554
555 if (ann.BoundingBox.Contains(ann.coord, e))
556 {
557 selectedROI = ann;
558 UpdateView();
559 return;
560 }
561 }
562
563 // 3 — Click on any ROI (select it)
564 foreach (var ann in selectedRois.Concat(unselectedRois))
565 {
566 if (ann == null)
567 continue;
568
569 bool hit = ann.type == ROI.Type.Mask ? HitTestMask(ann, e) : ann.BoundingBox.Contains(ann.coord, e);
570 if (hit)
571 {
572 if (!ctrl)
573 {
574 foreach (var other in selectableRois)
575 {
576 if (other != ann)
577 {
578 other.Selected = false;
579 other.selectedPoints?.Clear();
580 }
581 }
582 }
583
584 ann.Selected = true;
585 ann.selectedPoints?.Clear();
586 selectedROI = ann;
587 UpdateView();
588 return;
589 }
590 }
591
592 // 4 — Start selection rectangle
593 if (!ctrl)
594 {
595 foreach (var ann in selectableRois)
596 {
597 if (ann != null)
598 {
599 ann.Selected = false;
600 ann.selectedPoints?.Clear();
601 }
602 }
603 selectedROI = null;
604 }
605
606 Tools.GetTool(Tools.Tool.Type.move).Rectangle = new RectangleD(e.X, e.Y, 0, 0);
607 UpdateView();
608 }
609
610 private static IEnumerable<ROI> GetSelectableRois(BioImage image)
611 {
612 if (image == null)
613 yield break;
614
615 if (image.Annotations != null)
616 {
617 foreach (var roi in image.Annotations)
618 if (roi != null)
619 yield return roi;
620 }
621
622 if (App.viewer == null)
623 yield break;
624
625 foreach (var roi in App.viewer.GetZarrLabelOverlaysForImage(image))
626 if (roi != null)
627 yield return roi;
628 }
629 public void ToolMove_Move(PointD e, MotionNotifyEventArgs buts)
630 {
631 if (currentTool.type != Tool.Type.move ||
632 !buts.Event.State.HasFlag(ModifierType.Button1Mask))
633 return;
634
635 // Moving selected vertices/control points or ROIs
636 if (selectedROI != null && selectedROI.Selected && selectedROI.selectedPoints != null)
637 {
638 PointD delta = new PointD(e.X - App.viewer.MouseDown.X, e.Y - App.viewer.MouseDown.Y);
639
640 // Rectangle / Ellipse special handling
641 if (selectedROI.type == ROI.Type.Rectangle || selectedROI.type == ROI.Type.Ellipse)
642 {
643 // If exactly one corner is selected -> resize by that corner
644 if (selectedROI.selectedPoints.Count == 1)
645 {
646 int pointIndex = selectedROI.selectedPoints[0];
647 RectangleD rect = selectedROI.BoundingBox;
648
649 // Work with normalized copy to make logic clearer
650 double left = rect.X;
651 double top = rect.Y;
652 double right = rect.X + rect.W;
653 double bottom = rect.Y + rect.H;
654
655 switch (pointIndex)
656 {
657 case 0: // Top-Left
658 left += delta.X;
659 top += delta.Y;
660 break;
661 case 1: // Top-Right
662 right += delta.X;
663 top += delta.Y;
664 break;
665 case 2: // Bottom-Right
666 right += delta.X;
667 bottom += delta.Y;
668 break;
669 case 3: // Bottom-Left
670 left += delta.X;
671 bottom += delta.Y;
672 break;
673 default:
674 break;
675 }
676
677 // Build and assign normalized bounding box
678 RectangleD newRect = new RectangleD(
679 Math.Min(left, right),
680 Math.Min(top, bottom),
681 Math.Abs(right - left),
682 Math.Abs(bottom - top)
683 );
684
685 selectedROI.BoundingBox = newRect;
686
687 // Important: keep points and bbox in sync
688 SyncPointsFromBoundingBox(selectedROI);
689 selectedROI.UpdateBoundingBox(); // if your ROI uses this to recalc derived fields
690
691 App.viewer.MouseDown = new PointD(e.X, e.Y);
692 App.viewer.UpdateView();
693 return;
694 }
695 else
696 {
697 // No corner selected -> move entire ROI by delta
698 for (int i = 0; i < selectedROI.Points.Count; i++)
699 {
700 selectedROI.UpdatePoint(
701 new PointD(selectedROI.Points[i].X + delta.X,
702 selectedROI.Points[i].Y + delta.Y), i);
703 }
704
705 // Keep bounding box in sync with points
706 SyncBoundingBoxFromPoints(selectedROI);
707 selectedROI.UpdateBoundingBox();
708
709 App.viewer.MouseDown = new PointD(e.X, e.Y);
710 App.viewer.UpdateView();
711 return;
712 }
713 }
714 else
715 {
716 // Other shapes: polygons, polylines, freeforms, lines, points
717 if (selectedROI.selectedPoints.Count == 0)
718 {
719 // Move all points
720 for (int i = 0; i < selectedROI.Points.Count; i++)
721 {
722 selectedROI.UpdatePoint(
723 new PointD(selectedROI.Points[i].X + delta.X,
724 selectedROI.Points[i].Y + delta.Y), i);
725 }
726 }
727 else
728 {
729 // Move only selected points
730 foreach (int pointIndex in selectedROI.selectedPoints.ToArray())
731 {
732 if (pointIndex >= 0 && pointIndex < selectedROI.Points.Count)
733 {
734 PointD pt = selectedROI.Points[pointIndex];
735 selectedROI.UpdatePoint(
736 new PointD(pt.X + delta.X, pt.Y + delta.Y), pointIndex);
737 }
738 }
739 }
740
741 // Keep bbox updated
742 SyncBoundingBoxFromPoints(selectedROI);
743 selectedROI.UpdateBoundingBox();
744
745 App.viewer.MouseDown = new PointD(e.X, e.Y);
746 App.viewer.UpdateView();
747 return;
748 }
749 }
750 // Dragging entire selected ROI (no selectedPoints list)
751 else if (selectedROI != null && selectedROI.Selected)
752 {
753 PointD delta = new PointD(e.X - App.viewer.MouseDown.X, e.Y - App.viewer.MouseDown.Y);
754
755 for (int i = 0; i < selectedROI.Points.Count; i++)
756 {
757 selectedROI.UpdatePoint(
758 new PointD(selectedROI.Points[i].X + delta.X,
759 selectedROI.Points[i].Y + delta.Y), i);
760 }
761
762 SyncBoundingBoxFromPoints(selectedROI);
763 selectedROI.UpdateBoundingBox();
764
765 App.viewer.MouseDown = new PointD(e.X, e.Y);
766 App.viewer.UpdateView();
767 return;
768 }
769 // Drawing / updating selection rectangle
770 else
771 {
772 PointD d = new PointD(e.X - App.viewer.MouseDown.X, e.Y - App.viewer.MouseDown.Y);
773 RectangleD selRect = new RectangleD(
774 Math.Min(App.viewer.MouseDown.X, e.X),
775 Math.Min(App.viewer.MouseDown.Y, e.Y),
776 Math.Abs(d.X), Math.Abs(d.Y));
777
778 Tools.GetTool(Tools.Tool.Type.move).Rectangle = selRect;
779
780 bool controlHeld = buts.Event.State.HasFlag(ModifierType.ControlMask);
781
782 // Select ROIs intersecting with rectangle
783 foreach (ROI an in ImageView.SelectedImage.Annotations)
784 {
785 if (an == null) continue;
786
787 RectangleD selBound = an.GetSelectBound(
788 ROI.selectBoxSize * ImageView.SelectedImage.PhysicalSizeX,
789 ROI.selectBoxSize * ImageView.SelectedImage.PhysicalSizeY);
790
791 if (selBound.IntersectsWith(selRect))
792 {
793 an.Selected = true;
794
795 if (!controlHeld)
796 an.selectedPoints?.Clear();
797
798 // Select points within rectangle
799 RectangleD[] sels = an.GetSelectBoxes(
800 ROI.selectBoxSize * ImageView.SelectedImage.PhysicalSizeX);
801 if (sels != null)
802 {
803 for (int i = 0; i < sels.Length; i++)
804 {
805 if (sels[i].IntersectsWith(selRect))
806 {
807 if (an.selectedPoints == null) an.selectedPoints = new List<int>();
808 if (!an.selectedPoints.Contains(i))
809 an.selectedPoints.Add(i);
810 }
811 }
812 }
813 }
814 else if (!controlHeld)
815 {
816 an.Selected = false;
817 an.selectedPoints?.Clear();
818 }
819 }
820
821 if (selectedROI != null)
822 selectedROI.UpdateBoundingBox();
823
824 App.viewer.UpdateView();
825 }
826 }
827 public void ToolUp_Move(PointD e, ButtonReleaseEventArgs buts)
828 {
829 if (currentTool.type != Tool.Type.move || buts.Event.Button != 1)
830 return;
831
832 // Clear selection rectangle
833 Tools.GetTool(Tools.Tool.Type.move).Rectangle = new RectangleD(0, 0, 0, 0);
834 selectedROI?.UpdateBoundingBox();
835 App.viewer.UpdateView();
836 }
837
838
839 // ============================================================================
840 // 2. POINT TOOL
841 // ============================================================================
842 public void ToolDown_Point(PointD e, ButtonPressEventArgs buts)
843 {
844 if (currentTool.type != Tool.Type.point || buts.Event.Button != 1)
845 return;
846
847 ROI an = new ROI();
848 an.type = ROI.Type.Point;
849 an.Points.Clear();
850 an.Points.Add(new PointD(e.X, e.Y));
851 an.coord = App.viewer.GetCoordinate();
852 an.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
853 an.Selected = true;
854 selectedROI = an;
855 AddROI(selectedROI);
856 UpdateView();
857 }
858
859 // ============================================================================
860 // 3. LINE TOOL
861 // ============================================================================
862 public void ToolDown_Line(PointD e, ButtonPressEventArgs buts)
863 {
864 if (currentTool.type != Tool.Type.line || buts.Event.Button != 1)
865 return;
866
867 selectedROI = new ROI();
868 selectedROI.type = ROI.Type.Line;
869 selectedROI.AddPoint(new PointD(e.X, e.Y));
870 selectedROI.AddPoint(new PointD(e.X + ImageView.SelectedImage.PhysicalSizeX * ROI.selectBoxSize,
871 e.Y + ImageView.SelectedImage.PhysicalSizeX * ROI.selectBoxSize));
872 selectedROI.coord = App.viewer.GetCoordinate();
873 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
874 AddROI(selectedROI);
875 }
876
877 public void ToolMove_Line(PointD e, MotionNotifyEventArgs buts)
878 {
879 if (currentTool.type != Tool.Type.line ||
880 !buts.Event.State.HasFlag(ModifierType.Button1Mask))
881 return;
882
883 if (selectedROI != null)
884 {
885 if (selectedROI.selectedPoints.Count == 1)
886 {
887 selectedROI.UpdatePoint(new PointD(e.X, e.Y), selectedROI.selectedPoints[0]);
888 selectedROI.UpdateBoundingBox();
889 UpdateView();
890 }
891 else
892 {
893 selectedROI.UpdatePoint(new PointD(e.X, e.Y), 1);
894 selectedROI.UpdateBoundingBox();
895 UpdateView();
896 }
897 }
898 }
899
900 public void ToolUp_Line(PointD e, ButtonReleaseEventArgs buts)
901 {
902 if (currentTool.type != Tool.Type.line || buts.Event.Button != 1)
903 return;
904
905 if (selectedROI != null && selectedROI.GetPointCount() >= 2)
906 {
907 selectedROI.UpdatePoint(new PointD(e.X, e.Y), 1);
908 selectedROI.UpdateBoundingBox();
909 selectedROI = null;
910 UpdateView();
911 }
912 }
913
914 public void ToolDown_Rectangle(PointD e, ButtonPressEventArgs buts)
915 {
916 if (currentTool.type != Tool.Type.rect || buts.Event.Button != 1)
917 return;
918
919 selectedROI = new ROI();
920 selectedROI.type = ROI.Type.Rectangle;
921 selectedROI.BoundingBox = new RectangleD(e.X, e.Y, 0, 0); // Start with 0 size
922 selectedROI.coord = App.viewer.GetCoordinate();
923 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
924 AddROI(selectedROI);
925 }
926
927 public void ToolMove_Rectangle(PointD e, MotionNotifyEventArgs buts)
928 {
929 if (currentTool.type != Tool.Type.rect ||
930 !buts.Event.State.HasFlag(ModifierType.Button1Mask))
931 return;
932
933 if (selectedROI != null && selectedROI.type == ROI.Type.Rectangle)
934 {
935 selectedROI.BoundingBox = RectangleDExtensions.FromCorners(
936 new PointD(selectedROI.BoundingBox.X, selectedROI.BoundingBox.Y),
937 new PointD(e.X, e.Y));
938 UpdateView();
939 }
940 }
941
942 public void ToolUp_Rectangle(PointD e, ButtonReleaseEventArgs buts)
943 {
944 if (currentTool.type != Tool.Type.rect || buts.Event.Button != 1)
945 return;
946
947 if (selectedROI != null && selectedROI.type == ROI.Type.Rectangle)
948 {
949 // Update final rectangle bounds
950 selectedROI.BoundingBox = RectangleDExtensions.FromCorners(
951 new PointD(selectedROI.BoundingBox.X, selectedROI.BoundingBox.Y),
952 new PointD(e.X, e.Y));
953
954 // Validate the ROI (might reject if too small)
955 selectedROI.Validate();
956 selectedROI = null;
957 UpdateView();
958 }
959 }
960
961 // ============================================================================
962 // 5. ELLIPSE TOOL
963 // ============================================================================
964 public void ToolDown_Ellipse(PointD e, ButtonPressEventArgs buts)
965 {
966 if (currentTool.type != Tool.Type.ellipse || buts.Event.Button != 1)
967 return;
968
969 selectedROI = new ROI();
970 selectedROI.type = ROI.Type.Ellipse;
971 selectedROI.BoundingBox = new RectangleD(e.X, e.Y, 0, 0); // Start with 0 size
972 selectedROI.coord = App.viewer.GetCoordinate();
973 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
974 AddROI(selectedROI);
975 }
976
977 public void ToolMove_Ellipse(PointD e, MotionNotifyEventArgs buts)
978 {
979 if (currentTool.type != Tool.Type.ellipse ||
980 !buts.Event.State.HasFlag(ModifierType.Button1Mask))
981 return;
982
983 if (selectedROI != null && selectedROI.type == ROI.Type.Ellipse)
984 {
985 selectedROI.BoundingBox = RectangleDExtensions.FromCorners(
986 new PointD(selectedROI.BoundingBox.X, selectedROI.BoundingBox.Y),
987 new PointD(e.X, e.Y));
988 UpdateView();
989 }
990 }
991
992 public void ToolUp_Ellipse(PointD e, ButtonReleaseEventArgs buts)
993 {
994 if (currentTool.type != Tool.Type.ellipse || buts.Event.Button != 1)
995 return;
996
997 if (selectedROI != null && selectedROI.type == ROI.Type.Ellipse)
998 {
999 // Update final ellipse bounds
1000 selectedROI.BoundingBox = RectangleDExtensions.FromCorners(
1001 new PointD(selectedROI.BoundingBox.X, selectedROI.BoundingBox.Y),
1002 new PointD(e.X, e.Y));
1003
1004 // Validate the ROI (might reject if too small)
1005 selectedROI.Validate();
1006 selectedROI = null;
1007 UpdateView();
1008 }
1009 }
1010
1011 // ============================================================================
1012 // 6. POLYGON TOOL
1013 // ============================================================================
1014 public void ToolDown_Polygon(PointD e, ButtonPressEventArgs buts)
1015 {
1016 if (currentTool.type != Tool.Type.polygon || buts.Event.Button != 1)
1017 return;
1018
1019 if (selectedROI == null || selectedROI.type != ROI.Type.Polygon)
1020 {
1021 // Start new polygon
1022 selectedROI = new ROI();
1023 selectedROI.type = ROI.Type.Polygon;
1024 selectedROI.closed = false;
1025 selectedROI.AddPoint(new PointD(e.X, e.Y));
1026 selectedROI.coord = App.viewer.GetCoordinate();
1027 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
1028 AddROI(selectedROI);
1029 UpdateView();
1030 }
1031 else if (!selectedROI.closed)
1032 {
1033 // Check if clicking near first point to close polygon
1034 if (selectedROI.Points.Count >= 3)
1035 {
1036 PointD firstPoint = selectedROI.Points[0];
1037 double distance = Math.Sqrt(Math.Pow(e.X - firstPoint.X, 2) + Math.Pow(e.Y - firstPoint.Y, 2));
1038 double threshold = 10.0; // Adjust based on your zoom/scale
1039
1040 if (distance < threshold)
1041 {
1042 // Close polygon
1043 selectedROI.closed = true;
1044 selectedROI.Selected = false;
1045 selectedROI.UpdateBoundingBox();
1046 selectedROI.Validate();
1047 selectedROI = null;
1048 UpdateView();
1049 return;
1050 }
1051 }
1052
1053
1054 }
1055 // Add new point to polygon
1056 selectedROI.AddPoint(new PointD(e.X, e.Y));
1057 selectedROI.UpdateBoundingBox();
1058 UpdateView();
1059 }
1060
1061 private void ToolMove_Polygon(PointD e, MotionNotifyEventArgs buts)
1062 {
1063 if (currentTool.type != Tool.Type.polygon)
1064 return;
1065
1066 // Update preview of next point while drawing polygon
1067 if (selectedROI != null && selectedROI.type == ROI.Type.Polygon && !selectedROI.closed)
1068 {
1069 // Store current mouse position for preview rendering
1070 // (Assuming you have a way to render a preview line from last point to cursor)
1071 UpdateView();
1072 }
1073 }
1074 private void ToolUp_Polygon(PointD e, ButtonReleaseEventArgs buts)
1075 {
1076 // Polygon tool doesn't need mouse up handling since it works on clicks
1077 // The tool completes either by clicking near the first point or by double-clicking
1078 }
1079
1080 // ============================================================================
1081 // 7. FREEFORM TOOL
1082 // ============================================================================
1083 public void ToolDown_Freeform(PointD e, ButtonPressEventArgs buts)
1084 {
1085 if (currentTool.type != Tool.Type.freeform || buts.Event.Button != 1)
1086 return;
1087
1088 if (selectedROI == null || selectedROI.type != ROI.Type.Polygon)
1089 {
1090 // Start new freeform
1091 selectedROI = new ROI();
1092 selectedROI.type = ROI.Type.Polygon;
1093 selectedROI.closed = false;
1094 selectedROI.AddPoint(new PointD(e.X, e.Y));
1095 selectedROI.coord = App.viewer.GetCoordinate();
1096 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
1097 AddROI(selectedROI);
1098 UpdateView();
1099 }
1100 else if (!selectedROI.closed)
1101 {
1102 // Check if clicking near first point to close freeform
1103 if (selectedROI.Points.Count >= 3)
1104 {
1105 PointD firstPoint = selectedROI.Points[0];
1106 double distance = Math.Sqrt(Math.Pow(e.X - firstPoint.X, 2) + Math.Pow(e.Y - firstPoint.Y, 2));
1107 double threshold = 10.0; // Adjust based on your zoom/scale
1108
1109 if (distance < threshold)
1110 {
1111 // Close freeform
1112 selectedROI.closed = true;
1113 selectedROI.Selected = false;
1114 selectedROI.UpdateBoundingBox();
1115 selectedROI.Validate();
1116 selectedROI = null;
1117 UpdateView();
1118 return;
1119 }
1120 }
1121
1122
1123 }
1124 // Add new point to polygon
1125 selectedROI.AddPoint(new PointD(e.X, e.Y));
1126 selectedROI.UpdateBoundingBox();
1127 UpdateView();
1128 }
1129
1130 public void ToolMove_Freeform(PointD e, MotionNotifyEventArgs buts)
1131 {
1132 if (selectedROI != null)
1133 if (currentTool.type == Tool.Type.freeform && buts.Event.State.HasFlag(ModifierType.Button1Mask))
1134 {
1135 selectedROI.AddPoint(new PointD(e.X, e.Y));
1136 selectedROI.coord = App.viewer.GetCoordinate();
1137 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
1138 }
1139 }
1140
1141 public void ToolUp_Freeform(PointD e, ButtonReleaseEventArgs buts)
1142 {
1143 if (currentTool.type != Tool.Type.freeform || buts.Event.Button != 1)
1144 return;
1145
1146 if (selectedROI != null)
1147 {
1148 selectedROI.UpdateBoundingBox();
1149 //selectedROI = null;
1150 UpdateView();
1151 }
1152 }
1153
1154 // ============================================================================
1155 // 8. TEXT/LABEL TOOL
1156 // ============================================================================
1157 public void ToolDown_Text(PointD e, ButtonPressEventArgs buts)
1158 {
1159 if (currentTool.type != Tool.Type.text || buts.Event.Button != 1)
1160 return;
1161
1162 selectedROI = new ROI();
1163 selectedROI.type = ROI.Type.Label;
1164 selectedROI.AddPoint(new PointD(e.X, e.Y));
1165 selectedROI.coord = App.viewer.GetCoordinate();
1166 selectedROI.serie = ImageView.SelectedImage.isPyramidal ? App.viewer.Level : ImageView.SelectedImage.series;
1167
1168 // Show text input dialog
1169 ti = TextInput.Create();
1170 ti.ShowAll();
1171 ti.Run();
1172
1173 AddROI(selectedROI);
1174 selectedROI.UpdateBoundingBox();
1175 UpdateView();
1176 }
1177
1178 // ============================================================================
1179 // 9. DELETE TOOL
1180 // ============================================================================
1181 public void ToolDown_Delete(PointD e, ButtonPressEventArgs buts)
1182 {
1183 if (currentTool.type != Tool.Type.delete || buts.Event.Button != 1)
1184 return;
1185
1186 for (int i = ImageView.SelectedImage.Annotations.Count - 1; i >= 0; i--)
1187 {
1188 ROI an = ImageView.SelectedImage.Annotations[i];
1189 if (an == null) continue;
1190
1191 if (an.BoundingBox.IntersectsWith(new RectangleD(e.X, e.Y, 1, 1)))
1192 {
1193 if (an.selectedPoints == null || an.selectedPoints.Count == 0)
1194 {
1195 // Delete entire ROI
1196 ImageView.SelectedImage.Annotations.RemoveAt(i);
1197 break;
1198 }
1199 else if (an.selectedPoints.Count == 1 &&
1200 !(an.type == ROI.Type.Polygon ||
1201 an.type == ROI.Type.Polyline ||
1202 an.type == ROI.Type.Freeform))
1203 {
1204 // Delete ROI with single selected point (for non multi-point types)
1205 ImageView.SelectedImage.Annotations.RemoveAt(i);
1206 break;
1207 }
1208 else
1209 {
1210 // Delete selected points from polygon/polyline/freeform
1211 if (an.type == ROI.Type.Polygon ||
1212 an.type == ROI.Type.Polyline ||
1213 an.type == ROI.Type.Freeform)
1214 {
1215 an.closed = false;
1216 an.RemovePoints(an.selectedPoints.ToArray());
1217 an.UpdateBoundingBox();
1218 break;
1219 }
1220 }
1221 }
1222 }
1223 UpdateView();
1224 }
1225
1226 // ============================================================================
1227 // 10. BRUSH TOOL
1228 // ============================================================================
1229 public void ToolDown_Brush(PointD e, ButtonPressEventArgs buts)
1230 {
1231 if (currentTool.type != Tool.Type.brush || buts.Event.Button != 1)
1232 return;
1233
1234 PointD ip = ImageView.SelectedImage.ToImageSpace(e);
1235 if (ImageView.SelectedImage.isPyramidal)
1236 ip = new PointD(e.X, e.Y);
1237
1238 Bio.Graphics.Graphics g = Bio.Graphics.Graphics.FromImage(ImageView.SelectedBuffer);
1239 g.pen = new Bio.Graphics.Pen(DrawColor, (int)StrokeWidth, ImageView.SelectedImage.bitsPerPixel);
1240 g.FillEllipse(new Rectangle((int)ip.X, (int)ip.Y, (int)StrokeWidth, (int)StrokeWidth), g.pen.color);
1241
1242 App.viewer.UpdateImage();
1243 App.viewer.UpdateView();
1244 }
1245
1246 public void ToolMove_Brush(PointD e, MotionNotifyEventArgs buts)
1247 {
1248 if (currentTool.type != Tool.Type.brush ||
1249 !buts.Event.State.HasFlag(ModifierType.Button1Mask))
1250 return;
1251
1252 PointD ip = ImageView.SelectedImage.ToImageSpace(e);
1253 if (ImageView.SelectedImage.isPyramidal)
1254 ip = new PointD(e.X, e.Y);
1255
1256 Bio.Graphics.Graphics g = Bio.Graphics.Graphics.FromImage(ImageView.SelectedBuffer);
1257 g.pen = new Bio.Graphics.Pen(DrawColor, (int)StrokeWidth, ImageView.SelectedImage.bitsPerPixel);
1258 g.FillEllipse(new Rectangle((int)ip.X, (int)ip.Y, (int)StrokeWidth, (int)StrokeWidth), g.pen.color);
1259
1260 App.viewer.UpdateImages();
1261 App.viewer.UpdateView();
1262 }
1263
1264 // ============================================================================
1265 // 11. ERASER TOOL
1266 // ============================================================================
1267 public void ToolDown_Eraser(PointD e, ButtonPressEventArgs buts)
1268 {
1269 if (currentTool.type != Tool.Type.eraser || buts.Event.Button != 1)
1270 return;
1271
1272 PointD ip = ImageView.SelectedImage.ToImageSpace(e);
1273 if (ImageView.SelectedImage.isPyramidal)
1274 ip = new PointD(e.X, e.Y);
1275
1276 Bio.Graphics.Graphics g = Bio.Graphics.Graphics.FromImage(ImageView.SelectedBuffer);
1277 Bio.Graphics.Pen pen = new Bio.Graphics.Pen(EraseColor, (int)StrokeWidth, ImageView.SelectedBuffer.BitsPerPixel);
1278 g.FillEllipse(new Rectangle((int)ip.X, (int)ip.Y, (int)StrokeWidth, (int)StrokeWidth), pen.color);
1279
1280 App.viewer.UpdateImages();
1281 App.viewer.UpdateView();
1282 }
1283
1284 public void ToolMove_Eraser(PointD e, MotionNotifyEventArgs buts)
1285 {
1286 if (currentTool.type != Tool.Type.eraser ||
1287 !buts.Event.State.HasFlag(ModifierType.Button1Mask))
1288 return;
1289
1290 PointD ip = ImageView.SelectedImage.ToImageSpace(e);
1291 if (ImageView.SelectedImage.isPyramidal)
1292 ip = new PointD(e.X, e.Y);
1293
1294 Bio.Graphics.Graphics g = Bio.Graphics.Graphics.FromImage(ImageView.SelectedBuffer);
1295 Bio.Graphics.Pen pen = new Bio.Graphics.Pen(EraseColor, (int)StrokeWidth, ImageView.SelectedBuffer.BitsPerPixel);
1296 g.FillEllipse(new Rectangle((int)ip.X, (int)ip.Y, (int)StrokeWidth, (int)StrokeWidth), pen.color);
1297
1298 App.viewer.UpdateImages();
1299 App.viewer.UpdateView();
1300 }
1301
1302 // ============================================================================
1303 // 12. BUCKET/FILL TOOL
1304 // ============================================================================
1305 public void ToolUp_Bucket(PointD e, ButtonReleaseEventArgs buts)
1306 {
1307 if (currentTool.type != Tool.Type.bucket || buts.Event.Button != 1)
1308 return;
1309
1310 PointD ip = ImageView.SelectedImage.ToImageSpace(e);
1311
1312 if (ip.X >= ImageView.SelectedImage.SizeX || ip.Y >= ImageView.SelectedImage.SizeY)
1313 return;
1314
1315 floodFiller.FillColor = DrawColor;
1316 floodFiller.Tolerance = tolerance;
1317 floodFiller.Bitmap = ImageView.SelectedBuffer;
1318 floodFiller.FloodFill(new AForge.Point((int)ip.X, (int)ip.Y));
1319
1320 App.viewer.UpdateImages();
1321 App.viewer.UpdateView();
1322 }
1323
1324 // ============================================================================
1325 // 13. DROPPER/COLOR PICKER TOOL
1326 // ============================================================================
1327 public void ToolUp_Dropper(PointD e, ButtonReleaseEventArgs buts)
1328 {
1329 if (currentTool.type != Tool.Type.dropper || buts.Event.Button != 1)
1330 return;
1331
1332 PointD ip = ImageView.SelectedImage.ToImageSpace(e);
1333
1334 if (ip.X >= 0 && ip.Y >= 0 && ip.X < ImageView.SelectedImage.SizeX && ip.Y < ImageView.SelectedImage.SizeY)
1335 {
1336 DrawColor = ImageView.SelectedBuffer.GetPixel((int)ip.X, (int)ip.Y);
1337 App.viewer.UpdateView();
1338 }
1339 }
1340
1341 // ============================================================================
1342 // 14. PAN TOOL
1343 // ============================================================================
1344 // --------------------------------------------------------------------
1345 // PAN STATE
1346 // --------------------------------------------------------------------
1347 private bool isPanning;
1348 private double panStartX;
1349 private double panStartY;
1350
1351 private PointD initialPanOrigin;
1352 private double initialPanScale;
1353
1354 // Velocity (world units / second)
1355 private PointD panVelocity;
1356 private DateTime lastMoveTime;
1357
1358 // Kinetic settings
1359 private const double PanFriction = 50.0; // world units / s²
1360 private const double VelocityEpsilon = 0.0001;
1361
1362 // DPI normalization
1363 private double dpiScale = 1.0;
1364
1365 // Animation
1366 private bool kineticActive = true;
1367 // Pan speed multiplier (user configurable)
1368
1369 public static double PanSpeed = 0.75; // Default to 0.75 for more controlled panning
1370 // --------------------------------------------------------------------
1371 // Mouse Down
1372 // --------------------------------------------------------------------
1373 public void ToolDown_Pan(PointD e, ButtonPressEventArgs buts)
1374 {
1375 if (buts.Event.Button != 1 && buts.Event.Button != 2)
1376 return;
1377
1378 currentTool = GetTool(Tool.Type.pan);
1379 panStartX = buts.Event.X;
1380 panStartY = buts.Event.Y;
1381 initialPanScale = ImageView.SelectedImage.isPyramidal
1382 ? App.viewer.Resolution
1383 : App.viewer.Scale.Width;
1384
1385 if (ImageView.SelectedImage.isPyramidal)
1386 {
1387 initialPanOrigin = new PointD(
1388 App.viewer.PyramidalOrigin.X,
1389 App.viewer.PyramidalOrigin.Y);
1390 }
1391 else
1392 {
1393 initialPanOrigin = new PointD(
1394 App.viewer.Origin.X,
1395 App.viewer.Origin.Y);
1396 }
1397 }
1398
1399 // --------------------------------------------------------------------
1400 // Mouse Move
1401 // --------------------------------------------------------------------
1402 public void ToolMove_Pan(PointD e, MotionNotifyEventArgs buts)
1403 {
1404 if (currentTool.type != Tool.Type.pan)
1405 return;
1406
1407 if (!buts.Event.State.HasFlag(ModifierType.Button1Mask) &&
1408 !buts.Event.State.HasFlag(ModifierType.Button2Mask))
1409 return;
1410
1411 double dx = buts.Event.X - panStartX;
1412 double dy = buts.Event.Y - panStartY;
1413
1414 if (ImageView.SelectedImage.isPyramidal)
1415 {
1416 // PyramidalOrigin is in full-resolution (level-0) pixel space.
1417 // Resolution = full-res pixels per screen pixel, so multiplying
1418 // screen-pixel delta by Resolution gives the correct full-res shift.
1419 // PanSpeed applies a user-tunable multiplier on top.
1420 double deltaX = dx * App.viewer.Resolution * PanSpeed;
1421 double deltaY = dy * App.viewer.Resolution * PanSpeed;
1422 App.viewer.PyramidalOrigin = new PointD(
1423 initialPanOrigin.X - deltaX,
1424 initialPanOrigin.Y - deltaY);
1425 }
1426 else
1427 {
1428 // Non-pyramidal: convert screen pixels back into world units so
1429 // a 1 px drag produces a 1 px screen move at the current zoom.
1430 double scale = Math.Max(initialPanScale, double.Epsilon);
1431 double deltaX = dx / (App.viewer.PxWmicron * scale);
1432 double deltaY = dy / (App.viewer.PxHmicron * scale);
1433 App.viewer.Origin = new PointD(
1434 initialPanOrigin.X + deltaX,
1435 initialPanOrigin.Y + deltaY);
1436 }
1437 }
1438
1439
1440 // --------------------------------------------------------------------
1441 // Mouse Up
1442 // --------------------------------------------------------------------
1443 public void ToolUp_Pan(PointD e, ButtonReleaseEventArgs buts)
1444 {
1445 if (currentTool.type == Tool.Type.pan &&
1446 (buts.Event.Button == 1 || buts.Event.Button == 2))
1447 {
1448 currentTool = GetTool(Tool.Type.move);
1449 }
1450 }
1451 private void StartKineticLoop()
1452 {
1453 DateTime last = DateTime.UtcNow;
1454
1455 GLib.Timeout.Add(16, () =>
1456 {
1457 if (!kineticActive)
1458 return false;
1459
1460 DateTime now = DateTime.UtcNow;
1461 double dt = (now - last).TotalSeconds;
1462 last = now;
1463
1464 double speed = Math.Sqrt(
1465 panVelocity.X * panVelocity.X +
1466 panVelocity.Y * panVelocity.Y);
1467
1468 if (speed < VelocityEpsilon)
1469 {
1470 kineticActive = false;
1471 return false;
1472 }
1473
1474 // Apply friction
1475 double decel = PanFriction * dt;
1476 double scale = Math.Max(0, (speed - decel) / speed);
1477
1478 panVelocity = new PointD(
1479 panVelocity.X * scale,
1480 panVelocity.Y * scale);
1481
1482 PointD origin = GetOrigin();
1483 origin = new PointD(
1484 origin.X + panVelocity.X * dt,
1485 origin.Y + panVelocity.Y * dt);
1486
1487 origin = ClampOrigin(origin);
1488 SetOrigin(origin);
1489
1490 return true;
1491 });
1492 }
1493 private PointD ClampOrigin(PointD origin)
1494 {
1495 var img = ImageView.SelectedImage;
1496 var res0 = img.Resolutions[0];
1497
1498 double viewW = App.viewer.ImageViewWidth / App.viewer.Resolution;
1499 double viewH = App.viewer.ImageViewHeight / App.viewer.Resolution;
1500
1501 // Allow panning one full viewport past the image edge in both directions
1502 // so the image can be scrolled off-screen on all sides. Previously
1503 // minX/minY were hard-coded to 0, blocking all negative-X panning
1504 // (and negative-Y via this same path).
1505 double minX = -(viewW);
1506 double minY = -(viewH);
1507 double maxX = res0.SizeX;
1508 double maxY = res0.SizeY;
1509
1510 return new PointD(
1511 Math.Clamp(origin.X, minX, maxX),
1512 Math.Clamp(origin.Y, minY, maxY));
1513 }
1514 private PointD GetOrigin()
1515 {
1516 return ImageView.SelectedImage.isPyramidal
1517 ? App.viewer.PyramidalOrigin
1518 : App.viewer.Origin;
1519 }
1520
1521 private void SetOrigin(PointD p)
1522 {
1523 if (ImageView.SelectedImage.isPyramidal)
1524 App.viewer.PyramidalOrigin = p;
1525 else
1526 App.viewer.Origin = p;
1527 }
1528
1529 // ============================================================================
1530 // 14. Magic Wand TOOL
1531 // ============================================================================
1532 public void ToolDown_Magic(PointD e, ButtonPressEventArgs buts)
1533 {
1534 if (selectedROI == null)
1535 {
1536 selectedROI = new ROI();
1537 selectedROI.type = ROI.Type.Freeform;
1538 selectedROI.AddPoint(new PointD(e.X, e.Y));
1539 selectedROI.coord = App.viewer.GetCoordinate();
1540 if (ImageView.SelectedImage.isPyramidal)
1541 selectedROI.serie = App.viewer.Level;
1542 else
1543 selectedROI.serie = ImageView.SelectedImage.series;
1544 AddROI(selectedROI);
1545 }
1546 else
1547 {
1548 selectedROI.AddPoint(new PointD(e.X, e.Y));
1549 }
1550 }
1551
1552 public void ToolMove_Magic(PointD e, MotionNotifyEventArgs buts)
1553 {
1554 //First we draw the selection rectangle
1555 PointD d = new PointD(e.X - App.viewer.MouseDown.X, e.Y - App.viewer.MouseDown.Y);
1556 Tools.GetTool(Tools.Tool.Type.move).Rectangle = new RectangleD(App.viewer.MouseDown.X, App.viewer.MouseDown.Y, d.X, d.Y);
1557 UpdateView();
1558 }
1559
1560 public void ToolUp_Magic(PointD e, ButtonReleaseEventArgs buts)
1561 {
1562 if (!buts.Event.State.HasFlag(ModifierType.Button1Mask))
1563 return;
1564 // Define the rectangle from mouse coordinates
1565 RectangleD rectangle = new RectangleD(
1566 App.viewer.MouseDown.X,
1567 App.viewer.MouseDown.Y,
1568 Math.Abs(App.viewer.MouseUp.X - App.viewer.MouseDown.X),
1569 Math.Abs(App.viewer.MouseUp.Y - App.viewer.MouseDown.Y)
1570 );
1571
1572 // Convert to image space coordinates
1573 AForge.RectangleF rectInImageSpace = ImageView.SelectedImage.ToImageSpace(rectangle);
1574 ZCT coord = App.viewer.GetCoordinate();
1575 Bitmap bitmap;
1576
1577 // Check the number of RGB channels to choose filtering method
1578 if (ImageView.SelectedImage.Buffers[0].RGBChannelsCount > 1)
1579 {
1580 bitmap = ImageView.SelectedImage.GetFiltered(
1581 coord,
1582 new IntRange((int)ImageView.SelectedBuffer.Stats[0].Min, (int)ImageView.SelectedBuffer.Stats[0].Max),
1583 new IntRange((int)ImageView.SelectedBuffer.Stats[1].Min, (int)ImageView.SelectedBuffer.Stats[1].Max),
1584 new IntRange((int)ImageView.SelectedBuffer.Stats[2].Min, (int)ImageView.SelectedBuffer.Stats[2].Max)
1585 );
1586 }
1587 else
1588 {
1589 bitmap = ImageView.SelectedImage.GetFiltered(
1590 coord,
1591 new IntRange((int)ImageView.SelectedBuffer.Stats[0].Min, (int)ImageView.SelectedBuffer.Stats[0].Max),
1592 new IntRange((int)ImageView.SelectedBuffer.Stats[0].Min, (int)ImageView.SelectedBuffer.Stats[0].Max),
1593 new IntRange((int)ImageView.SelectedBuffer.Stats[0].Min, (int)ImageView.SelectedBuffer.Stats[0].Max)
1594 );
1595 }
1596
1597 // Crop the image to the selected rectangle
1598 bitmap.Crop(rectInImageSpace.ToRectangleInt());
1599 // Determine the threshold based on magicSelect settings
1600 Statistics[] st = Statistics.FromBytes(bitmap.Bytes, bitmap.SizeX, bitmap.Height, bitmap.RGBChannelsCount, bitmap.BitsPerPixel, bitmap.Stride, bitmap.PixelFormat);
1601 int threshold = magicSelect.Numeric ? magicSelect.Threshold : CalculateThreshold(magicSelect.Index, st[0]);
1602 BlobCounter blobCounter = new BlobCounter();
1603 OtsuThreshold th = new OtsuThreshold();
1604 bitmap.To8Bit();
1605 th.ApplyInPlace(bitmap);
1606 blobCounter.FilterBlobs = true;
1607 blobCounter.MinWidth = 2;
1608 blobCounter.MinHeight = 2;
1609 blobCounter.ProcessImage(bitmap);
1610 // Retrieve detected blobs
1611 Blob[] blobs = blobCounter.GetObjectsInformation();
1612 double pixelSizeX = ImageView.SelectedImage.PhysicalSizeX;
1613 double pixelSizeY = ImageView.SelectedImage.PhysicalSizeY;
1614 // Annotate detected blobs in the original image
1615 foreach (Blob blob in blobs)
1616 {
1617 AForge.RectangleD blobRectangle = new AForge.RectangleD(
1618 blob.Rectangle.X * pixelSizeX,
1619 blob.Rectangle.Y * pixelSizeY,
1620 blob.Rectangle.Width * pixelSizeX,
1621 blob.Rectangle.Height * pixelSizeY
1622 );
1623 // Calculate the location of the detected blob
1624 PointD location = new PointD(rectangle.X + blobRectangle.X, rectangle.Y + blobRectangle.Y);
1625 // Create and add ROI annotation
1626 ROI annotation = ROI.CreateRectangle(coord, location.X, location.Y, blobRectangle.W, blobRectangle.H);
1627 ImageView.SelectedImage.Annotations.Add(annotation);
1628 }
1629 App.viewer.UpdateView(true);
1630 }
1631
1632 /* It's a class that defines a tool */
1633 public class Tool
1634 {
1635 static int gridW = 2;
1636 static int gridH = 11;
1637 /* Defining an enum. */
1638 public enum ToolType
1639 {
1640 color,
1641 annotation,
1642 select,
1643 function
1644 }
1645 public enum Type
1646 {
1647 pan,
1648 move,
1649 point,
1650 line,
1651 rect,
1652 ellipse,
1653 polygon,
1654 text,
1655 delete,
1656 freeform,
1657 magic,
1658 brush,
1659 bucket,
1660 eraser,
1661 dropper,
1662 switchColors,
1663 color1,
1664 color2,
1665 script,
1666 }
1667
1669 public static void Init()
1670 {
1671 int w = 33;
1672 int h = 33;
1673 string s = System.IO.Path.GetDirectoryName(Environment.ProcessPath) + "/";
1674 tools.Clear();
1675 if (tools.Count == 0)
1676 {
1677 int x = 0; int y = 0;
1678 foreach (Tool.Type t in (Tool.Type[])Enum.GetValues(typeof(Tool.Type)))
1679 {
1680 if (t != Type.script)
1681 if (t != Type.color1 && t != Type.color2)
1682 {
1683 Tool tool = new Tool(t, new ColorS(0, 0, 0));
1684 tool.bounds = new Rectangle(x * w, y * h, w, h);
1685 tool.image = new Pixbuf(s + "Images/" + tool.type.ToString() + ".png");
1686 tools.Add(tool.ToString(), tool);
1687 }
1688 else
1689 {
1690 Tool tool = new Tool(t, new ColorS(0, 0, 0));
1691 tool.bounds = new Rectangle(x * w, y * h, w, h);
1692 tools.Add(tool.ToString(), tool);
1693 }
1694 x++;
1695 if (x > Tools.gridW - 1)
1696 {
1697 x = 0;
1698 y++;
1699 }
1700 }
1701 }
1702 DrawColor = new ColorS(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
1703 }
1704 public List<AForge.Point> Points;
1705 public ToolType toolType;
1706 public bool selected;
1707 public Pixbuf image;
1708 public Rectangle bounds;
1709 private RectangleD rect;
1710 public RectangleD Rectangle
1711 {
1712 get { return rect; }
1713 set { rect = value; }
1714 }
1715 public RectangleF RectangleF
1716 {
1717 get { return new RectangleF((float)rect.X, (float)rect.Y, (float)rect.W, (float)rect.H); }
1718 }
1719 public string script;
1720 public Type type;
1721 public ColorS tolerance;
1722 public ColorS color;
1723 public Tool()
1724 {
1725 tolerance = new ColorS(0, 0, 0);
1726 }
1727 public Tool(Type t)
1728 {
1729 type = t;
1730 tolerance = new ColorS(0, 0, 0);
1731 }
1732 public Tool(Type t, ColorS col)
1733 {
1734 type = t;
1735 color = col;
1736 tolerance = new ColorS(0, 0, 0);
1737 }
1738 public Tool(Type t, RectangleD r)
1739 {
1740 type = t;
1741 rect = r;
1742 tolerance = new ColorS(0, 0, 0);
1743 }
1744 public Tool(Type t, string sc)
1745 {
1746 type = t;
1747 script = sc;
1748 tolerance = new ColorS(0, 0, 0);
1749 }
1750 public override string ToString()
1751 {
1752 return type.ToString();
1753 }
1754
1755 }
1756
1757 public static Tool currentTool;
1758 /* Setting the color of the drawing tool. */
1759 public static ColorS DrawColor
1760 {
1761 get
1762 {
1763 return drawColor;
1764 }
1765 set
1766 {
1767 drawColor = value;
1768 tools[Tool.Type.color1.ToString()].color = drawColor;
1769 }
1770 }
1771 /* Setting the color of the eraser. */
1772 public static ColorS EraseColor
1773 {
1774 get
1775 {
1776 return eraseColor;
1777 }
1778 set
1779 {
1780 eraseColor = value;
1781 tools[Tool.Type.color2.ToString()].color = eraseColor;
1782 }
1783 }
1784
1785 /* A property that is used to set the width of the stroke. */
1786 public static int StrokeWidth
1787 {
1788 get
1789 {
1790 return width;
1791 }
1792 set
1793 {
1794 width = value;
1795 }
1796 }
1797
1798 public bool IsPanning { get; set; }
1799
1800 public static RectangleD selectionRect;
1801 public static TextInput ti = null;
1802
1809 public static Tool GetTool(string name)
1810 {
1811 if (tools.Count == 0)
1812 return null;
1813 if (tools.ContainsKey(name))
1814 return (Tool)tools[name];
1815 else return tools[Tool.Type.move.ToString()];
1816 }
1817 public static Tool GetTool(Tool.Type t)
1818 {
1819 if (tools.ContainsKey(t.ToString()))
1820 return (Tool)tools[t.ToString()];
1821 else return tools.FirstOrDefault().Value;
1822 }
1826 public static int CalculateThreshold(int index, Statistics stat)
1827 {
1828 return index switch
1829 {
1830 2 => (int)(stat.Min + stat.Mean),
1831 1 => (int)stat.Median,
1832 _ => (int)stat.Min
1833 };
1834 }
1835 }
1836 // -----------------------------
1837 // RectangleD extensions / helpers
1838 // -----------------------------
1839 public static class RectangleDExtensions
1840 {
1841 // Handles negative width/height gracefully
1842 public static bool Contains(this RectangleD r, ZCT coord, PointD p)
1843 {
1844 if (App.viewer.GetCoordinate() == coord)
1845 {
1846 double left = Math.Min(r.X, r.X + r.W);
1847 double right = Math.Max(r.X, r.X + r.W);
1848 double top = Math.Min(r.Y, r.Y + r.H);
1849 double bottom = Math.Max(r.Y, r.Y + r.H);
1850 return p.X >= left && p.X <= right && p.Y >= top && p.Y <= bottom;
1851 }
1852 return false;
1853 }
1854
1855 public static RectangleD FromCorners(PointD a, PointD b)
1856 {
1857 double x = Math.Min(a.X, b.X);
1858 double y = Math.Min(a.Y, b.Y);
1859 double w = Math.Abs(b.X - a.X);
1860 double h = Math.Abs(b.Y - a.Y);
1861 return new RectangleD(x, y, w, h);
1862 }
1863 }
1864
1865}
static MagicSelect Create(int index)
static TextInput Create(bool ROItype=true)
Definition TextInput.cs:36
static void Init()
It adds all the tools to the tools list.
Definition Tools.cs:1669
Implementations for all tool types defined in Tools.cs.
Definition Tools.cs:23
static Tool GetTool(string name)
Definition Tools.cs:1809
static Tools Create()
Definition Tools.cs:36
static int CalculateThreshold(int index, Statistics stat)
Helper method to calculate threshold for magic select.
Definition Tools.cs:1826
void SetupHandlers()
Sets up the handlers.
Definition Tools.cs:143
Definition ML.cs:17