Skip to content
Draft
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
179 changes: 119 additions & 60 deletions agave_app/AppearanceSettingsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ QAppearanceSettingsWidget::QAppearanceSettingsWidget(QWidget* pParent,

Section* sectionCP = createClipPlaneSection(pToggleRotateAction, pToggleTranslateAction);
m_MainLayout.addRow(sectionCP);
for (int pi = 1; pi < NUM_CLIP_PLANES; ++pi) {
m_MainLayout.addRow(m_clipPlaneGui[pi].section);
}

QFrame* lineB = new QFrame();
lineB->setFrameShape(QFrame::HLine);
Expand Down Expand Up @@ -354,79 +357,89 @@ QAppearanceSettingsWidget::QAppearanceSettingsWidget(QWidget* pParent,
Section*
QAppearanceSettingsWidget::createClipPlaneSection(QAction* pToggleRotateAction, QAction* pToggleTranslateAction)
{
Section::CheckBoxInfo checkBoxInfo = { this->m_scene ? this->m_scene->m_clipPlane->m_enabled : false,
"Enable/disable clip plane",
"Enable/disable clip plane" };
m_clipPlaneSection = new Section("Clip Plane", 0, &checkBoxInfo);
// section checkbox turns clip plane on or off
QObject::connect(m_clipPlaneSection, &Section::checked, [this](bool is_checked) {
if (this->m_scene && this->m_scene->m_clipPlane) {
this->m_scene->m_clipPlane->m_enabled = is_checked;
m_qrendersettings->renderSettings()->m_DirtyFlags.SetFlag(RoiDirty);
}
});
// Create all clip plane sections
for (int pi = 0; pi < NUM_CLIP_PLANES; ++pi) {
auto& gui = m_clipPlaneGui[pi];
QString title = (pi == 0) ? "Clip Plane" : QString("Clip Plane %1").arg(pi + 1);

Section::CheckBoxInfo checkBoxInfo = { this->m_scene ? this->m_scene->m_clipPlanes[pi]->m_enabled : false,
"Enable/disable clip plane",
"Enable/disable clip plane" };
gui.section = new Section(title, 0, &checkBoxInfo);

QObject::connect(gui.section, &Section::checked, [this, pi](bool is_checked) {
if (this->m_scene && this->m_scene->m_clipPlanes[pi]) {
this->m_scene->m_clipPlanes[pi]->m_enabled = is_checked;
m_qrendersettings->renderSettings()->m_DirtyFlags.SetFlag(RoiDirty);
}
});

auto* sectionLayout = Controls::createAgaveFormLayout();
gui.sectionLayout = Controls::createAgaveFormLayout();
auto* sectionLayout = gui.sectionLayout;

auto btnLayout = new QHBoxLayout();
auto btnLayout = new QHBoxLayout();

m_clipPlaneRotateButton = new QPushButton("Rotate");
m_clipPlaneRotateButton->setStatusTip(tr("Show interactive controls in viewport for clip plane rotation angle"));
m_clipPlaneRotateButton->setToolTip(tr("Show interactive controls in viewport for clip plane rotation angle"));
btnLayout->addWidget(m_clipPlaneRotateButton);
m_transformMode->registerButton(m_clipPlaneRotateButton, pToggleRotateAction, [this]() -> SceneObject* {
return this->m_scene ? this->m_scene->m_clipPlane.get() : nullptr;
});
gui.rotateButton = new QPushButton("Rotate");
gui.rotateButton->setStatusTip(tr("Show interactive controls in viewport for clip plane rotation angle"));
gui.rotateButton->setToolTip(tr("Show interactive controls in viewport for clip plane rotation angle"));
btnLayout->addWidget(gui.rotateButton);
m_transformMode->registerButton(gui.rotateButton, pToggleRotateAction, [this, pi]() -> SceneObject* {
return this->m_scene ? this->m_scene->m_clipPlanes[pi].get() : nullptr;
});

m_clipPlaneTranslateButton = new QPushButton("Translate");
m_clipPlaneTranslateButton->setStatusTip(tr("Show interactive controls in viewport for clip plane translation"));
m_clipPlaneTranslateButton->setToolTip(tr("Show interactive controls in viewport for clip plane translation"));
btnLayout->addWidget(m_clipPlaneTranslateButton);
m_transformMode->registerButton(m_clipPlaneTranslateButton, pToggleTranslateAction, [this]() -> SceneObject* {
return this->m_scene ? this->m_scene->m_clipPlane.get() : nullptr;
});
gui.translateButton = new QPushButton("Translate");
gui.translateButton->setStatusTip(tr("Show interactive controls in viewport for clip plane translation"));
gui.translateButton->setToolTip(tr("Show interactive controls in viewport for clip plane translation"));
btnLayout->addWidget(gui.translateButton);
m_transformMode->registerButton(gui.translateButton, pToggleTranslateAction, [this, pi]() -> SceneObject* {
return this->m_scene ? this->m_scene->m_clipPlanes[pi].get() : nullptr;
});

sectionLayout->addLayout(btnLayout, sectionLayout->rowCount(), 0, 1, 2);
sectionLayout->addLayout(btnLayout, sectionLayout->rowCount(), 0, 1, 2);

m_hideUserClipPlane = new QCheckBox();
m_hideUserClipPlane->setChecked(false);
m_hideUserClipPlane->setStatusTip(tr("Hide clip plane grid in viewport"));
m_hideUserClipPlane->setToolTip(tr("Hide clip plane grid in viewport"));
QObject::connect(
m_hideUserClipPlane, &QCheckBox::clicked, [this, pToggleRotateAction, pToggleTranslateAction](bool toggled) {
gui.hideCheckBox = new QCheckBox();
gui.hideCheckBox->setChecked(false);
gui.hideCheckBox->setStatusTip(tr("Hide clip plane grid in viewport"));
gui.hideCheckBox->setToolTip(tr("Hide clip plane grid in viewport"));
QObject::connect(gui.hideCheckBox, &QCheckBox::clicked, [this, pi](bool toggled) {
if (!this->m_scene) {
return;
}

ScenePlane* p = this->m_scene->m_clipPlane.get();
ScenePlane* p = this->m_scene->m_clipPlanes[pi].get();
p->setVisible(!toggled);
});
sectionLayout->addRow("Hide", gui.hideCheckBox);

sectionLayout->addRow("Hide", m_hideUserClipPlane);
gui.resetButton = new QPushButton("Reset");
gui.resetButton->setStatusTip(tr("Reset clip plane"));
gui.resetButton->setToolTip(tr("Reset clip plane"));
QObject::connect(gui.resetButton, &QPushButton::clicked, [this, pi]() {
if (!this->m_scene || !this->m_scene->m_clipPlanes[pi]) {
return;
}
glm::vec3 c = this->m_scene->m_boundingBox.GetCenter();
this->m_scene->m_clipPlanes[pi]->resetTo(c);
m_qrendersettings->renderSettings()->m_DirtyFlags.SetFlag(RoiDirty);
if (this->m_scene->m_selection == this->m_scene->m_clipPlanes[pi].get()) {
emit this->m_qrendersettings->Selected(this->m_scene->m_clipPlanes[pi].get());
}
});
auto* resetbtnLayout = new QHBoxLayout();
resetbtnLayout->addWidget(new QWidget());
resetbtnLayout->addWidget(gui.resetButton);
sectionLayout->addLayout(resetbtnLayout, sectionLayout->rowCount(), 0, 1, 2);

// Add the Reset button
m_clipPlaneResetButton = new QPushButton("Reset");
m_clipPlaneResetButton->setStatusTip(tr("Reset clip plane to 0,0,0,0"));
m_clipPlaneResetButton->setToolTip(tr("Reset clip plane to 0,0,0,0"));
QObject::connect(m_clipPlaneResetButton, &QPushButton::clicked, [this]() {
if (!this->m_scene || !this->m_scene->m_clipPlane) {
return;
}
glm::vec3 c = this->m_scene->m_boundingBox.GetCenter();
this->m_scene->m_clipPlane->resetTo(c);
m_qrendersettings->renderSettings()->m_DirtyFlags.SetFlag(RoiDirty);
// re-select to cause Origins.update to reset the translate or rotate tools
if (this->m_scene->m_selection == this->m_scene->m_clipPlane.get()) {
emit this->m_qrendersettings->Selected(this->m_scene->m_clipPlane.get());
}
});
// Channel assignment checkboxes (populated in initClipPlaneControls when image loads)
gui.section->setContentLayout(*sectionLayout);
}

auto* resetbtnLayout = new QHBoxLayout();
resetbtnLayout->addWidget(new QWidget());
resetbtnLayout->addWidget(m_clipPlaneResetButton);
sectionLayout->addLayout(resetbtnLayout, sectionLayout->rowCount(), 0, 1, 2);
// Backward compat aliases
m_clipPlaneSection = m_clipPlaneGui[0].section;
m_hideUserClipPlane = m_clipPlaneGui[0].hideCheckBox;
m_clipPlaneRotateButton = m_clipPlaneGui[0].rotateButton;
m_clipPlaneTranslateButton = m_clipPlaneGui[0].translateButton;
m_clipPlaneResetButton = m_clipPlaneGui[0].resetButton;

m_clipPlaneSection->setContentLayout(*sectionLayout);
return m_clipPlaneSection;
}

Expand Down Expand Up @@ -1058,8 +1071,54 @@ normalizeColorForGui(const glm::vec3& incolor, QColor& outcolor, float& outinten
void
QAppearanceSettingsWidget::initClipPlaneControls(Scene* scene)
{
const ScenePlane* clipPlane = scene->m_clipPlane.get();
m_clipPlaneSection->setChecked(clipPlane->m_enabled);
for (int pi = 0; pi < NUM_CLIP_PLANES; ++pi) {
auto& gui = m_clipPlaneGui[pi];
const ScenePlane* clipPlane = scene->m_clipPlanes[pi].get();
gui.section->setChecked(clipPlane->m_enabled);

// Clear and rebuild channel checkboxes
for (auto* cb : gui.channelCheckBoxes) {
// Remove from layout
if (gui.sectionLayout) {
gui.sectionLayout->removeWidget(cb);
}
delete cb;
}
gui.channelCheckBoxes.clear();

if (scene->m_volume && gui.sectionLayout) {
auto* formLayout = gui.sectionLayout;
int nch = (int)scene->m_volume->sizeC();
for (int ch = 0; ch < nch; ++ch) {
auto* cb = new QCheckBox();
cb->setChecked(scene->m_material.m_clipPlaneGroup[ch] == pi);
cb->setToolTip(QString("Assign channel %1 to this clip plane").arg(ch));
gui.channelCheckBoxes.push_back(cb);
formLayout->addRow(QString("Ch %1").arg(ch), cb);

QObject::connect(cb, &QCheckBox::clicked, [this, pi, ch](bool checked) {
if (!this->m_scene) {
return;
}
if (checked) {
this->m_scene->m_material.m_clipPlaneGroup[ch] = pi;
// Uncheck this channel in other clip plane sections
for (int other = 0; other < NUM_CLIP_PLANES; ++other) {
if (other != pi && ch < (int)m_clipPlaneGui[other].channelCheckBoxes.size()) {
m_clipPlaneGui[other].channelCheckBoxes[ch]->blockSignals(true);
m_clipPlaneGui[other].channelCheckBoxes[ch]->setChecked(false);
m_clipPlaneGui[other].channelCheckBoxes[ch]->blockSignals(false);
}
}
} else {
// Unchecking: set to -1 (no clip plane)
this->m_scene->m_material.m_clipPlaneGroup[ch] = -1;
}
m_qrendersettings->renderSettings()->m_DirtyFlags.SetFlag(RenderParamsDirty);
});
}
}
}
}

void
Expand Down
13 changes: 13 additions & 0 deletions agave_app/AppearanceSettingsWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ public slots:
QPushButton* m_clipPlaneTranslateButton;
QPushButton* m_clipPlaneResetButton;

struct ClipPlaneGui
{
Section* section = nullptr;
QCheckBox* hideCheckBox = nullptr;
QPushButton* rotateButton = nullptr;
QPushButton* translateButton = nullptr;
QPushButton* resetButton = nullptr;
class AgaveFormLayout* sectionLayout = nullptr;
std::vector<QCheckBox*> channelCheckBoxes;
};
static constexpr int NUM_CLIP_PLANES = 4;
ClipPlaneGui m_clipPlaneGui[NUM_CLIP_PLANES];

Section* m_scaleSection;
QDoubleSpinner* m_xscaleSpinner;
QCheckBox* m_xFlipCheckBox;
Expand Down
23 changes: 20 additions & 3 deletions agave_app/Section.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Section::Section(const QString& title, const int animationDuration, const CheckBoxInfo* checkBoxInfo, QWidget* parent)
: QWidget(parent)
, m_animationDuration(animationDuration)
, m_collapsedHeight(0)
, m_checkBox(nullptr)
{
m_toggleButton = new QToolButton(this);
Expand Down Expand Up @@ -63,6 +64,22 @@ Section::Section(const QString& title, const int animationDuration, const CheckB

QObject::connect(m_toggleButton, &QToolButton::clicked, [this](const bool checked) {
m_toggleButton->setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow);

// Recalculate animation targets from the current content layout size,
// so dynamically added widgets (e.g. channel checkboxes) are accounted for.
if (m_collapsedHeight > 0 && m_contentArea->layout()) {
auto contentHeight = m_contentArea->layout()->sizeHint().height();
for (int i = 0; i < m_toggleAnimation->animationCount() - 1; ++i) {
auto* a = static_cast<QPropertyAnimation*>(m_toggleAnimation->animationAt(i));
a->setStartValue(m_collapsedHeight);
a->setEndValue(m_collapsedHeight + contentHeight);
}
auto* ca =
static_cast<QPropertyAnimation*>(m_toggleAnimation->animationAt(m_toggleAnimation->animationCount() - 1));
ca->setStartValue(0);
ca->setEndValue(contentHeight);
}

m_toggleAnimation->setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward);
m_toggleAnimation->start();
});
Expand All @@ -88,14 +105,14 @@ Section::setContentLayout(QLayout& contentLayout)
{
delete m_contentArea->layout();
m_contentArea->setLayout(&contentLayout);
const auto collapsedHeight = sizeHint().height() - m_contentArea->maximumHeight();
m_collapsedHeight = sizeHint().height() - m_contentArea->maximumHeight();
auto contentHeight = contentLayout.sizeHint().height();

for (int i = 0; i < m_toggleAnimation->animationCount() - 1; ++i) {
QPropertyAnimation* SectionAnimation = static_cast<QPropertyAnimation*>(m_toggleAnimation->animationAt(i));
SectionAnimation->setDuration(m_animationDuration);
SectionAnimation->setStartValue(collapsedHeight);
SectionAnimation->setEndValue(collapsedHeight + contentHeight);
SectionAnimation->setStartValue(m_collapsedHeight);
SectionAnimation->setEndValue(m_collapsedHeight + contentHeight);
}

QPropertyAnimation* contentAnimation =
Expand Down
1 change: 1 addition & 0 deletions agave_app/Section.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Section : public QWidget
QParallelAnimationGroup* m_toggleAnimation;
QScrollArea* m_contentArea;
int m_animationDuration;
int m_collapsedHeight;

QCheckBox* m_checkBox;

Expand Down
22 changes: 15 additions & 7 deletions agave_app/Serialize.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,17 @@ struct ClipPlane
Transform transform;
std::array<float, 4> clipPlane = { 0, 0, 0, 0 };
bool enabled = false;
// Per-channel clip plane group: maps channel index to this clip plane.
// Channels listed here are assigned to this clip plane.
std::vector<int32_t> channelGroups;

bool operator==(const ClipPlane& other) const
{
return clipPlane == other.clipPlane && transform == other.transform && enabled == other.enabled;
return clipPlane == other.clipPlane && transform == other.transform && enabled == other.enabled &&
channelGroups == other.channelGroups;
}

NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ClipPlane, clipPlane, transform, enabled)
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ClipPlane, clipPlane, transform, enabled, channelGroups)
};

struct ViewerState
Expand All @@ -116,7 +120,10 @@ struct ViewerState

// [[xm, xM], [ym, yM], [zm, zM]]
std::array<std::array<float, 2>, 3> clipRegion = { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f };
// Legacy single clip plane (for backward compat reading old files)
ClipPlane clipPlane;
// New: array of up to 4 clip planes
std::vector<ClipPlane> clipPlanes;

std::array<float, 3> scale = { 1, 1, 1 }; // m_scaleX, m_scaleY, m_scaleZ
std::array<int, 3> flipAxis = { 1, 1, 1 }; // 1 is unflipped, -1 is flipped
Expand Down Expand Up @@ -144,11 +151,11 @@ struct ViewerState
{
return datasets == other.datasets && version == other.version && pathTracer == other.pathTracer &&
timeline == other.timeline && clipRegion == other.clipRegion && clipPlane == other.clipPlane &&
scale == other.scale && flipAxis == other.flipAxis && camera == other.camera &&
backgroundColor == other.backgroundColor && boundingBoxColor == other.boundingBoxColor &&
showBoundingBox == other.showBoundingBox && showScaleBar == other.showScaleBar &&
channels == other.channels && density == other.density && lights == other.lights &&
capture == other.capture && interpolate == other.interpolate;
clipPlanes == other.clipPlanes && scale == other.scale && flipAxis == other.flipAxis &&
camera == other.camera && backgroundColor == other.backgroundColor &&
boundingBoxColor == other.boundingBoxColor && showBoundingBox == other.showBoundingBox &&
showScaleBar == other.showScaleBar && channels == other.channels && density == other.density &&
lights == other.lights && capture == other.capture && interpolate == other.interpolate;
}
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ViewerState,
datasets,
Expand All @@ -158,6 +165,7 @@ struct ViewerState
timeline,
clipRegion,
clipPlane,
clipPlanes,
scale,
flipAxis,
camera,
Expand Down
26 changes: 26 additions & 0 deletions agave_app/ViewerState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,32 @@ stateToPythonScript(const Serialize::ViewerState& s)
} else {
ss << obj << SetClipPlaneCommand({ 0, 0, 0, 0 }).toPythonString() << std::endl;
}

// Emit additional clip planes (if clipPlanes array is present)
for (int32_t pi = 0; pi < (int32_t)s.clipPlanes.size() && pi < (int32_t)MAX_CLIP_PLANES; ++pi) {
const auto& cp = s.clipPlanes[pi];
if (cp.enabled) {
Plane p;
p.normal.x = cp.clipPlane[0];
p.normal.y = cp.clipPlane[1];
p.normal.z = cp.clipPlane[2];
p.d = cp.clipPlane[3];
Transform3d tr;
tr.m_center = { cp.transform.translation[0], cp.transform.translation[1], cp.transform.translation[2] };
tr.m_rotation = {
cp.transform.rotation[3], cp.transform.rotation[0], cp.transform.rotation[1], cp.transform.rotation[2]
};
p = p.transform(tr);
ss << obj << SetClipPlaneIndexCommand({ pi, p.normal.x, p.normal.y, p.normal.z, p.d }).toPythonString()
<< std::endl;
}
ss << obj << EnableClipPlaneCommand({ pi, cp.enabled ? 1 : 0 }).toPythonString() << std::endl;

// Emit channel group assignments from this clip plane
for (int32_t ch : cp.channelGroups) {
ss << obj << SetChannelClipPlaneGroupCommand({ ch, pi }).toPythonString() << std::endl;
}
}
ss << obj << SetCameraPosCommand({ s.camera.eye[0], s.camera.eye[1], s.camera.eye[2] }).toPythonString() << std::endl;
ss << obj << SetCameraTargetCommand({ s.camera.target[0], s.camera.target[1], s.camera.target[2] }).toPythonString()
<< std::endl;
Expand Down
Loading