nettracer3d 0.8.6__py3-none-any.whl → 0.8.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -2172,6 +2172,15 @@ class ImageViewerWindow(QMainWindow):
2172
2172
  if self.machine_window is not None:
2173
2173
  self.machine_window.silence_button()
2174
2174
  self.canvas.setCursor(Qt.CursorShape.CrossCursor)
2175
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
2176
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
2177
+ (hasattr(self, 'current_operation') and self.current_operation):
2178
+ # Finish current operation first
2179
+ if hasattr(self, 'current_operation') and self.current_operation:
2180
+ self.pm.finish_current_virtual_operation()
2181
+ # Now convert to real data
2182
+ self.pm.convert_virtual_strokes_to_data()
2183
+ self.update_display()
2175
2184
  if self.pan_mode or self.brush_mode:
2176
2185
  current_xlim = self.ax.get_xlim()
2177
2186
  current_ylim = self.ax.get_ylim()
@@ -2196,6 +2205,15 @@ class ImageViewerWindow(QMainWindow):
2196
2205
  self.threed = False
2197
2206
  self.last_change = None
2198
2207
  self.brush_mode = False
2208
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
2209
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
2210
+ (hasattr(self, 'current_operation') and self.current_operation):
2211
+ # Finish current operation first
2212
+ if hasattr(self, 'current_operation') and self.current_operation:
2213
+ self.pm.finish_current_virtual_operation()
2214
+ # Now convert to real data
2215
+ self.pm.convert_virtual_strokes_to_data()
2216
+ self.update_display()
2199
2217
  if self.machine_window is not None:
2200
2218
  self.machine_window.silence_button()
2201
2219
  self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
@@ -2229,8 +2247,6 @@ class ImageViewerWindow(QMainWindow):
2229
2247
  else:
2230
2248
  channel = 2
2231
2249
 
2232
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2233
-
2234
2250
  if self.pan_mode:
2235
2251
  current_xlim = self.ax.get_xlim()
2236
2252
  current_ylim = self.ax.get_ylim()
@@ -2242,6 +2258,15 @@ class ImageViewerWindow(QMainWindow):
2242
2258
  self.zoom_mode = False
2243
2259
  self.update_brush_cursor()
2244
2260
  else:
2261
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
2262
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
2263
+ (hasattr(self, 'current_operation') and self.current_operation):
2264
+ # Finish current operation first
2265
+ if hasattr(self, 'current_operation') and self.current_operation:
2266
+ self.pm.finish_current_virtual_operation()
2267
+ # Now convert to real data
2268
+ self.pm.convert_virtual_strokes_to_data()
2269
+ self.update_display()
2245
2270
  # Get current zoom and do display update
2246
2271
  current_xlim = self.ax.get_xlim()
2247
2272
  current_ylim = self.ax.get_ylim()
@@ -2525,21 +2550,16 @@ class ImageViewerWindow(QMainWindow):
2525
2550
  if event.inaxes != self.ax:
2526
2551
  return
2527
2552
 
2528
- """
2529
- try:
2530
- if self.machine_window is not None and not self.machine_window.segmentation_worker._paused:
2531
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2532
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2553
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2554
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2533
2555
 
2534
- if self.pen_button.isChecked():
2535
- channel = self.active_channel
2536
- else:
2537
- channel = 2
2556
+ if self.pen_button.isChecked():
2557
+ channel = self.active_channel
2558
+ else:
2559
+ channel = 2
2560
+
2561
+ self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2538
2562
 
2539
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2540
- except:
2541
- pass
2542
- """
2543
2563
 
2544
2564
  if event.button == 1 or event.button == 3:
2545
2565
  if self.machine_window is not None:
@@ -2555,7 +2575,7 @@ class ImageViewerWindow(QMainWindow):
2555
2575
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2556
2576
 
2557
2577
  if event.button == 1 and getattr(self, 'can', False):
2558
- self.update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = True)
2578
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2559
2579
  self.handle_can(x, y)
2560
2580
  return
2561
2581
 
@@ -3121,24 +3141,26 @@ class ImageViewerWindow(QMainWindow):
3121
3141
 
3122
3142
  if not hasattr(self, 'zoom_changed'):
3123
3143
  self.zoom_changed = False
3124
-
3144
+
3125
3145
  self.canvas.draw()
3126
3146
 
3127
3147
  # Handle brush mode cleanup with paint session management
3128
3148
  if self.brush_mode and hasattr(self, 'painting') and self.painting:
3149
+
3150
+ self.pm.connect_virtual_paint_points()
3151
+ self.pm.update_virtual_paint_display()
3152
+
3153
+
3129
3154
  # Finish current operation
3155
+ self.pm.finish_current_stroke()
3130
3156
  self.pm.finish_current_virtual_operation()
3131
3157
 
3132
3158
  # Reset last position for next stroke
3133
- self.last_virtual_pos = None
3159
+ #self.last_virtual_pos = None
3134
3160
 
3135
3161
  # End this stroke but keep session active for continuous painting
3136
3162
  self.painting = False
3137
3163
 
3138
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3139
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3140
-
3141
- self.update_display(preserve_zoom=(current_xlim, current_ylim), continue_paint = True)
3142
3164
 
3143
3165
  if self.resume:
3144
3166
  self.machine_window.segmentation_worker.resume()
@@ -3264,7 +3286,6 @@ class ImageViewerWindow(QMainWindow):
3264
3286
 
3265
3287
  self.zoom_changed = False # Flag that zoom has changed
3266
3288
 
3267
-
3268
3289
 
3269
3290
  self.canvas.draw()
3270
3291
 
@@ -5110,6 +5131,14 @@ class ImageViewerWindow(QMainWindow):
5110
5131
  """Actually perform the slice update after debounce delay."""
5111
5132
  if self.pending_slice is not None:
5112
5133
  slice_value, view_settings = self.pending_slice
5134
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
5135
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
5136
+ (hasattr(self, 'current_operation') and self.current_operation):
5137
+ # Finish current operation first
5138
+ if hasattr(self, 'current_operation') and self.current_operation:
5139
+ self.pm.finish_current_virtual_operation()
5140
+ # Now convert to real data
5141
+ self.pm.convert_virtual_strokes_to_data()
5113
5142
  self.current_slice = slice_value
5114
5143
  if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
5115
5144
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
@@ -5133,7 +5162,7 @@ class ImageViewerWindow(QMainWindow):
5133
5162
 
5134
5163
 
5135
5164
 
5136
- def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, continue_paint = False, skip_paint_reinit = False):
5165
+ def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
5137
5166
  """Update the display with currently visible channels and highlight overlay."""
5138
5167
  try:
5139
5168
  self.figure.clear()
@@ -5163,17 +5192,17 @@ class ImageViewerWindow(QMainWindow):
5163
5192
  self.restore_channels = []
5164
5193
  except:
5165
5194
  pass
5166
- if not continue_paint:
5167
- self.static_background = None
5168
5195
 
5169
- if self.machine_window is None:
5170
- try:
5171
- self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
5172
- self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5173
- self.channel_data[4] = None
5174
- self.channel_visible[4] = False
5175
- except:
5176
- pass
5196
+ self.static_background = None
5197
+
5198
+ if self.machine_window is None:
5199
+ try:
5200
+ self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
5201
+ self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5202
+ self.channel_data[4] = None
5203
+ self.channel_visible[4] = False
5204
+ except:
5205
+ pass
5177
5206
 
5178
5207
  # Get active channels and their dimensions
5179
5208
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
@@ -5404,17 +5433,6 @@ class ImageViewerWindow(QMainWindow):
5404
5433
 
5405
5434
  self.canvas.draw()
5406
5435
 
5407
- if self.brush_mode and not skip_paint_reinit:
5408
- # Get current zoom to preserve it
5409
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
5410
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
5411
-
5412
- if self.pen_button.isChecked():
5413
- channel = self.active_channel
5414
- else:
5415
- channel = 2
5416
-
5417
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
5418
5436
 
5419
5437
  except:
5420
5438
  import traceback
@@ -9770,18 +9788,6 @@ class MachineWindow(QMainWindow):
9770
9788
 
9771
9789
  self.parent().pm = painting.PaintManager(parent = self.parent())
9772
9790
 
9773
- # Start virtual paint session
9774
- # Get current zoom to preserve it
9775
- current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
9776
- current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
9777
-
9778
- if self.parent().pen_button.isChecked():
9779
- channel = self.parent().active_channel
9780
- else:
9781
- channel = 2
9782
-
9783
- self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
9784
-
9785
9791
  self.parent().pan_button.setChecked(False)
9786
9792
  self.parent().zoom_button.setChecked(False)
9787
9793
  if self.parent().pan_mode:
@@ -9809,6 +9815,15 @@ class MachineWindow(QMainWindow):
9809
9815
  # Wait a bit for cleanup
9810
9816
  time.sleep(0.1)
9811
9817
 
9818
+ if (hasattr(self.parent(), 'virtual_draw_operations') and self.parent().virtual_draw_operations) or \
9819
+ (hasattr(self.parent(), 'virtual_erase_operations') and self.parent().virtual_erase_operations) or \
9820
+ (hasattr(self.parent(), 'current_operation') and self.parent().current_operation):
9821
+ # Finish current operation first
9822
+ if hasattr(self.parent(), 'current_operation') and self.parent().current_operation:
9823
+ self.parent().pm.finish_current_virtual_operation()
9824
+ # Now convert to real data
9825
+ self.parent().pm.convert_virtual_strokes_to_data()
9826
+
9812
9827
  self.previewing = True
9813
9828
  try:
9814
9829
  try:
@@ -9852,7 +9867,7 @@ class MachineWindow(QMainWindow):
9852
9867
  return
9853
9868
  else:
9854
9869
  self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
9855
- self.segmentation_worker.chunk_processed.connect(lambda: self.update_display(skip_paint_reinit = True)) # Just update display
9870
+ self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
9856
9871
  current_xlim = self.parent().ax.get_xlim()
9857
9872
  current_ylim = self.parent().ax.get_ylim()
9858
9873
  try:
@@ -9905,7 +9920,7 @@ class MachineWindow(QMainWindow):
9905
9920
 
9906
9921
  return changed
9907
9922
 
9908
- def update_display(self, skip_paint_reinit = False):
9923
+ def update_display(self):
9909
9924
  if not hasattr(self, '_last_update'):
9910
9925
  self._last_update = 0
9911
9926
 
@@ -9932,18 +9947,7 @@ class MachineWindow(QMainWindow):
9932
9947
 
9933
9948
  if not self.parent().painting:
9934
9949
  # Only update if view limits are valid
9935
- self.parent().update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = skip_paint_reinit)
9936
-
9937
- if self.parent().brush_mode:
9938
- current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
9939
- current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
9940
-
9941
- if self.parent().pen_button.isChecked():
9942
- channel = self.parent().active_channel
9943
- else:
9944
- channel = 2
9945
-
9946
- self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
9950
+ self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
9947
9951
 
9948
9952
  self._last_update = current_time
9949
9953
  except Exception as e:
nettracer3d/painting.py CHANGED
@@ -7,8 +7,26 @@ import numpy as np
7
7
  class PaintManager(QMainWindow):
8
8
  def __init__(self, parent = None):
9
9
  super().__init__(parent)
10
-
11
10
  self.resume = False
11
+
12
+ # Initialize stroke tracking storage once
13
+ if parent is not None:
14
+ if not hasattr(parent, 'completed_paint_strokes'):
15
+ parent.completed_paint_strokes = [] # List of individual completed strokes
16
+ if not hasattr(parent, 'current_stroke_points'):
17
+ parent.current_stroke_points = [] # Current stroke being drawn
18
+ if not hasattr(parent, 'current_stroke_type'):
19
+ parent.current_stroke_type = None # 'draw' or 'erase'
20
+
21
+ # Keep the old properties for display purposes
22
+ if not hasattr(parent, 'virtual_draw_operations'):
23
+ parent.virtual_draw_operations = []
24
+ if not hasattr(parent, 'virtual_erase_operations'):
25
+ parent.virtual_erase_operations = []
26
+ if not hasattr(parent, 'current_operation'):
27
+ parent.current_operation = []
28
+ if not hasattr(parent, 'current_operation_type'):
29
+ parent.current_operation_type = None
12
30
 
13
31
  def get_line_points(self, x0, y0, x1, y1):
14
32
  """Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
@@ -72,11 +90,35 @@ class PaintManager(QMainWindow):
72
90
  if not self.parent().channel_visible[channel]:
73
91
  self.parent().channel_visible[channel] = True
74
92
 
75
- # Initialize virtual paint storage - separate draw and erase operations
76
- self.parent().virtual_draw_operations = [] # Stores drawing operations
77
- self.parent().virtual_erase_operations = [] # Stores erase operations
93
+ # Initialize stroke tracking storage ONLY if they don't exist
94
+ if not hasattr(self.parent(), 'completed_paint_strokes'):
95
+ self.parent().completed_paint_strokes = []
96
+ if not hasattr(self.parent(), 'current_stroke_points'):
97
+ self.parent().current_stroke_points = []
98
+ if not hasattr(self.parent(), 'current_stroke_type'):
99
+ self.parent().current_stroke_type = None
100
+
101
+ # Initialize display storage ONLY if they don't exist
102
+ if not hasattr(self.parent(), 'virtual_draw_operations'):
103
+ self.parent().virtual_draw_operations = []
104
+ if not hasattr(self.parent(), 'virtual_erase_operations'):
105
+ self.parent().virtual_erase_operations = []
106
+ if not hasattr(self.parent(), 'current_operation'):
107
+ self.parent().current_operation = []
108
+ if not hasattr(self.parent(), 'current_operation_type'):
109
+ self.parent().current_operation_type = None
110
+
111
+ def reset_all_paint_storage(self):
112
+ """Reset all paint storage - call this when you want to start completely fresh."""
113
+ self.parent().completed_paint_strokes = []
114
+ self.parent().current_stroke_points = []
115
+ self.parent().current_stroke_type = None
116
+ self.parent().virtual_draw_operations = []
117
+ self.parent().virtual_erase_operations = []
78
118
  self.parent().current_operation = []
79
- self.parent().current_operation_type = None # 'draw' or 'erase'
119
+ self.parent().current_operation_type = None
120
+
121
+
80
122
 
81
123
  def add_virtual_paint_point(self, x, y, brush_size, erase=False, foreground=True):
82
124
  """Add a single paint point to the virtual layer."""
@@ -111,11 +153,19 @@ class PaintManager(QMainWindow):
111
153
  'threedthresh': getattr(self.parent(), 'threedthresh', 1)
112
154
  }
113
155
 
114
- # Create visual circle
156
+ # Add to stroke tracking (NEW - separate stroke tracking)
157
+ if self.parent().current_stroke_type != operation_type:
158
+ # Finish previous stroke if switching between draw/erase
159
+ self.finish_current_stroke()
160
+ self.parent().current_stroke_type = operation_type
161
+
162
+ self.parent().current_stroke_points.append(operation_data)
163
+
164
+ # Create visual circle for display
115
165
  circle = plt.Circle((x, y), brush_size/2,
116
166
  color=paint_color, alpha=alpha, animated=True)
117
167
 
118
- # Add to current operation
168
+ # Add to display operations (OLD - for visual display)
119
169
  if self.parent().current_operation_type != operation_type:
120
170
  # Finish previous operation if switching between draw/erase
121
171
  self.finish_current_virtual_operation()
@@ -128,6 +178,23 @@ class PaintManager(QMainWindow):
128
178
 
129
179
  self.parent().ax.add_patch(circle)
130
180
 
181
+ def finish_current_stroke(self):
182
+ """Finish the current stroke and add it to completed strokes."""
183
+ if not self.parent().current_stroke_points:
184
+ return
185
+
186
+ # Store the completed stroke with its type
187
+ stroke_data = {
188
+ 'points': self.parent().current_stroke_points.copy(),
189
+ 'type': self.parent().current_stroke_type
190
+ }
191
+
192
+ self.parent().completed_paint_strokes.append(stroke_data)
193
+
194
+ # Clear current stroke
195
+ self.parent().current_stroke_points = []
196
+ self.parent().current_stroke_type = None
197
+
131
198
  def add_virtual_paint_stroke(self, x, y, brush_size, erase=False, foreground=True):
132
199
  """Add a paint stroke - simple visual, interpolation happens during data conversion."""
133
200
  # Just add the current point for visual display (no interpolation yet)
@@ -136,22 +203,119 @@ class PaintManager(QMainWindow):
136
203
  # Store the last position for data conversion later
137
204
  self.parent().last_virtual_pos = (x, y)
138
205
 
139
- def finish_current_virtual_operation(self):
140
- """Finish the current operation (draw or erase) and add it to the appropriate list."""
206
+ def connect_virtual_paint_points(self):
207
+ """Connect points with lines matching the circle size by converting to screen coordinates."""
141
208
 
142
- if not self.parent().current_operation:
209
+ if not hasattr(self.parent(), 'current_operation') or len(self.parent().current_operation) < 2:
143
210
  return
144
-
145
- if self.parent().current_operation_type == 'draw':
146
- self.parent().virtual_draw_operations.append(self.parent().current_operation)
147
- elif self.parent().current_operation_type == 'erase':
148
- self.parent().virtual_erase_operations.append(self.parent().current_operation)
149
211
 
150
- self.parent().current_operation = []
151
- self.parent().current_operation_type = None
212
+ # Get existing points but DON'T remove them
213
+ existing_points = self.parent().current_operation.copy()
214
+ point_data = [item['data'] for item in existing_points if 'data' in item]
215
+
216
+ if len(point_data) < 2:
217
+ return
218
+
219
+ # Get visual properties and brush size from first point
220
+ first_data = point_data[0]
221
+ brush_size_data = first_data['brush_size'] # This is in data coordinates
222
+
223
+ # Convert brush size from data coordinates to points for linewidth
224
+ # Get the transformation from data to display coordinates
225
+ ax = self.parent().ax
226
+
227
+ # Transform two points to see the scaling
228
+ p1_data = [0, 0]
229
+ p2_data = [brush_size_data, 0] # One brush_size unit away
230
+
231
+ p1_display = ax.transData.transform(p1_data)
232
+ p2_display = ax.transData.transform(p2_data)
233
+
234
+ # Calculate pixels per data unit
235
+ pixels_per_data_unit = abs(p2_display[0] - p1_display[0])
236
+
237
+ # Convert to points (matplotlib uses 72 points per inch, figure.dpi pixels per inch)
238
+ fig = ax.figure
239
+ points_per_pixel = 72.0 / fig.dpi
240
+ brush_size_points = pixels_per_data_unit * points_per_pixel
241
+
242
+ if first_data['erase']:
243
+ line_color = 'black'
244
+ alpha = 0.5
245
+ else:
246
+ if self.parent().machine_window is not None:
247
+ if first_data['foreground']:
248
+ line_color = 'green'
249
+ alpha = 0.7
250
+ else:
251
+ line_color = 'red'
252
+ alpha = 0.7
253
+ else:
254
+ line_color = 'white'
255
+ alpha = 0.7
256
+
257
+ # Create line segments for connections using LineCollection
258
+ from matplotlib.collections import LineCollection
259
+
260
+ segments = []
261
+ for i in range(len(point_data) - 1):
262
+ x1, y1 = point_data[i]['x'], point_data[i]['y']
263
+ x2, y2 = point_data[i+1]['x'], point_data[i+1]['y']
264
+ segments.append([(x1, y1), (x2, y2)])
265
+
266
+ # Create line collection with converted linewidth
267
+ if segments:
268
+ lc = LineCollection(segments,
269
+ colors=line_color,
270
+ alpha=alpha,
271
+ linewidths=brush_size_points, # Now in points, matching circles
272
+ animated=True)
273
+ self.parent().ax.add_collection(lc)
274
+
275
+ # Add the line collection as a visual-only element
276
+ self.parent().current_operation.append({
277
+ 'line_collection': lc,
278
+ 'is_connection_visual': True
279
+ })
280
+
281
+ def finish_current_virtual_operation(self):
282
+ """Finish the current operation (draw or erase) and add it to the appropriate list."""
283
+
284
+ if not self.parent().current_operation:
285
+ return
286
+
287
+ # Filter out connection visuals from the operation before storing
288
+ data_items = []
289
+ visual_items = []
290
+
291
+ for item in self.parent().current_operation:
292
+ if item.get('is_connection_visual', False):
293
+ visual_items.append(item)
294
+ else:
295
+ data_items.append(item)
296
+
297
+ # Only store the data items for this specific stroke
298
+ if data_items:
299
+ if self.parent().current_operation_type == 'draw':
300
+ self.parent().virtual_draw_operations.append(data_items)
301
+ elif self.parent().current_operation_type == 'erase':
302
+ self.parent().virtual_erase_operations.append(data_items)
303
+
304
+ # Clean up visual items that are connection-only
305
+ for item in visual_items:
306
+ try:
307
+ if 'line_collection' in item:
308
+ item['line_collection'].remove()
309
+ elif 'line' in item:
310
+ item['line'].remove()
311
+ except:
312
+ pass
313
+
314
+ self.parent().current_operation = []
315
+ self.parent().current_operation_type = None
152
316
 
153
317
  def update_virtual_paint_display(self):
154
- """Update display with virtual paint strokes - super fast like selection rectangle."""
318
+ """Update display with virtual paint strokes - handles different object types."""
155
319
  if not hasattr(self.parent(), 'static_background') or self.parent().static_background is None:
156
320
  return
157
321
 
@@ -162,108 +326,118 @@ class PaintManager(QMainWindow):
162
326
  for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
163
327
  for operation in operation_list:
164
328
  for item in operation:
165
- self.parent().ax.draw_artist(item['circle'])
329
+ self._draw_virtual_item(item)
166
330
 
167
331
  # Draw current operation being painted
168
332
  if hasattr(self.parent(), 'current_operation'):
169
333
  for item in self.parent().current_operation:
170
- self.parent().ax.draw_artist(item['circle'])
334
+ self._draw_virtual_item(item)
171
335
 
172
336
  # Blit everything at once
173
337
  self.parent().canvas.blit(self.parent().ax.bbox)
174
338
 
339
+ def _draw_virtual_item(self, item):
340
+ """Helper method to draw different types of virtual paint items."""
341
+ try:
342
+ # Skip items that are marked as visual-only connections
343
+ if item.get('is_connection_visual', False):
344
+ if 'line' in item:
345
+ self.parent().ax.draw_artist(item['line'])
346
+ elif 'line_collection' in item:
347
+ self.parent().ax.draw_artist(item['line_collection'])
348
+ elif 'circle' in item:
349
+ self.parent().ax.draw_artist(item['circle'])
350
+ elif 'line' in item:
351
+ self.parent().ax.draw_artist(item['line'])
352
+ elif 'line_collection' in item:
353
+ self.parent().ax.draw_artist(item['line_collection'])
354
+ except Exception as e:
355
+ # Skip items that can't be drawn (might have been removed)
356
+ pass
357
+
175
358
  def convert_virtual_strokes_to_data(self):
176
- """Convert virtual paint strokes to actual array data with interpolation applied here."""
359
+ """Convert each stroke separately to actual array data using ONLY the new stroke tracking system."""
177
360
 
178
- # First, apply all drawing operations with interpolation
179
- for operation in self.parent().virtual_draw_operations:
180
- last_pos = None
181
- for item in operation:
182
- data = item['data']
183
- current_pos = (data['x'], data['y'])
184
-
185
- if last_pos is not None:
186
- points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
187
- for px, py in points:
188
- self.paint_at_position_vectorized(
189
- px, py,
190
- erase=False,
191
- channel=data['channel'],
192
- brush_size=data['brush_size'],
193
- threed=data['threed'], # Add this
194
- threedthresh=data['threedthresh'], # Add this
195
- foreground=data['foreground'],
196
- machine_window=self.parent().machine_window
197
- )
198
- else:
199
- self.paint_at_position_vectorized(
200
- data['x'], data['y'],
201
- erase=False,
202
- channel=data['channel'],
203
- brush_size=data['brush_size'],
204
- threed=data['threed'], # Add this
205
- threedthresh=data['threedthresh'], # Add this
206
- foreground=data['foreground'],
207
- machine_window=self.parent().machine_window
208
- )
209
-
210
- last_pos = current_pos
211
- try:
212
- item['circle'].remove()
213
- except:
214
- pass
361
+ # Finish the current stroke first
362
+ self.finish_current_stroke()
215
363
 
216
- # Then, apply all erase operations with interpolation (same changes)
217
- for operation in self.parent().virtual_erase_operations:
364
+ # Process ONLY the completed_paint_strokes (ignore old display operations)
365
+ for stroke in self.parent().completed_paint_strokes:
366
+ stroke_points = stroke['points']
367
+ stroke_type = stroke['type']
368
+
369
+ if len(stroke_points) == 0:
370
+ continue
371
+
372
+ # Apply interpolation within this stroke only
218
373
  last_pos = None
219
- for item in operation:
220
- data = item['data']
221
- current_pos = (data['x'], data['y'])
374
+ for point_data in stroke_points:
375
+ current_pos = (point_data['x'], point_data['y'])
222
376
 
223
377
  if last_pos is not None:
378
+ # Interpolate between consecutive points in this stroke
224
379
  points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
225
380
  for px, py in points:
226
381
  self.paint_at_position_vectorized(
227
382
  px, py,
228
- erase=True,
229
- channel=data['channel'],
230
- brush_size=data['brush_size'],
231
- threed=data['threed'], # Add this
232
- threedthresh=data['threedthresh'], # Add this
233
- foreground=data['foreground'],
383
+ erase=point_data['erase'],
384
+ channel=point_data['channel'],
385
+ brush_size=point_data['brush_size'],
386
+ threed=point_data['threed'],
387
+ threedthresh=point_data['threedthresh'],
388
+ foreground=point_data['foreground'],
234
389
  machine_window=self.parent().machine_window
235
390
  )
236
391
  else:
392
+ # First point in stroke
237
393
  self.paint_at_position_vectorized(
238
- data['x'], data['y'],
239
- erase=True,
240
- channel=data['channel'],
241
- brush_size=data['brush_size'],
242
- threed=data['threed'], # Add this
243
- threedthresh=data['threedthresh'], # Add this
244
- foreground=data['foreground'],
394
+ point_data['x'], point_data['y'],
395
+ erase=point_data['erase'],
396
+ channel=point_data['channel'],
397
+ brush_size=point_data['brush_size'],
398
+ threed=point_data['threed'],
399
+ threedthresh=point_data['threedthresh'],
400
+ foreground=point_data['foreground'],
245
401
  machine_window=self.parent().machine_window
246
402
  )
247
403
 
248
404
  last_pos = current_pos
249
- try:
250
- item['circle'].remove()
251
- except:
252
- pass
253
405
 
254
- # Clean up
255
- self.parent().virtual_draw_operations = []
256
- self.parent().virtual_erase_operations = []
406
+ # Clean up ALL visual elements (both old and new systems)
407
+ for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
408
+ for operation in operation_list:
409
+ for item in operation:
410
+ try:
411
+ if 'circle' in item:
412
+ item['circle'].remove()
413
+ elif 'line_collection' in item:
414
+ item['line_collection'].remove()
415
+ elif 'line' in item:
416
+ item['line'].remove()
417
+ except:
418
+ pass
419
+
257
420
  if hasattr(self.parent(), 'current_operation'):
258
421
  for item in self.parent().current_operation:
259
422
  try:
260
- item['circle'].remove()
423
+ if 'circle' in item:
424
+ item['circle'].remove()
425
+ elif 'line_collection' in item:
426
+ item['line_collection'].remove()
427
+ elif 'line' in item:
428
+ item['line'].remove()
261
429
  except:
262
430
  pass
263
- self.parent().current_operation = []
431
+
432
+ # Reset all storage for next paint session
433
+ self.parent().completed_paint_strokes = []
434
+ self.parent().current_stroke_points = []
435
+ self.parent().current_stroke_type = None
436
+ self.parent().virtual_draw_operations = []
437
+ self.parent().virtual_erase_operations = []
438
+ self.parent().current_operation = []
264
439
  self.parent().current_operation_type = None
265
440
 
266
-
267
441
  def end_virtual_paint_session(self):
268
442
  """Convert virtual paint to actual array modifications when exiting paint mode."""
269
443
  if not hasattr(self.parent(), 'virtual_paint_strokes'):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.8.6
3
+ Version: 0.8.7
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -110,6 +110,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
110
110
 
111
111
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
112
112
 
113
- -- Version 0.8.6 Updates --
113
+ -- Version 0.8.7 Updates --
114
114
 
115
115
  * See Documentation Once Updated
@@ -6,20 +6,20 @@ nettracer3d/modularity.py,sha256=O9OeKbjD3v6gSFz9K2GzP6LsxlpQaPfeJbM1pyIEigw,217
6
6
  nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
7
7
  nettracer3d/neighborhoods.py,sha256=VWubD5CBu9aNPhUea7FbAk9aTOq0FLKR9y-1VT7YkAc,39677
8
8
  nettracer3d/nettracer.py,sha256=TEV-nDmkcGP3UjWEor1LtEwm5mFBQu2nB0VRyz9Lt08,253649
9
- nettracer3d/nettracer_gui.py,sha256=JUk6s0awzK_y3AEsmIXZdF24Nz9lw8tfuV6g-E-v48I,539966
9
+ nettracer3d/nettracer_gui.py,sha256=yrWkpWVt7IvgGFkvc8tbMWy8t6O8P_2ulOKW-eOe9B4,540425
10
10
  nettracer3d/network_analysis.py,sha256=kBzsVaq4dZkMe0k-VGvQIUvM-tK0ZZ8bvb-wtsugZRQ,46150
11
11
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
12
12
  nettracer3d/node_draw.py,sha256=kZcR1PekLg0riioNeGcALIXQyZ5PtHA_9MT6z7Zovdk,10401
13
- nettracer3d/painting.py,sha256=gTu-l7CkWiXLj_sQsywIQ5F7igm00zBjJBP62bmjys0,15823
13
+ nettracer3d/painting.py,sha256=K_dwngivw80r-Yyg4btKMsWGn566ZE9PnrQl986uxJE,23497
14
14
  nettracer3d/proximity.py,sha256=bTaucn_InQ-v1GIk8ug-dXvDhIO59rnBMl5nIwAmNyw,35335
15
15
  nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
16
16
  nettracer3d/segmenter.py,sha256=O3xjCimPwoL8LM1w4cKVTB7saY-UptFuYC8qOIo3iWg,61637
17
17
  nettracer3d/segmenter_GPU.py,sha256=3CJLXCiySZP2dJbkpfBoXwAYbV4TnvIYAm6oQv-T-y4,63479
18
18
  nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
19
19
  nettracer3d/smart_dilate.py,sha256=TvRUh6B4q4zIdCO1BWH-xgTdND5OUNmo99eyxG9oIAU,27145
20
- nettracer3d-0.8.6.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
21
- nettracer3d-0.8.6.dist-info/METADATA,sha256=DMFcu_kU32ETwJOlrbLJgTCDUG5Zir6yzuBOAJFO2Kg,7008
22
- nettracer3d-0.8.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- nettracer3d-0.8.6.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
24
- nettracer3d-0.8.6.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
25
- nettracer3d-0.8.6.dist-info/RECORD,,
20
+ nettracer3d-0.8.7.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
21
+ nettracer3d-0.8.7.dist-info/METADATA,sha256=6Q68XlohpUOpHNK6V7doYlQ4R9twu5D9qoJNwoyfs48,7008
22
+ nettracer3d-0.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ nettracer3d-0.8.7.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
24
+ nettracer3d-0.8.7.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
25
+ nettracer3d-0.8.7.dist-info/RECORD,,