/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Icy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Icy. If not, see <http://www.gnu.org/licenses/>. */ package plugins.kernel.roi.roi3d; import java.awt.Color; import java.awt.Graphics2D; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.HashSet; import java.util.Map.Entry; import java.util.Set; import icy.canvas.IcyCanvas; import icy.common.CollapsibleEvent; import icy.gui.inspector.RoisPanel; import icy.main.Icy; import icy.painter.VtkPainter; import icy.roi.BooleanMask2D; import icy.roi.BooleanMask3D; import icy.roi.ROI; import icy.roi.ROI2D; import icy.roi.ROI3D; import icy.roi.ROIEvent; import icy.sequence.Sequence; import icy.system.thread.ThreadUtil; import icy.type.point.Point3D; import icy.type.point.Point5D; import icy.type.rectangle.Rectangle3D; import icy.util.StringUtil; import icy.vtk.IcyVtkPanel; import icy.vtk.VtkUtil; import plugins.kernel.canvas.VtkCanvas; import plugins.kernel.roi.roi2d.ROI2DArea; import vtk.vtkActor; import vtk.vtkImageData; import vtk.vtkInformation; import vtk.vtkPolyData; import vtk.vtkPolyDataMapper; import vtk.vtkProp; /** * 3D Area ROI. * * @author Stephane */ public class ROI3DArea extends ROI3DStack<ROI2DArea> { public class ROI3DAreaPainter extends ROI3DStackPainter implements VtkPainter, Runnable { // VTK 3D objects protected vtkPolyData outline; protected vtkPolyDataMapper outlineMapper; protected vtkActor outlineActor; protected vtkInformation vtkInfo; protected vtkPolyData polyData; protected vtkPolyDataMapper polyMapper; protected vtkActor surfaceActor; // 3D internal protected boolean needRebuild; protected double scaling[]; protected WeakReference<VtkCanvas> canvas3d; public ROI3DAreaPainter() { super(); outline = null; outlineMapper = null; outlineActor = null; vtkInfo = null; polyData = null; polyMapper = null; surfaceActor = null; scaling = new double[3]; Arrays.fill(scaling, 1d); needRebuild = true; canvas3d = new WeakReference<VtkCanvas>(null); } @Override protected void finalize() throws Throwable { super.finalize(); // release allocated VTK resources if (surfaceActor != null) surfaceActor.Delete(); if (polyMapper != null) polyMapper.Delete(); if (polyData != null) { polyData.GetPointData().GetScalars().Delete(); polyData.GetPointData().Delete(); polyData.Delete(); } if (outlineActor != null) { outlineActor.SetPropertyKeys(null); outlineActor.Delete(); } if (vtkInfo != null) { vtkInfo.Remove(VtkCanvas.visibilityKey); vtkInfo.Delete(); } if (outlineMapper != null) outlineMapper.Delete(); if (outline != null) { outline.GetPointData().GetScalars().Delete(); outline.GetPointData().Delete(); outline.Delete(); } }; protected void initVtkObjects() { outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d); outlineMapper = new vtkPolyDataMapper(); outlineMapper.SetInputData(outline); outlineActor = new vtkActor(); outlineActor.SetMapper(outlineMapper); // disable picking on the outline outlineActor.SetPickable(0); // and set it to wireframe representation outlineActor.GetProperty().SetRepresentationToWireframe(); // use vtkInformations to store outline visibility state (hacky) vtkInfo = new vtkInformation(); vtkInfo.Set(VtkCanvas.visibilityKey, 0); // VtkCanvas use this to restore correctly outline visibility flag outlineActor.SetPropertyKeys(vtkInfo); polyMapper = new vtkPolyDataMapper(); surfaceActor = new vtkActor(); surfaceActor.SetMapper(polyMapper); final Color col = getColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; // set actors color outlineActor.GetProperty().SetColor(r, g, b); surfaceActor.GetProperty().SetColor(r, g, b); } /** * rebuild VTK objects (called only when VTK canvas is selected). */ protected void rebuildVtkObjects() { final VtkCanvas canvas = canvas3d.get(); // canvas was closed if (canvas == null) return; final IcyVtkPanel vtkPanel = canvas.getVtkPanel(); // canvas was closed if (vtkPanel == null) return; final Sequence seq = canvas.getSequence(); // nothing to update if (seq == null) return; // get previous polydata object final vtkPolyData previousPolyData = polyData; // get VTK binary image from ROI mask final vtkImageData imageData = VtkUtil.getBinaryImageData(ROI3DArea.this, seq.getSizeZ(), canvas.getPositionT()); // adjust spacing imageData.SetSpacing(scaling[0], scaling[1], scaling[2]); // get VTK polygon data representing the surface of the binary image polyData = VtkUtil.getSurfaceFromImage(imageData, 0.5d); // get bounds final Rectangle3D bounds = getBounds3D(); // apply scaling on bounds bounds.setX(bounds.getX() * scaling[0]); bounds.setSizeX(bounds.getSizeX() * scaling[0]); bounds.setY(bounds.getY() * scaling[1]); bounds.setSizeY(bounds.getSizeY() * scaling[1]); if (bounds.isInfiniteZ()) { bounds.setZ(0); bounds.setSizeZ(seq.getSizeZ() * scaling[2]); } else { bounds.setZ(bounds.getZ() * scaling[2]); bounds.setSizeZ(bounds.getSizeZ() * scaling[2]); } // actor can be accessed in canvas3d for rendering so we need to synchronize access vtkPanel.lock(); try { // update outline data VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas); outlineMapper.Update(); // update surface polygon data polyMapper.SetInputData(polyData); polyMapper.Update(); // update actor position surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ()); // release image data imageData.GetPointData().GetScalars().Delete(); imageData.GetPointData().Delete(); imageData.Delete(); // release previous polydata if (previousPolyData != null) { previousPolyData.GetPointData().GetScalars().Delete(); previousPolyData.GetPointData().Delete(); previousPolyData.Delete(); } } finally { vtkPanel.unlock(); } // update color and others properties updateVtkDisplayProperties(); } protected void updateVtkDisplayProperties() { if (surfaceActor == null) return; final VtkCanvas cnv = canvas3d.get(); final Color col = getDisplayColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; // final double strk = getStroke(); // final float opacity = getOpacity(); final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null; // we need to lock canvas as actor can be accessed during rendering if (vtkPanel != null) vtkPanel.lock(); try { // set actors color outlineActor.GetProperty().SetColor(r, g, b); if (isSelected()) { outlineActor.GetProperty().SetRepresentationToWireframe(); outlineActor.SetVisibility(1); vtkInfo.Set(VtkCanvas.visibilityKey, 1); } else { outlineActor.GetProperty().SetRepresentationToPoints(); outlineActor.SetVisibility(0); vtkInfo.Set(VtkCanvas.visibilityKey, 0); } surfaceActor.GetProperty().SetColor(r, g, b); // opacity here is about ROI content, global opacity is handled by Layer // surfaceActor.GetProperty().SetOpacity(opacity); setVtkObjectsColor(col); } finally { if (vtkPanel != null) vtkPanel.unlock(); } // need to repaint painterChanged(); } protected void setVtkObjectsColor(Color color) { if (outline != null) VtkUtil.setPolyDataColor(outline, color, canvas3d.get()); if (polyData != null) VtkUtil.setPolyDataColor(polyData, color, canvas3d.get()); } @Override public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseClick(e, imagePoint.toPoint2D(), canvas); else mouseClick(e, (Point2D) null, canvas); // not yet consumed... if (!e.isConsumed()) { // and process ROI stuff now if (isActiveFor(canvas)) { final int clickCount = e.getClickCount(); // double click if (clickCount == 2) { // focused ? if (isFocused()) { // show in ROI panel final RoisPanel roiPanel = Icy.getMainInterface().getRoisPanel(); if (roiPanel != null) { roiPanel.scrollTo(ROI3DArea.this); // consume event e.consume(); } } } } } } @Override public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (isActiveFor(canvas)) { if (canvas instanceof VtkCanvas) { // 3D canvas final VtkCanvas cnv = (VtkCanvas) canvas; // update reference if needed if (canvas3d.get() != cnv) canvas3d = new WeakReference<VtkCanvas>(cnv); // FIXME : need a better implementation final double[] s = cnv.getVolumeScale(); // scaling changed ? if (!Arrays.equals(scaling, s)) { // update scaling scaling = s; // need rebuild needRebuild = true; } // need to rebuild 3D data structures ? if (needRebuild) { // initialize VTK objects if not yet done if (surfaceActor == null) initVtkObjects(); // request rebuild 3D objects ThreadUtil.runSingle(this); needRebuild = false; } } else super.paint(g, sequence, canvas); } } @Override protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas) { // specific VTK canvas processing if (canvas instanceof VtkCanvas) { // mouse is over the ROI actor ? --> focus the ROI final boolean focused = (surfaceActor != null) && (surfaceActor == ((VtkCanvas) canvas).getPickedObject()); setFocused(focused); return focused; } return super.updateFocus(e, imagePoint, canvas); } @Override public vtkProp[] getProps() { // initialize VTK objects if not yet done if (surfaceActor == null) initVtkObjects(); return new vtkActor[] {surfaceActor, outlineActor}; } @Override public void run() { rebuildVtkObjects(); } } public ROI3DArea() { super(ROI2DArea.class); } public ROI3DArea(Point3D pt) { this(); addBrush(pt.toPoint2D(), (int) pt.getZ()); } public ROI3DArea(Point5D pt) { this(pt.toPoint3D()); } /** * Create a 3D Area ROI type from the specified {@link BooleanMask3D}. */ public ROI3DArea(BooleanMask3D mask) { this(); setAsBooleanMask(mask); } /** * Create a copy of the specified 3D Area ROI. */ public ROI3DArea(ROI3DArea area) { this(); // copy the source 3D area ROI for (Entry<Integer, ROI2DArea> entry : area.slices.entrySet()) slices.put(entry.getKey(), new ROI2DArea(entry.getValue())); roiChanged(true); } /** * Create a 3D Area ROI type from the specified {@link BooleanMask3D}. */ public ROI3DArea(BooleanMask2D mask2d, int zMin, int zMax) { this(); if (zMax < zMin) throw new IllegalArgumentException("ROI3DArea: cannot create the ROI (zMax < zMin)."); beginUpdate(); try { for (int z = zMin; z <= zMax; z++) setSlice(z, new ROI2DArea(mask2d)); } finally { endUpdate(); } } @Override public String getDefaultName() { return "Area3D"; } @Override protected ROIPainter createPainter() { return new ROI3DAreaPainter(); } /** * Adds the specified point to this ROI */ public void addPoint(int x, int y, int z) { setPoint(x, y, z, true); } /** * Remove a point from the mask.<br> * Don't forget to call optimizeBounds() after consecutive remove operation * to refresh the mask bounds. */ public void removePoint(int x, int y, int z) { setPoint(x, y, z, false); } /** * Set the value for the specified point in the mask. * Don't forget to call optimizeBounds() after consecutive remove point operation * to refresh the mask bounds. */ public void setPoint(int x, int y, int z, boolean value) { final ROI2DArea slice = getSlice(z, value); if (slice != null) slice.setPoint(x, y, value); } /** * Add brush point at specified position and for specified Z slice. */ public void addBrush(Point2D pos, int z) { getSlice(z, true).addBrush(pos); } /** * Remove brush point from the mask at specified position and for specified Z slice.<br> * Don't forget to call optimizeBounds() after consecutive remove operation * to refresh the mask bounds. */ public void removeBrush(Point2D pos, int z) { final ROI2DArea slice = getSlice(z, false); if (slice != null) slice.removeBrush(pos); } /** * Add the specified {@link BooleanMask3D} content to this ROI3DArea */ public void add(BooleanMask3D mask) { beginUpdate(); try { for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet()) add(entry.getKey().intValue(), entry.getValue()); } finally { endUpdate(); } } /** * Add the specified BooleanMask2D with the existing slice at given Z position.<br> * If there is no slice at this Z position then the method is equivalent to {@link #setSlice(int, ROI2DArea)} with * <code>new ROI2DArea(maskSlice)</code> * * @param z * the position where the slice must be added * @param maskSlice * the 2D boolean mask to merge */ public void add(int z, BooleanMask2D maskSlice) { if (maskSlice == null) return; final ROI2DArea currentSlice = getSlice(z); if (currentSlice != null) // merge slices currentSlice.add(maskSlice); else // add new slice setSlice(z, new ROI2DArea(maskSlice)); } /** * Exclusively add the specified {@link BooleanMask3D} content to this ROI3DArea */ public void exclusiveAdd(BooleanMask3D mask) { beginUpdate(); try { for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet()) exclusiveAdd(entry.getKey().intValue(), entry.getValue()); } finally { endUpdate(); } } /** * Exclusively add the specified BooleanMask2D with the existing slice at given Z position.<br> * If there is no slice at this Z position then the method is equivalent to {@link #setSlice(int, ROI2DArea)} with * <code>new ROI2DArea(maskSlice)</code> * * @param z * the position where the slice must be exclusively added * @param maskSlice * the 2D boolean mask to merge */ public void exclusiveAdd(int z, BooleanMask2D maskSlice) { if (maskSlice == null) return; final ROI2DArea currentSlice = getSlice(z); // merge both slice if (currentSlice != null) { // process exclusive add currentSlice.exclusiveAdd(maskSlice); // remove it if empty if (currentSlice.isEmpty()) removeSlice(z); } // add new slice else setSlice(z, new ROI2DArea(maskSlice)); } /** * Intersect the specified {@link BooleanMask3D} content with this ROI3DArea */ public void intersect(BooleanMask3D mask) { beginUpdate(); try { final Set<Integer> keys = mask.mask.keySet(); final Set<Integer> toRemove = new HashSet<Integer>(); // remove slices which are not contained for (Integer key : slices.keySet()) if (!keys.contains(key)) toRemove.add(key); // do remove first for (Integer key : toRemove) removeSlice(key.intValue()); // then process intersection for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet()) intersect(entry.getKey().intValue(), entry.getValue()); } finally { endUpdate(); } } /** * Intersect the specified BooleanMask2D with the existing slice at given Z position. * * @param z * the position where the slice must be set * @param maskSlice * the 2D boolean mask to merge */ public void intersect(int z, BooleanMask2D maskSlice) { // better to throw an exception here than removing slice if (maskSlice == null) throw new IllegalArgumentException("Cannot intersect an empty slice in a 3D ROI"); final ROI2DArea currentSlice = getSlice(z); if (currentSlice != null) { // build ROI from mask final ROI2DArea roi = new ROI2DArea(maskSlice); // set same position as slice roi.setT(currentSlice.getT()); roi.setZ(currentSlice.getZ()); roi.setC(currentSlice.getC()); // compute intersection currentSlice.intersect(roi, false); // remove it if empty if (currentSlice.isEmpty()) removeSlice(z); } } /** * Subtract the specified {@link BooleanMask3D} from this ROI3DArea */ public void subtract(BooleanMask3D mask) { beginUpdate(); try { for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet()) subtract(entry.getKey().intValue(), entry.getValue()); } finally { endUpdate(); } } /** * Subtract the specified BooleanMask2D from the existing slice at given Z position.<br> * * @param z * the position where the slice must be subtracted * @param maskSlice * the 2D boolean mask to subtract */ public void subtract(int z, BooleanMask2D maskSlice) { if (maskSlice == null) return; final ROI2DArea currentSlice = getSlice(z); // merge both slice if (currentSlice != null) { // process exclusive add currentSlice.subtract(maskSlice); // remove it if empty if (currentSlice.isEmpty()) removeSlice(z); } } @Override public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI3D) { final ROI3D roi3d = (ROI3D) roi; // only if on same position if ((getT() == roi3d.getT()) && (getC() == roi3d.getC())) { if (roi3d instanceof ROI3DArea) add((ROI3DArea) roi3d); else add(roi3d.getBooleanMask(true)); return this; } } return super.add(roi, allowCreate); } @Override public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI3D) { final ROI3D roi3d = (ROI3D) roi; // only if on same position if ((getT() == roi3d.getT()) && (getC() == roi3d.getC())) { if (roi3d instanceof ROI3DArea) exclusiveAdd((ROI3DArea) roi3d); else exclusiveAdd(roi3d.getBooleanMask(true)); return this; } } return super.exclusiveAdd(roi, allowCreate); } @Override public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI3D) { final ROI3D roi3d = (ROI3D) roi; // only if on same position if ((getT() == roi3d.getT()) && (getC() == roi3d.getC())) { if (roi3d instanceof ROI3DArea) intersect((ROI3DArea) roi3d); else intersect(roi3d.getBooleanMask(true)); return this; } } return super.intersect(roi, allowCreate); } @Override public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI3D) { final ROI3D roi3d = (ROI3D) roi; // only if on same position if ((getT() == roi3d.getT()) && (getC() == roi3d.getC())) { if (roi3d instanceof ROI3DArea) subtract((ROI3DArea) roi3d); else subtract(roi3d.getBooleanMask(true)); return this; } } return super.subtract(roi, allowCreate); } /** * Sets the BooleanMask2D slice at given Z position to this 3D ROI * * @param z * the position where the slice must be set * @param maskSlice * the BooleanMask2D to set */ public void setSlice(int z, BooleanMask2D maskSlice) { // empty mask --> just remove previous if (maskSlice == null) { removeSlice(z); return; } setSlice(z, new ROI2DArea(maskSlice)); } /** * @deprecated Use one of these methods instead :<br> * {@link #setSlice(int, ROI2DArea)}, {@link #setSlice(int, BooleanMask2D)}, * {@link #add(int, ROI2DArea)} or {@link #add(BooleanMask3D)} */ @Deprecated public void setSlice(int z, ROI2D roiSlice, boolean merge) { if (roiSlice == null) throw new IllegalArgumentException("Cannot add an empty slice in a 3D ROI"); final ROI2DArea currentSlice = getSlice(z); final ROI newSlice; // merge both slice if ((currentSlice != null) && merge) { // we need to modify the Z, T and C position so we do the merge correctly roiSlice.setZ(z); roiSlice.setT(getT()); roiSlice.setC(getC()); // do ROI union newSlice = currentSlice.getUnion(roiSlice); } else newSlice = roiSlice; if (newSlice instanceof ROI2DArea) setSlice(z, (ROI2DArea) newSlice); else if (newSlice instanceof ROI2D) setSlice(z, new ROI2DArea(((ROI2D) newSlice).getBooleanMask(true))); else throw new IllegalArgumentException( "Can't add the result of the merge operation on 2D slice " + z + ": " + newSlice.getClassName()); } // /** // * Merge the specified ROI with the existing slice at given Z position.<br> // * If there is no slice at this Z position then the method is equivalent to {@link #setSlice(int, ROI2DArea)} // * // * @param z // * the position where the slice must be set // * @param roiSlice // * the 2D ROI to merge // */ // public void addROI2DSlice(int z, ROI2D roiSlice) // { // if (roiSlice == null) // return; // // final ROI2DArea currentSlice = getSlice(z); // // // merge both slice // if (currentSlice != null) // { // // we need to modify the Z, T and C position so we do the merge correctly // roiSlice.setZ(z); // roiSlice.setT(getT()); // roiSlice.setC(getC()); // // do ROI union // currentSlice.add(roiSlice, true); // } // else // setSlice(z, new ROI2DArea(roiSlice.getBooleanMask(true))); // } /** * Returns true if the ROI is empty (the mask does not contains any point). */ @Override public boolean isEmpty() { for (ROI2DArea area : slices.values()) if (!area.isEmpty()) return false; return true; } /** * @deprecated Use {@link #getBooleanMask(boolean)} and {@link BooleanMask3D#getContourPoints()} instead. */ @Deprecated public Point3D[] getEdgePoints() { return getBooleanMask(true).getContourPoints(); } /** * @deprecated Use {@link #getBooleanMask(boolean)} and {@link BooleanMask3D#getPoints()} instead. */ @Deprecated public Point3D[] getPoints() { return getBooleanMask(true).getPoints(); } /** * @deprecated Use {@link #translate(double, double, double)} instead. */ @Deprecated public void translate(double dx, double dy) { beginUpdate(); try { for (ROI2DArea slice : slices.values()) slice.translate(dx, dy); } finally { endUpdate(); } } @Override public boolean isOverEdge(IcyCanvas canvas, double x, double y, double z) { final ROI2DArea slice = getSlice((int) z); if (slice != null) return slice.isOverEdge(canvas, x, y); return false; } /** * Set all 2D slices ROI to same position.<br> */ public void setPosition2D(Point2D newPosition) { beginUpdate(); try { for (ROI2DArea slice : slices.values()) slice.setPosition2D(newPosition); } finally { endUpdate(); } } /** * Set the mask from a BooleanMask3D object.<br> * If specified mask is <i>null</i> then ROI is cleared. */ public void setAsBooleanMask(BooleanMask3D mask) { // mask empty ? --> just clear the ROI if ((mask == null) || mask.isEmpty()) clear(); else { final Rectangle3D.Integer bounds3d = mask.bounds; final int startZ = bounds3d.z; final int sizeZ = bounds3d.sizeZ; final BooleanMask2D masks2d[] = new BooleanMask2D[sizeZ]; for (int z = 0; z < sizeZ; z++) masks2d[z] = mask.getMask2D(startZ + z); setAsBooleanMask(bounds3d, masks2d); } } /** * Set the 3D mask from a 2D boolean mask array * * @param rect * the 3D region defined by 2D boolean mask array * @param mask * the 3D mask data (array length should be equals to rect.sizeZ) */ public void setAsBooleanMask(Rectangle3D.Integer rect, BooleanMask2D[] mask) { if (rect.isInfiniteZ()) throw new IllegalArgumentException("Cannot set infinite Z dimension on the 3D Area ROI."); beginUpdate(); try { clear(); for (int z = 0; z < rect.sizeZ; z++) setSlice(z + rect.z, new ROI2DArea(mask[z])); } finally { endUpdate(); } } /** * Optimize the bounds size to the minimum surface which still include all mask.<br> * You should call it after consecutive remove operations if you directly addressed mask data. */ public void optimizeBounds() { final Rectangle3D.Integer bounds = getBounds(); beginUpdate(); try { for (int z = bounds.z; z < bounds.z + bounds.sizeZ; z++) { final ROI2DArea roi = getSlice(z); if (roi != null) { if (roi.isEmpty()) removeSlice(z); else { if (roi.optimizeBounds()) roi.roiChanged(true); } } } } finally { endUpdate(); } } /** * roi changed */ @Override public void onChanged(CollapsibleEvent object) { final ROIEvent event = (ROIEvent) object; // do here global process on ROI change switch (event.getType()) { case ROI_CHANGED: // the painter need to be rebuild ((ROI3DAreaPainter) painter).needRebuild = true; break; case FOCUS_CHANGED: case SELECTION_CHANGED: ((ROI3DAreaPainter) getOverlay()).updateVtkDisplayProperties(); break; case PROPERTY_CHANGED: final String property = event.getPropertyName(); if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR) || StringUtil.equals(property, PROPERTY_OPACITY)) ((ROI3DAreaPainter) getOverlay()).updateVtkDisplayProperties(); break; default: break; } super.onChanged(object); } }