diff --git a/weasis-core/src/main/java/org/weasis/core/api/gui/util/ActionW.java b/weasis-core/src/main/java/org/weasis/core/api/gui/util/ActionW.java index 76d216b8b..92f3dba16 100755 --- a/weasis-core/src/main/java/org/weasis/core/api/gui/util/ActionW.java +++ b/weasis-core/src/main/java/org/weasis/core/api/gui/util/ActionW.java @@ -41,12 +41,54 @@ public class ActionW { public static final BasicActionStateValue VIEW_MODE = new BasicActionStateValue(Messages.getString("ActionW.view_mode"), "viewMode", 0, 0, null); public static final SliderChangeListenerValue ZOOM = - new SliderChangeListenerValue( - Messages.getString("ActionW.zoom"), - "zoom", // NON-NLS - KeyEvent.VK_Z, - 0, - Feature.getSvgCursor("zoom.svg", Messages.getString("ActionW.zoom"), 0.5f, 0.5f)); + new SliderChangeListenerValue( + Messages.getString("ActionW.zoom"), + "zoom", // NON-NLS + KeyEvent.VK_Z, + 0, + Feature.getSvgCursor("zoom.svg", Messages.getString("ActionW.zoom"), 0.5f, 0.5f)); + public static final ToggleButtonListenerValue SLICE_ENABLE = + new ToggleButtonListenerValue( + "Slicing Enabled", + "flip", + 0, + 0, + null); // NON-NLS + public static final SliderChangeListenerValue SLICE_X = + new SliderChangeListenerValue( + "Slice X", + "slice_x", // NON-NLS + 0, + 0, + Feature.getSvgCursor("zoom.svg", "Slice", 0.5f, 0.5f)); + public static final SliderChangeListenerValue SLICE_Y = + new SliderChangeListenerValue( + "Slice Y", + "slice_y", // NON-NLS + 0, + 0, + Feature.getSvgCursor("zoom.svg", "Slice", 0.5f, 0.5f)); + public static final SliderChangeListenerValue SLICE_Z = + new SliderChangeListenerValue( + "Slice Z", + "slice_z", // NON-NLS + 0, + 0, + Feature.getSvgCursor("zoom.svg", "Slice", 0.5f, 0.5f)); + public static final SliderChangeListenerValue SLICE_X_NORM = + new SliderChangeListenerValue( + "Slice Yaw", + "slice_x_norm", // NON-NLS + 0, + 0, + Feature.getSvgCursor("zoom.svg", "Slice", 0.5f, 0.5f)); + public static final SliderChangeListenerValue SLICE_Y_NORM = + new SliderChangeListenerValue( + "Slice Pitch", + "slice_y_norm", // NON-NLS + 0, + 0, + Feature.getSvgCursor("zoom.svg", "Slice", 0.5f, 0.5f)); public static final SliderCineListenerValue SCROLL_SERIES = new SliderCineListenerValue( Messages.getString("ActionW.scroll"), diff --git a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/EventManager.java b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/EventManager.java index f844a5d26..79f97d3bf 100755 --- a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/EventManager.java +++ b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/EventManager.java @@ -116,6 +116,12 @@ private EventManager() { setAction(newLevelAction()); setAction(newRotateAction()); setAction(newZoomAction()); + setAction(newSliceEnableAction()); + setAction(newSliceXAction()); + setAction(newSliceYAction()); + setAction(newSliceZAction()); + setAction(newSliceXNormAction()); + setAction(newSliceYNormAction()); setAction(newMipTypeOption()); setAction(newMipDepthAction()); setAction(newOpacityAction()); @@ -291,6 +297,177 @@ public void mouseWheelMoved(MouseWheelEvent e) { }; } + private ToggleButtonListener newSliceEnableAction() { + return new ToggleButtonListener(ActionW.SLICE_ENABLE, false) { + @Override + public void actionPerformed(boolean selected) { + firePropertyChange( + ActionW.SYNCH.cmd(), + null, + new SynchEvent( + getSelectedViewPane(), + getActionW().cmd(), + selected)); + } + }; + } + + protected SliderChangeListener newSliceXAction() { + + return new SliderChangeListener( + ActionW.SLICE_X, + 0.0, + 3.0, + 0.5, + true, + 0.3, + 300) { + + @Override + public void stateChanged(BoundedRangeModel model) { + firePropertyChange( + ActionW.SYNCH.cmd(), + null, + new SynchEvent( + getSelectedViewPane(), + getActionW().cmd(), + toModelValue(model.getValue()), + model.getValueIsAdjusting())); + } + + @Override + public String getValueToDisplay() { + return DecFormatter.twoDecimal(getRealValue()); + } + + }; + } + + protected SliderChangeListener newSliceYAction() { + + return new SliderChangeListener( + ActionW.SLICE_Y, + 0.0, + 3.0, + 0.5, + true, + 0.3, + 300) { + + @Override + public void stateChanged(BoundedRangeModel model) { + firePropertyChange( + ActionW.SYNCH.cmd(), + null, + new SynchEvent( + getSelectedViewPane(), + getActionW().cmd(), + toModelValue(model.getValue()), + model.getValueIsAdjusting())); + } + + @Override + public String getValueToDisplay() { + return DecFormatter.twoDecimal(getRealValue()); + } + + }; + } + + protected SliderChangeListener newSliceZAction() { + + return new SliderChangeListener( + ActionW.SLICE_Z, + 0.0, + 3.0, + 0.5, + true, + 0.3, + 300) { + + @Override + public void stateChanged(BoundedRangeModel model) { + firePropertyChange( + ActionW.SYNCH.cmd(), + null, + new SynchEvent( + getSelectedViewPane(), + getActionW().cmd(), + toModelValue(model.getValue()), + model.getValueIsAdjusting())); + } + + @Override + public String getValueToDisplay() { + return DecFormatter.twoDecimal(getRealValue()); + } + + }; + } + + protected SliderChangeListener newSliceXNormAction() { + + return new SliderChangeListener( + ActionW.SLICE_X_NORM, + 0.0, + 360.0, + 0.0, + true, + 0.3, + 360) { + + @Override + public void stateChanged(BoundedRangeModel model) { + firePropertyChange( + ActionW.SYNCH.cmd(), + null, + new SynchEvent( + getSelectedViewPane(), + getActionW().cmd(), + toModelValue(model.getValue()), + model.getValueIsAdjusting())); + } + + @Override + public String getValueToDisplay() { + return DecFormatter.twoDecimal(getRealValue()); + } + + }; + } + + protected SliderChangeListener newSliceYNormAction() { + + return new SliderChangeListener( + ActionW.SLICE_Y_NORM, + 0.0, + 360.0, + 0.0, + true, + 0.3, + 360) { + + @Override + public void stateChanged(BoundedRangeModel model) { + firePropertyChange( + ActionW.SYNCH.cmd(), + null, + new SynchEvent( + getSelectedViewPane(), + getActionW().cmd(), + toModelValue(model.getValue()), + model.getValueIsAdjusting())); + } + + @Override + public String getValueToDisplay() { + return DecFormatter.twoDecimal(getRealValue()); + } + + }; + } + + @Override protected SliderChangeListener newRotateAction() { return new ArcballMouseListener() { diff --git a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/dockable/VolumeTool.java b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/dockable/VolumeTool.java index bbd206b94..7f66e54c2 100755 --- a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/dockable/VolumeTool.java +++ b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/dockable/VolumeTool.java @@ -22,9 +22,8 @@ import javax.swing.JScrollPane; import javax.swing.JToggleButton; import javax.swing.border.Border; -import org.weasis.core.api.gui.util.ActionW; -import org.weasis.core.api.gui.util.GuiUtils; -import org.weasis.core.api.gui.util.JSliderW; + +import org.weasis.core.api.gui.util.*; import org.weasis.core.api.util.ResourceUtil; import org.weasis.core.api.util.ResourceUtil.ActionIcon; import org.weasis.core.api.util.ResourceUtil.OtherIcon; @@ -39,7 +38,9 @@ public class VolumeTool extends PluginTool { - public static final String BUTTON_NAME = Messages.getString("3d.tool"); + private final Feature.SliderChangeListenerValue slider = new Feature.SliderChangeListenerValue("Slicing", "zoom", 90, 0, Feature.getSvgCursor("zoom.svg", "Slicing", 0.5F, 0.5F)); + + public static final String BUTTON_NAME = Messages.getString("3d.tool"); private final JScrollPane rootPane = new JScrollPane(); private final Border spaceY = GuiUtils.getEmptyBorder(15, 3, 0, 3); @@ -54,6 +55,8 @@ public VolumeTool(String pluginName) { private void init() { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); add(getWindowLevelPanel()); + add(getSlicePanel()); + add(getSliceNormPanel()); add(getVolumetricPanel()); add(getTransformPanel()); add(GuiUtils.boxYLastElement(3)); @@ -222,7 +225,86 @@ private JPanel getTransformPanel() { return transform; } - @Override + private JPanel getSlicePanel() { + JPanel slice = GuiUtils.getVerticalBoxLayoutPanel(); + slice.setBorder( + BorderFactory.createCompoundBorder( + spaceY, + GuiUtils.getTitledBorder("Slicing"))); + + + + EventManager.getInstance() + .getAction(ActionW.SLICE_X) + .ifPresent( + sliderItem -> { + JSliderW xSliceSlider = sliderItem.createSlider(0, true); + GuiUtils.setPreferredWidth(xSliceSlider, 100); + slice.add(xSliceSlider); + + }); + EventManager.getInstance() + .getAction(ActionW.SLICE_Y) + .ifPresent( + sliderItem -> { + JSliderW sliceSlider = sliderItem.createSlider(0, true); + GuiUtils.setPreferredWidth(sliceSlider, 100); + slice.add(sliceSlider); + + }); + EventManager.getInstance() + .getAction(ActionW.SLICE_Z) + .ifPresent( + sliderItem -> { + JSliderW sliceSlider = sliderItem.createSlider(0, true); + GuiUtils.setPreferredWidth(sliceSlider, 100); + slice.add(sliceSlider); + + }); + EventManager.getInstance() + .getAction(ActionW.SLICE_ENABLE) + .ifPresent( + toggleButton -> { + JPanel pane = GuiUtils.getFlowLayoutPanel(); + pane.add( + toggleButton.createCheckBox( + "Slicing Enabled")); + slice.add(pane); + }); + return slice; + } + private JPanel getSliceNormPanel() { + JPanel slice = GuiUtils.getVerticalBoxLayoutPanel(); + slice.setBorder( + BorderFactory.createCompoundBorder( + spaceY, + GuiUtils.getTitledBorder("Slicing Orientation"))); + + + + EventManager.getInstance() + .getAction(ActionW.SLICE_X_NORM) + .ifPresent( + sliderItem -> { + JSliderW sliceSlider = sliderItem.createSlider(0, true); + GuiUtils.setPreferredWidth(sliceSlider, 100); + slice.add(sliceSlider); + + }); + EventManager.getInstance() + .getAction(ActionW.SLICE_Y_NORM) + .ifPresent( + sliderItem -> { + JSliderW sliceSlider = sliderItem.createSlider(0, true); + GuiUtils.setPreferredWidth(sliceSlider, 100); + slice.add(sliceSlider); + + }); + return slice; + } + + + @Override protected void changeToolWindowAnchor(CLocation clocation) { // Do nothing } diff --git a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/vr/View3d.java b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/vr/View3d.java index 2b70de22d..035e4f5ee 100755 --- a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/vr/View3d.java +++ b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/java/org/weasis/dicom/viewer3d/vr/View3d.java @@ -44,6 +44,7 @@ import javax.swing.ToolTipManager; import org.dcm4che3.img.lut.PresetWindowLevel; import org.joml.Vector3f; +import org.joml.Quaternionf; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.gui.util.ActionState; @@ -135,6 +136,11 @@ public enum ViewType { private ViewType viewType; private JProgressBar progressBar; + private boolean slicingEnabled = false; + private Vector3f slicingNormal = new Vector3f(1.0f, 0.0f, 0.0f);; + private Vector3f slicingPoint = new Vector3f(0.5f, 0.5f, 0.5f); + private Vector3f slicingEulerAngles = new Vector3f(0.0f, 0.0f, 0.0f); + public View3d( ImageViewerEventManager eventManager, DicomVolTexture volTexture) { super(eventManager, volTexture, null); @@ -361,6 +367,27 @@ public void initShaders(GL4 gl4) { 1, false, camera.getViewMatrix().invert().get(Buffers.newDirectFloatBuffer(16)))); + + //---------------------------------------- + + program.allocateUniform( + gl4, "sliceEnabled", (gl, loc) -> gl.glUniform1i(loc, slicingEnabled ? 1 : 0)); + + + program.allocateUniform( + gl4, + "sliceNormal", + (gl, loc) -> gl.glUniform3fv(loc, 1, slicingNormal.get(Buffers.newDirectFloatBuffer(3))) + ); + + program.allocateUniform( + gl4, + "slicePoint", + (gl, loc) -> gl.glUniform3fv(loc, 1, slicingPoint.get(Buffers.newDirectFloatBuffer(3))) + ); + + + //---------------------------------------- program.allocateUniform( gl4, "projectionMatrix", @@ -480,6 +507,49 @@ public void initShaders(GL4 gl4) { Buffers.newDirectFloatBuffer(vertexBufferData), GL.GL_STATIC_DRAW); } + public void setSlicePlane(double y) { + setSlicePlane(this.slicingNormal, new Vector3f(0, (float) y, 0)); + } + + public void setSlicePlane(Vector3f normal, Vector3f point) { + // Update the slicing plane parameters + this.slicingNormal = normal; + this.slicingPoint = point; + updateSlicePlane(); + } + + public void updateSlicePlane() { + Vector3f normalVector = new Vector3f(1, 0, 0); + + // Convert degrees to radians for rotation + float yaw = (float) Math.toRadians(this.slicingEulerAngles.x); // Yaw (rotation around Z-axis) + float pitch = (float) Math.toRadians(this.slicingEulerAngles.y); // Pitch (rotation around Y-axis) + + this.slicingNormal = rotateVector(normalVector, yaw, pitch); + + // Make sure to update the shader with the new slicing parameters + GL4 gl4 = OpenglUtils.getGL4(); + program.use(gl4); + program.setUniforms(gl4); + + renderingLayer.fireLayerChanged(); + } + + // Method to rotate a Vector3f using yaw and pitch + private Vector3f rotateVector(Vector3f vector, float yaw, float pitch) { + // Create a quaternion for yaw (rotation around Z-axis) + Quaternionf yawRotation = new Quaternionf().fromAxisAngleRad(0, 0, 1, yaw); + + // Create a quaternion for pitch (rotation around Y-axis) + Quaternionf pitchRotation = new Quaternionf().fromAxisAngleRad(0, 1, 0, pitch); + + // Combine the yaw and pitch rotations (yaw then pitch) + yawRotation.mul(pitchRotation); + + // Rotate the vector by applying the combined quaternion rotation + return yawRotation.transform(vector); + } + private boolean isSegMode() { return volumePreset != null && "Segmentation".equals(volumePreset.getName()); // NON-NLS @@ -948,7 +1018,7 @@ private void propertyChange(final SynchEvent synch) { } else { Object zoomType = actionsInView.get(ViewCanvas.ZOOM_TYPE_CMD); actionsInView.put( - ViewCanvas.ZOOM_TYPE_CMD, value == -100.0 ? ZoomType.REAL : ZoomType.BEST_FIT); + ViewCanvas.ZOOM_TYPE_CMD, value == -100.0 ? ZoomType.REAL : ZoomType.BEST_FIT); zoom(0.0); actionsInView.put(ViewCanvas.ZOOM_TYPE_CMD, zoomType); } @@ -956,6 +1026,29 @@ private void propertyChange(final SynchEvent synch) { if (val instanceof PanPoint panPoint) { moveOrigin(panPoint); } + } else if (command.equals(ActionW.SLICE_ENABLE.cmd())) { + this.slicingEnabled = (boolean) (Boolean) val; + updateSlicePlane(); + } else if (command.equals(ActionW.SLICE_X.cmd())) { + double value = (Double) val; + this.slicingPoint.x = (float) value; + updateSlicePlane(); + } else if (command.equals(ActionW.SLICE_Y.cmd())) { + double value = (Double) val; + this.slicingPoint.y = (float) value; + updateSlicePlane(); + } else if (command.equals(ActionW.SLICE_Z.cmd())) { + double value = (Double) val; + this.slicingPoint.z = (float) value; + updateSlicePlane(); + } else if (command.equals(ActionW.SLICE_X_NORM.cmd())) { + double value = (Double) val; + this.slicingEulerAngles.x = (float) value; + updateSlicePlane(); + } else if (command.equals(ActionW.SLICE_Y_NORM.cmd())) { + double value = (Double) val; + this.slicingEulerAngles.y = (float) value; + updateSlicePlane(); } else if (command.equals(ActionW.FLIP.cmd())) { actionsInView.put(ActionW.FLIP.cmd(), val); // LangUtil.getNULLtoFalse((Boolean) val); diff --git a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelFunctions.glsl b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelFunctions.glsl index 67c65cd2f..8b5db5fa1 100755 --- a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelFunctions.glsl +++ b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelFunctions.glsl @@ -1,9 +1,38 @@ +// ************************************************************************************************* +// My functions +// ************************************************************************************************* + +// ************************************************************************************************* +// Slicing logic +// ************************************************************************************************* + +// Function to calculate signed distance from a point to the slicing plane +float getDistanceToPlane(vec3 point, vec3 planePoint, vec3 planeNormal) { + return dot(point - planePoint, planeNormal); +} + +// Function to check if a voxel is inside the slice based on distance to the plane +bool isVoxelNearSlice(vec3 voxelCoord, vec3 planePoint, vec3 planeNormal) { + float dist = getDistanceToPlane(voxelCoord, planePoint, planeNormal); + return dist >= 0; +} + + + + + // ************************************************************************************************* // Get voxel value from 3D texture // ************************************************************************************************* float getVoxelValue(vec3 coordinates) { - return texture(volTexture, coordinates).r; + // Check if the slicing is disabled or voxel is inside the slice + if (!sliceEnabled || isVoxelNearSlice(coordinates, slicePoint, sliceNormal)) { + return texture(volTexture, coordinates).r; + } else { + // Outside slice, discard fragment + return 0; + } } float getOriginalVoxelValue(float texValue) { diff --git a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelUniforms.glsl b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelUniforms.glsl index dc1e741c2..e73ce8267 100755 --- a/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelUniforms.glsl +++ b/weasis-dicom/weasis-dicom-3d/viewer3d/src/main/resources/shader/voxelUniforms.glsl @@ -1,3 +1,11 @@ +// ************************************************************************************************* +// My functions +// ************************************************************************************************* + +uniform bool sliceEnabled; +uniform vec3 sliceNormal; // Normal vector of the slicing plane +uniform vec3 slicePoint; // A point on the slicing plane + // Texture data type uniform uint textureDataType; const uint dataTypeByte = 0x00000000u;