Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,163 changes: 1,163 additions & 0 deletions src/main/java/sc/fiji/snt/CurationAssistantPanel.java

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions src/main/java/sc/fiji/snt/InteractiveTracerCanvas.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import sc.fiji.snt.analysis.graph.DirectedWeightedGraph;
import sc.fiji.snt.analysis.graph.DirectedWeightedSubgraph;
import sc.fiji.snt.analysis.graph.SWCWeightedEdge;
import sc.fiji.snt.analysis.curation.PlausibilityCheck;
import sc.fiji.snt.gui.GuiUtils;
import sc.fiji.snt.gui.IconFactory;
import sc.fiji.snt.hyperpanes.MultiDThreePanes;
Expand Down Expand Up @@ -98,6 +99,17 @@ class InteractiveTracerCanvas extends TracerCanvas implements MouseWheelListener
// slight mouse/trackpad movement during click, especially on macOS trackpads.
private static final int CLICK_TOLERANCE_SQ = 25;

private List<PointInImage> warningLocations = new ArrayList<>();
private List<Path> warningAffectedPaths = new ArrayList<>();
private PlausibilityCheck.Severity warningMaxSeverity = null;
private boolean warningViewportTint = false;
private long warningFlashStart = 0;
private static final long FLASH_DURATION_MS = 3000;
private static final long TINT_DURATION_MS = 400;
private static final Color WARNING_RING_COLOR = new Color(255, 165, 0); // orange
private static final Color ERROR_RING_COLOR = new Color(255, 80, 80); // red
private static final Color VIEWPORT_TINT_COLOR = new Color(255, 60, 60, 50); // faint red wash

protected InteractiveTracerCanvas(final ImagePlus imp,
final SNT plugin, final int plane,
final PathAndFillManager pathAndFillManager)
Expand Down Expand Up @@ -749,6 +761,46 @@ protected void clickAtMaxPoint(final boolean join_modifier_pressed) {
tracerPlugin.clickAtMaxPoint(x, y, plane, join_modifier_pressed);
}

/**
* Sets the warning overlay state for the canvas.
*
* @param locations spatial locations of warnings (may be null/empty)
* @param affectedPaths paths involved in warnings, highlighted with a thick overlay
* @param maxSeverity the highest severity among the warnings (drives visual tier)
* @param viewportTint whether to flash a brief viewport tint (for ERRORs when panel not visible)
*/
public void setWarningOverlay(final List<PointInImage> locations,
final List<Path> affectedPaths,
final PlausibilityCheck.Severity maxSeverity,
final boolean viewportTint) {
this.warningLocations = (locations != null) ? new ArrayList<>(locations) : new ArrayList<>();
this.warningAffectedPaths = (affectedPaths != null) ? new ArrayList<>(affectedPaths) : new ArrayList<>();
this.warningMaxSeverity = maxSeverity;
this.warningViewportTint = viewportTint;
this.warningFlashStart = System.currentTimeMillis();
repaint();
}

public void clearWarningOverlay() {
warningLocations.clear();
warningAffectedPaths.clear();
warningMaxSeverity = null;
warningViewportTint = false;
repaint();
}

@Override
public void repaint() {
super.repaint();
// Schedule repaints for pulsing animation while flash is active
if (!warningLocations.isEmpty() || !warningAffectedPaths.isEmpty()) {
final long elapsed = System.currentTimeMillis() - warningFlashStart;
if (elapsed < FLASH_DURATION_MS) {
super.repaint(50);
}
}
}

protected void startShollAnalysis() {
PointInImage centerScaled = null;
if (pathAndFillManager.anySelected()) {
Expand Down Expand Up @@ -1261,6 +1313,88 @@ protected void drawOverlay(final Graphics2D g) {
if (tracerPlugin.manualRadius > 0 && last_x_in_pane_precise != Double.MIN_VALUE) {
drawManualRadiusCursor(g);
}

// Warning overlay (tiered by severity)
drawWarningOverlay(g);
}

private void drawWarningOverlay(final Graphics2D g) {
if (warningLocations.isEmpty() && warningAffectedPaths.isEmpty()) return;
final long elapsed = System.currentTimeMillis() - warningFlashStart;
if (elapsed >= FLASH_DURATION_MS) return;

final Stroke savedStroke = g.getStroke();
final Color savedColor = g.getColor();
final Composite savedComposite = g.getComposite();

// Pulsing alpha: 600ms cycle, sinusoidal
final double phase = (elapsed % 600) / 600.0;
final float pulseAlpha = (float) (0.4 + 0.6 * Math.abs(Math.sin(phase * Math.PI)));
final Color baseColor = (warningMaxSeverity == PlausibilityCheck.Severity.ERROR)
? ERROR_RING_COLOR : WARNING_RING_COLOR;

// Tier 1 (all severities): Affected path highlights (simplified thick lines)
if (!warningAffectedPaths.isEmpty()) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pulseAlpha * 0.5f));
g.setStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.setColor(baseColor);
for (final Path path : warningAffectedPaths) {
drawPathSimplified(g, path);
}
}

// Tier 1 (all severities): Pulsing rings at warning locations
if (!warningLocations.isEmpty()) {
final int ringRadius = Math.max(10, (int) (12 * getMagnification() / 4));
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pulseAlpha));
for (final PointInImage loc : warningLocations) {
final int sx = myScreenXD(loc.x / tracerPlugin.x_spacing);
final int sy = myScreenYD(loc.y / tracerPlugin.y_spacing);
// Dark outline ring
g.setColor(Color.BLACK);
g.setStroke(new BasicStroke(3f));
g.drawOval(sx - ringRadius, sy - ringRadius, ringRadius * 2, ringRadius * 2);
// Colored ring
g.setColor(baseColor);
g.setStroke(new BasicStroke(2f));
g.drawOval(sx - ringRadius, sy - ringRadius, ringRadius * 2, ringRadius * 2);
}
}

// Tier 3 (ERROR + panel not visible): Brief viewport tint
if (warningViewportTint && elapsed < TINT_DURATION_MS) {
final float tintAlpha = 1f - (float) elapsed / TINT_DURATION_MS; // fade out
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, tintAlpha));
g.setColor(VIEWPORT_TINT_COLOR);
g.fillRect(0, 0, getWidth(), getHeight());
}
g.setStroke(savedStroke);
g.setColor(savedColor);
g.setComposite(savedComposite);
}

/** Draws a path as connected line segments without delegating to Path's own drawing logic. */
private void drawPathSimplified(final Graphics2D g, final Path path) {
if (path == null || path.size() < 2) return;
final int sliceZeroIndexed = imp.getZ() - 1;
final double xSpacing = tracerPlugin.x_spacing;
final double ySpacing = tracerPlugin.y_spacing;
final double zSpacing = tracerPlugin.z_spacing;
int prevSx = Integer.MIN_VALUE, prevSy = Integer.MIN_VALUE;
for (int i = 0; i < path.size(); i++) {
final PointInImage p = path.getNodeWithoutChecks(i);
// Skip nodes far from current Z slice (tolerance: ±2 slices)
final int nodeSlice = (int) Math.round(p.z / zSpacing);
if (just_near_slices && Math.abs(nodeSlice - sliceZeroIndexed) > Math.max(eitherSide, 2))
continue;
final int sx = myScreenXD(p.x / xSpacing);
final int sy = myScreenYD(p.y / ySpacing);
if (prevSx != Integer.MIN_VALUE) {
g.drawLine(prevSx, prevSy, sx, sy);
}
prevSx = sx;
prevSy = sy;
}
}

private void drawManualRadiusCursor(final Graphics2D g) {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/sc/fiji/snt/Path.java
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,27 @@ private double getExtensionAngle(final int view) {
return Math.toDegrees(angle); // return angle in 0-360 degrees
}

/**
* Returns a (non-normalized) direction vector from the first node to the
* node at index {@code min(maxNodes, size()-1)}. This is useful for
* measuring the initial heading of a child path near a fork point.
*
* @param maxNodes the maximum node index to use as the end point
* @return a 3-element array {@code [dx, dy, dz]}, or {@code null} if the
* path has fewer than 2 nodes or the resulting vector is zero-length
*/
public double[] getInitialDirection(final int maxNodes) {
final int end = Math.min(maxNodes, size() - 1);
if (end < 1) return null;
final PointInImage p0 = getNodeWithoutChecks(0);
final PointInImage pN = getNodeWithoutChecks(end);
final double dx = pN.x - p0.x;
final double dy = pN.y - p0.y;
final double dz = pN.z - p0.z;
if (dx == 0 && dy == 0 && dz == 0) return null;
return new double[]{ dx, dy, dz };
}

/**
* Returns the 3D extension direction vector of this path using linear regression.
* The vector represents the overall direction from start to end of the path, fitted through all its nodes.
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/sc/fiji/snt/SNT.java
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,10 @@ public void finished(final SearchInterface source, final boolean success) {
return;
}
setTemporaryPath(result);
// Hook 2: Run plausibility checks on candidate segment
if (ui != null && ui.getPlausibilityMonitor().isEnabled() && currentPath != null) {
ui.getPlausibilityMonitor().onSegmentCompleted(result);
}
if (rubberBandTracing) {
// In rubber band mode: show preview, stay in PARTIAL_PATH.
// The user clicks to accept; no Y confirmation needed.
Expand Down Expand Up @@ -2464,6 +2468,10 @@ protected synchronized void finishedPath() {
}
if (pathAndFillManager.getPathFromID(currentPath.getID()) == null) {
pathAndFillManager.addPath(currentPath, true, false, false);
// Hook 3: Run holistic plausibility check on completed path
if (ui != null && ui.getPlausibilityMonitor().isEnabled()) {
ui.getPlausibilityMonitor().onPathFinalized(currentPath);
}
}
lastStartPointSet = false;
if (activateFinishedPath) selectPath(currentPath, false);
Expand Down Expand Up @@ -2646,12 +2654,19 @@ synchronized void startPath(final double world_x, final double world_y,
real_last_start_y = world_y;
real_last_start_z = world_z;
ballColor = getXYCanvas().getTemporaryPathColor();
// Clear fork context: this is a root path, not a fork
if (ui != null) ui.getPlausibilityMonitor().clearForkContext();
}
else {
real_last_start_x = joinPoint.x;
real_last_start_y = joinPoint.y;
real_last_start_z = joinPoint.z;
path.setBranchFrom(joinPoint.onPath, joinPoint);
// Hook 1: Notify plausibility monitor of fork initiation
if (ui != null && ui.getPlausibilityMonitor().isEnabled()) {
final int branchIdx = path.getBranchPointIndex();
ui.getPlausibilityMonitor().onForkInitiated(joinPoint.onPath, branchIdx);
}
ballColor = Color.GREEN;
}
// offset is irrelevant: newly created paths always have canvasOffset = (0,0,0)
Expand Down
Loading
Loading