g4display
Loading...
Searching...
No Matches
g4displayview.cc
Go to the documentation of this file.
1#include "g4displayview.h"
2#include "g4display_options.h"
3#include "gutilities.h"
4
5#include <QPainter>
6
7using namespace g4display;
8
9// c++
10#include <algorithm>
11#include <iostream>
12#include <sstream>
13#include <string>
14using namespace std;
15using namespace gutilities;
16
17// c++ math
18#include <cmath>
19
20// geant4
21#include "G4UImanager.hh"
22#include "G4VisManager.hh"
23#include "G4VViewer.hh"
24
25// Implementation note:
26// Detailed Doxygen documentation for public behavior and slots is maintained in g4displayview.h (see rule 7).
27namespace {
28QColor colorFromG4Rgb(const std::string& rgb) {
29 std::istringstream stream(rgb);
30 double r = 0.05;
31 double g = 0.05;
32 double b = 0.26;
33 stream >> r >> g >> b;
34 return QColor::fromRgbF(std::clamp(r, 0.0, 1.0),
35 std::clamp(g, 0.0, 1.0),
36 std::clamp(b, 0.0, 1.0));
37}
38
39QString g4RgbFromColor(const QColor& color) {
40 return QString("%1 %2 %3")
41 .arg(color.redF(), 0, 'f', 5)
42 .arg(color.greenF(), 0, 'f', 5)
43 .arg(color.blueF(), 0, 'f', 5);
44}
45
46QIcon colorIcon(const QColor& color, const QColor& border) {
47 QPixmap pixmap(18, 18);
48 pixmap.fill(Qt::transparent);
49
50 QPainter painter(&pixmap);
51 painter.setRenderHint(QPainter::Antialiasing);
52 painter.setBrush(color);
53 painter.setPen(border);
54 painter.drawRoundedRect(pixmap.rect().adjusted(1, 1, -2, -2), 3, 3);
55 return QIcon(pixmap);
56}
57
58std::string rootExtentForFieldCommand(const std::shared_ptr<GOptions>& gopts) {
59 if (gopts == nullptr) { return ""; }
60
61 std::string rootDefinition = gopts->getScalarString("root");
62 for (auto& c : rootDefinition) {
63 if (c == ',') { c = ' '; }
64 }
65
66 const auto tokens = getStringVectorFromString(rootDefinition);
67 if (tokens.size() < 5 || tokens[0] != "G4Box") { return ""; }
68
69 const double dx = getG4Number(tokens[1]);
70 const double dy = getG4Number(tokens[2]);
71 const double dz = getG4Number(tokens[3]);
72 if (dx <= 0 || dy <= 0 || dz <= 0) { return ""; }
73
74 std::ostringstream command;
75 command << "/vis/set/extentForField "
76 << -dx << " " << dx << " "
77 << -dy << " " << dy << " "
78 << -dz << " " << dz << " mm";
79 return command.str();
80}
81
82void applyRootExtentForField(const std::shared_ptr<GOptions>& gopts) {
83 const auto command = rootExtentForFieldCommand(gopts);
84 if (!command.empty()) { G4UImanager::GetUIpointer()->ApplyCommand(command); }
85}
86}
87
88G4DisplayView::G4DisplayView(const std::shared_ptr<GOptions>& gopts,
89 std::shared_ptr<GLogger> logger,
90 QWidget* parent)
91 : QWidget(parent), gopts(gopts), log(logger) {
92 log->debug(CONSTRUCTOR, "G4DisplayView");
93
94 // LCD font used to display theta/phi degrees next to sliders.
95 QFont flcd;
96 flcd.setFamilies({"Helvetica"}); // Qt6: prefer setFamilies over family string ctor
97 flcd.setPointSize(24);
98 flcd.setBold(true);
99
100 // Initial camera configuration comes from options; values are converted to degrees for UI.
101 G4Camera jcamera = getG4Camera(gopts);
102 double thetaValue = getG4Number(jcamera.theta);
103 double phiValue = getG4Number(jcamera.phi);
104 G4Light jlight = getG4Light(gopts);
105 double lightThetaValue = getG4Number(jlight.theta);
106 double lightPhiValue = getG4Number(jlight.phi);
107 G4View g4view = getG4View(gopts);
108 backgroundColor = colorFromG4Rgb(g4view.background);
109 cloudPoints = g4view.cloudPoints;
110
111 // Toggle buttons for common viewer/scene flags.
112 // Button 0 uses an SVG icon (set below); its title is left empty so only the SVG is shown.
113 vector<string> toggle_button_titles;
114 toggle_button_titles.emplace_back("");
115 toggle_button_titles.emplace_back("");
116 toggle_button_titles.emplace_back("");
117 toggle_button_titles.emplace_back("");
118
119 buttons_set1 = new GQTToggleButtonWidget(120, 120, 0, toggle_button_titles, false, this);
120 connect(buttons_set1, &GQTToggleButtonWidget::buttonPressedIndexChanged, this, &G4DisplayView::apply_buttons_set1);
121
122 buttons_set1->setSvgButtonIcon(0, ":/images/hidden_lines.svg", QSize(120, 120));
123 buttons_set1->setSvgButtonIcon(1, ":/images/anti_aliasing.svg", QSize(120, 120));
124 buttons_set1->setSvgButtonIcon(2, ":/images/auxiliary_edges.svg", QSize(120, 120));
125 buttons_set1->setSvgButtonIcon(3, ":/images/field_lines.svg", QSize(120, 120));
126
127 // Preset angle sets used by camera and light dropdowns.
128 QStringList theta_angle_Set;
129 for (int t = 0; t <= 180; t += 30) { theta_angle_Set << QString::number(t); }
130 QStringList phi_angle_Set;
131 for (int t = 0; t <= 360; t += 30) { phi_angle_Set << QString::number(t); }
132
133 // -------------------------
134 // Camera direction controls
135 // -------------------------
136
137 cameraTheta = new QSlider(Qt::Horizontal);
138 cameraTheta->setRange(0, 180);
139 cameraTheta->setSingleStep(1);
140 cameraTheta->setValue(thetaValue);
141 cameraTheta->setTracking(true); // updates while dragging
142
143 auto cameraThetaLabel = new QLabel(tr("θ"));
144
145 thetaLCD = new QLCDNumber(this);
146 thetaLCD->setFont(flcd);
147 thetaLCD->setMaximumSize(QSize(42, 32));
148 thetaLCD->setSegmentStyle(QLCDNumber::Flat);
149
150 thetaDropdown = new QComboBox(this);
151 thetaDropdown->addItems(theta_angle_Set);
152 thetaDropdown->setMaximumSize(QSize(100, 45));
153
154 auto cameraThetaLayout = new QHBoxLayout;
155 cameraThetaLayout->setContentsMargins(0, 0, 0, 0);
156 cameraThetaLayout->setSpacing(4);
157 cameraThetaLayout->addWidget(cameraThetaLabel);
158 cameraThetaLayout->addWidget(cameraTheta);
159 cameraThetaLayout->addWidget(thetaLCD);
160 cameraThetaLayout->addWidget(thetaDropdown);
161
162 cameraPhi = new QSlider(Qt::Horizontal);
163 cameraPhi->setRange(0, 360);
164 cameraPhi->setSingleStep(1);
165 cameraPhi->setValue(phiValue);
166 cameraPhi->setTracking(true); // updates while dragging
167
168 auto cameraPhiLabel = new QLabel(tr("ɸ"));
169
170 phiLCD = new QLCDNumber(this);
171 phiLCD->setFont(flcd);
172 phiLCD->setMaximumSize(QSize(42, 32));
173 phiLCD->setSegmentStyle(QLCDNumber::Flat);
174
175 phiDropdown = new QComboBox(this);
176 phiDropdown->addItems(phi_angle_Set);
177 phiDropdown->setMaximumSize(QSize(100, 45));
178
179 auto cameraPhiLayout = new QHBoxLayout;
180 cameraPhiLayout->setContentsMargins(0, 0, 0, 0);
181 cameraPhiLayout->setSpacing(4);
182 cameraPhiLayout->addWidget(cameraPhiLabel);
183 cameraPhiLayout->addWidget(cameraPhi);
184 cameraPhiLayout->addWidget(phiLCD);
185 cameraPhiLayout->addWidget(phiDropdown);
186
187 auto* readViewButton = new QPushButton(tr("Read View"), this);
188 readViewButton->setToolTip(tr("Sync sliders to current viewer orientation"));
189 auto* readViewRow = new QHBoxLayout;
190 readViewRow->setContentsMargins(0, 0, 0, 0);
191 readViewRow->addStretch(1);
192 readViewRow->addWidget(readViewButton);
193
194 QVBoxLayout* cameraDirectionLayout = new QVBoxLayout;
195 cameraDirectionLayout->setContentsMargins(6, 6, 6, 6);
196 cameraDirectionLayout->setSpacing(2);
197 cameraDirectionLayout->addLayout(cameraThetaLayout);
198 cameraDirectionLayout->addLayout(cameraPhiLayout);
199 cameraDirectionLayout->addLayout(readViewRow);
200
201 QGroupBox* cameraAnglesGroup = new QGroupBox(tr("Camera Direction"));
202 cameraAnglesGroup->setLayout(cameraDirectionLayout);
203
204 // Slider -> Geant4 command, and slider -> LCD.
205 connect(cameraTheta, &QSlider::valueChanged, this, &G4DisplayView::changeCameraDirection);
206 connect(cameraTheta, &QSlider::valueChanged, thetaLCD, qOverload<int>(&QLCDNumber::display));
207
208 // Dropdown -> Geant4 command, and dropdown -> slider sync.
209 connect(thetaDropdown, &QComboBox::currentTextChanged,
210 this, [this](const QString&) { setCameraDirection(0); });
211
212 connect(cameraPhi, &QSlider::valueChanged, this, &G4DisplayView::changeCameraDirection);
213 connect(cameraPhi, &QSlider::valueChanged, phiLCD, qOverload<int>(&QLCDNumber::display));
214
215 connect(phiDropdown, &QComboBox::currentTextChanged,
216 this, [this](const QString&) { setCameraDirection(1); });
217
218 connect(readViewButton, &QPushButton::clicked, this, &G4DisplayView::readCameraFromViewer);
219
220 // ---------------------
221 // View properties group
222 // ---------------------
223
224 // Projection: orthogonal or perspective (with typical angles).
225 QLabel* projLabel = new QLabel(tr("Projection:"));
226 perspectiveDropdown = new QComboBox;
227 perspectiveDropdown->addItem(tr("Orthogonal"));
228 perspectiveDropdown->addItem(tr("Perspective 30"));
229 perspectiveDropdown->addItem(tr("Perspective 45"));
230 perspectiveDropdown->addItem(tr("Perspective 60"));
231
232 // Sides per circle: maps to /vis/viewer/set/lineSegmentsPerCircle.
233 QLabel* sides_per_circlesLabel = new QLabel(tr("Sides per circle:"));
234 precisionDropdown = new QComboBox;
235 precisionDropdown->addItem(tr("50"));
236 precisionDropdown->addItem(tr("100"));
237 precisionDropdown->addItem(tr("200"));
238 precisionDropdown->addItem(tr("300"));
239 precisionDropdown->setCurrentIndex(0);
240
241 connect(perspectiveDropdown, &QComboBox::currentTextChanged, this, &G4DisplayView::set_projection);
242 connect(precisionDropdown, &QComboBox::currentTextChanged, this, &G4DisplayView::set_precision);
243
244 QVBoxLayout* resolutionAndPerspectiveLayout = new QVBoxLayout;
245 resolutionAndPerspectiveLayout->setContentsMargins(6, 6, 6, 6);
246 resolutionAndPerspectiveLayout->setSpacing(4);
247 resolutionAndPerspectiveLayout->addWidget(projLabel);
248 resolutionAndPerspectiveLayout->addWidget(perspectiveDropdown);
249 resolutionAndPerspectiveLayout->addWidget(sides_per_circlesLabel);
250 resolutionAndPerspectiveLayout->addWidget(precisionDropdown);
251
252 QGroupBox* propertyGroup = new QGroupBox(tr("View Properties"));
253 propertyGroup->setLayout(resolutionAndPerspectiveLayout);
254
255 QHBoxLayout* cameraAndPerspective = new QHBoxLayout;
256 cameraAndPerspective->setContentsMargins(0, 0, 0, 0);
257 cameraAndPerspective->setSpacing(6);
258 cameraAndPerspective->addWidget(cameraAnglesGroup);
259 cameraAndPerspective->addWidget(propertyGroup);
260
261 // -------------------------
262 // Light direction controls
263 // -------------------------
264
265 lightTheta = new QSlider(Qt::Horizontal);
266 lightTheta->setRange(0, 180);
267 lightTheta->setSingleStep(1);
268 lightTheta->setValue(lightThetaValue);
269 lightTheta->setTracking(true);
270
271 auto lightThetaLabel = new QLabel(tr("θ"));
272
273 lthetaLCD = new QLCDNumber(this);
274 lthetaLCD->setFont(flcd);
275 lthetaLCD->setMaximumSize(QSize(42, 32));
276 lthetaLCD->setSegmentStyle(QLCDNumber::Flat);
277
278 lthetaDropdown = new QComboBox(this);
279 lthetaDropdown->addItems(theta_angle_Set);
280 lthetaDropdown->setMaximumSize(QSize(100, 45));
281
282 auto lightThetaLayout = new QHBoxLayout;
283 lightThetaLayout->setContentsMargins(0, 0, 0, 0);
284 lightThetaLayout->setSpacing(4);
285 lightThetaLayout->addWidget(lightThetaLabel);
286 lightThetaLayout->addWidget(lightTheta);
287 lightThetaLayout->addWidget(lthetaLCD);
288 lightThetaLayout->addWidget(lthetaDropdown);
289
290 lightPhi = new QSlider(Qt::Horizontal);
291 lightPhi->setRange(0, 360);
292 lightPhi->setSingleStep(1);
293 lightPhi->setValue(lightPhiValue);
294 lightPhi->setTracking(true);
295
296 auto lightPhiLabel = new QLabel(tr("ɸ"));
297
298 lphiLCD = new QLCDNumber(this);
299 lphiLCD->setFont(flcd);
300 lphiLCD->setMaximumSize(QSize(42, 32));
301 lphiLCD->setSegmentStyle(QLCDNumber::Flat);
302
303 lphiDropdown = new QComboBox(this);
304 lphiDropdown->addItems(phi_angle_Set);
305 lphiDropdown->setMaximumSize(QSize(100, 45));
306
307 auto lightPhiLayout = new QHBoxLayout;
308 lightPhiLayout->setContentsMargins(0, 0, 0, 0);
309 lightPhiLayout->setSpacing(4);
310 lightPhiLayout->addWidget(lightPhiLabel);
311 lightPhiLayout->addWidget(lightPhi);
312 lightPhiLayout->addWidget(lphiLCD);
313 lightPhiLayout->addWidget(lphiDropdown);
314
315 auto lightDirectionLayout = new QVBoxLayout;
316 lightDirectionLayout->setContentsMargins(6, 6, 6, 6);
317 lightDirectionLayout->setSpacing(2);
318 lightDirectionLayout->addLayout(lightThetaLayout);
319 lightDirectionLayout->addLayout(lightPhiLayout);
320
321 QGroupBox* lightAnglesGroup = new QGroupBox(tr("Light Direction"));
322 lightAnglesGroup->setLayout(lightDirectionLayout);
323
324 connect(lightTheta, &QSlider::valueChanged, this, &G4DisplayView::changeLightDirection);
325 connect(lightTheta, &QSlider::valueChanged, lthetaLCD, qOverload<int>(&QLCDNumber::display));
326 connect(lthetaDropdown, &QComboBox::currentTextChanged,
327 this, [this](const QString&) { setLightDirection(0); });
328
329 connect(lightPhi, &QSlider::valueChanged, this, &G4DisplayView::changeLightDirection);
330 connect(lightPhi, &QSlider::valueChanged, lphiLCD, qOverload<int>(&QLCDNumber::display));
331 connect(lphiDropdown, &QComboBox::currentTextChanged,
332 this, [this](const QString&) { setLightDirection(1); });
333
334 // ----------------------
335 // Scene properties group
336 // ----------------------
337
338 QLabel* cullingLabel = new QLabel(tr("Culling:"));
339 cullingDropdown = new QComboBox;
340 cullingDropdown->addItem(tr("Reset"));
341 cullingDropdown->addItem(tr("Covered Daughters"));
342 cullingDropdown->addItem(tr("Density: 1 mg/cm3"));
343 cullingDropdown->addItem(tr("Density: 10 mg/cm3"));
344 cullingDropdown->addItem(tr("Density: 100 mg/cm3"));
345 cullingDropdown->addItem(tr("Density: 1 g/cm3"));
346 cullingDropdown->addItem(tr("Density: 10 g/cm3"));
347
348 QLabel* backgroundColorLabel = new QLabel(tr("Background Color:"));
349 backgroundColorDropdown = new QComboBox;
350 const auto addBackgroundPreset = [this](const QString& name, const QString& rgb) {
351 backgroundColorDropdown->addItem(name, rgb);
352 };
353 addBackgroundPreset(tr("lightslategray"), "0.46667 0.53333 0.60000");
354 addBackgroundPreset(tr("ghostwhite"), "0.97255 0.97255 1.00000");
355 addBackgroundPreset(tr("black"), "0.00000 0.00000 0.00000");
356 addBackgroundPreset(tr("navy"), "0.00000 0.00000 0.50196");
357 addBackgroundPreset(tr("whitesmoke"), "0.96078 0.96078 0.96078");
358 addBackgroundPreset(tr("lightskyblue"), "0.52941 0.80784 0.98039");
359 addBackgroundPreset(tr("deepskyblue"), "0.00000 0.74902 1.00000");
360 addBackgroundPreset(tr("lightsteelblue"), "0.69020 0.76863 0.87059");
361 addBackgroundPreset(tr("blueviolet"), "0.54118 0.16863 0.88627");
362 addBackgroundPreset(tr("turquoise"), "0.25098 0.87843 0.81569");
363 addBackgroundPreset(tr("mediumaquamarine"), "0.40000 0.80392 0.66667");
364 addBackgroundPreset(tr("springgreen"), "0.00000 1.00000 0.49804");
365 addBackgroundPreset(tr("lawngreen"), "0.48627 0.98824 0.00000");
366 addBackgroundPreset(tr("yellowgreen"), "0.60392 0.80392 0.19608");
367 addBackgroundPreset(tr("lemonchiffon"), "1.00000 0.98039 0.80392");
368 addBackgroundPreset(tr("antiquewhite"), "0.98039 0.92157 0.84314");
369 addBackgroundPreset(tr("wheat"), "0.96078 0.87059 0.70196");
370 addBackgroundPreset(tr("sienna"), "0.62745 0.32157 0.17647");
371 addBackgroundPreset(tr("snow"), "1.00000 0.98039 0.98039");
372 addBackgroundPreset(tr("floralwhite"), "1.00000 0.98039 0.94118");
373 addBackgroundPreset(tr("lightsalmon"), "1.00000 0.62745 0.47843");
374 addBackgroundPreset(tr("orchid"), "0.85490 0.43922 0.83922");
375 addBackgroundPreset(tr("plum"), "0.86667 0.62745 0.86667");
376 setBackgroundDropdownColor(backgroundColor);
377
378 backgroundColorButton = new QToolButton(this);
379 backgroundColorButton->setToolTip(tr("Choose background color"));
380 backgroundColorButton->setAutoRaise(true);
381 backgroundColorButton->setIconSize(QSize(18, 18));
382 setBackgroundButtonColor(backgroundColor);
383
384 auto* cloudPointsLabel = new QLabel(tr("Number of Cloud Points:"));
385 cloudPointsSpinBox = new QSpinBox(this);
386 cloudPointsSpinBox->setRange(1, 100000000);
387 cloudPointsSpinBox->setSingleStep(100);
388 cloudPointsSpinBox->setGroupSeparatorShown(true);
389 cloudPointsSpinBox->setValue(std::max(1, cloudPoints));
390
391 auto* explodeLabel = new QLabel(tr("Explode Factor:"));
392 explodeSlider = new QSlider(Qt::Horizontal);
393 explodeSlider->setRange(0, 100);
394 explodeSlider->setValue(0);
395 explodeSlider->setSingleStep(1);
396 explodeSlider->setPageStep(5);
397 explodeValueLabel = new QLabel(tr("1.00"));
398 explodeIntensityDropdown = new QComboBox;
399 explodeIntensityDropdown->addItem(tr("Low"), QVariant(45.0));
400 explodeIntensityDropdown->addItem(tr("Medium"), QVariant(15.0));
401 explodeIntensityDropdown->addItem(tr("High"), QVariant(5.0));
402 explodeIntensityDropdown->setCurrentIndex(1);
403
404 connect(cullingDropdown, &QComboBox::currentTextChanged, this, &G4DisplayView::set_culling);
405 connect(backgroundColorDropdown, &QComboBox::currentTextChanged, this, &G4DisplayView::set_background);
406 connect(backgroundColorButton, &QToolButton::clicked, this, &G4DisplayView::choose_background_color);
407 connect(cloudPointsSpinBox, qOverload<int>(&QSpinBox::valueChanged), this, &G4DisplayView::set_cloud_points);
408 connect(explodeSlider, &QSlider::valueChanged, this, &G4DisplayView::set_explode);
409 connect(explodeIntensityDropdown, &QComboBox::currentIndexChanged, this, &G4DisplayView::set_explode);
410
411 QVBoxLayout* sceneLayout = new QVBoxLayout;
412 sceneLayout->setContentsMargins(6, 6, 6, 6);
413 sceneLayout->setSpacing(4);
414 sceneLayout->addWidget(cullingLabel);
415 sceneLayout->addWidget(cullingDropdown);
416 sceneLayout->addWidget(backgroundColorLabel);
417 auto* backgroundLayout = new QHBoxLayout;
418 backgroundLayout->setContentsMargins(0, 0, 0, 0);
419 backgroundLayout->setSpacing(4);
420 backgroundLayout->addWidget(backgroundColorDropdown);
421 backgroundLayout->addWidget(backgroundColorButton);
422 sceneLayout->addLayout(backgroundLayout);
423 sceneLayout->addWidget(cloudPointsLabel);
424 sceneLayout->addWidget(cloudPointsSpinBox);
425 sceneLayout->addWidget(explodeLabel);
426 auto* explodeRow = new QHBoxLayout;
427 explodeRow->setContentsMargins(0, 0, 0, 0);
428 explodeRow->setSpacing(4);
429 explodeRow->addWidget(explodeSlider);
430 explodeRow->addWidget(explodeValueLabel);
431 explodeRow->addWidget(explodeIntensityDropdown);
432 sceneLayout->addLayout(explodeRow);
433
434 QGroupBox* spropertyGroup = new QGroupBox(tr("Scene Properties"));
435 spropertyGroup->setLayout(sceneLayout);
436
437 QHBoxLayout* lightAndProperties = new QHBoxLayout;
438 lightAndProperties->setContentsMargins(0, 0, 0, 0);
439 lightAndProperties->setSpacing(6);
440 lightAndProperties->addWidget(lightAnglesGroup);
441 lightAndProperties->addWidget(spropertyGroup);
442
443 // -------------------------
444 // Slice (cutaway) controls
445 // -------------------------
446
447 // X slice controls.
448 sliceXEdit = new QLineEdit(tr("0"));
449 sliceXEdit->setMaximumWidth(100);
450 sliceXEdit->setValidator(new QDoubleValidator(sliceXEdit));
451 sliceXActi = new QCheckBox(tr("&On"));
452 sliceXActi->setChecked(false);
453 sliceXInve = new QCheckBox(tr("&Flip"));
454 sliceXInve->setChecked(false);
455
456 auto sliceXLayout = new QHBoxLayout;
457 sliceXLayout->setContentsMargins(0, 0, 0, 0);
458 sliceXLayout->setSpacing(4);
459 sliceXLayout->addWidget(new QLabel(tr("X: ")));
460 sliceXLayout->addWidget(sliceXEdit);
461 sliceXLayout->addSpacing(16);
462 sliceXLayout->addWidget(sliceXActi);
463 sliceXLayout->addWidget(sliceXInve);
464 sliceXLayout->addStretch(1);
465
466 // Y slice controls.
467 sliceYEdit = new QLineEdit(tr("0"));
468 sliceYEdit->setMaximumWidth(100);
469 sliceYEdit->setValidator(new QDoubleValidator(sliceYEdit));
470 sliceYActi = new QCheckBox(tr("&On"));
471 sliceYActi->setChecked(false);
472 sliceYInve = new QCheckBox(tr("&Flip"));
473 sliceYInve->setChecked(false);
474
475 auto sliceYLayout = new QHBoxLayout;
476 sliceYLayout->setContentsMargins(0, 0, 0, 0);
477 sliceYLayout->setSpacing(4);
478 sliceYLayout->addWidget(new QLabel(tr("Y: ")));
479 sliceYLayout->addWidget(sliceYEdit);
480 sliceYLayout->addSpacing(16);
481 sliceYLayout->addWidget(sliceYActi);
482 sliceYLayout->addWidget(sliceYInve);
483 sliceYLayout->addStretch(1);
484
485 // Z slice controls.
486 sliceZEdit = new QLineEdit(tr("0"));
487 sliceZEdit->setMaximumWidth(100);
488 sliceZEdit->setValidator(new QDoubleValidator(sliceZEdit));
489 sliceZActi = new QCheckBox(tr("&On"));
490 sliceZActi->setChecked(false);
491 sliceZInve = new QCheckBox(tr("&Flip"));
492 sliceZInve->setChecked(false);
493
494 auto sliceZLayout = new QHBoxLayout;
495 sliceZLayout->setContentsMargins(0, 0, 0, 0);
496 sliceZLayout->setSpacing(4);
497 sliceZLayout->addWidget(new QLabel(tr("Z: ")));
498 sliceZLayout->addWidget(sliceZEdit);
499 sliceZLayout->addSpacing(16);
500 sliceZLayout->addWidget(sliceZActi);
501 sliceZLayout->addWidget(sliceZInve);
502 sliceZLayout->addStretch(1);
503
504 // Clear slice planes button.
505 QPushButton* clearSliceButton = new QPushButton(tr("Clear Slices"));
506 clearSliceButton->setToolTip("Clear Slice Planes");
507 clearSliceButton->setIcon(QIcon::fromTheme("edit-clear"));
508 clearSliceButton->setIconSize(QSize(16, 16));
509 connect(clearSliceButton, &QPushButton::clicked, this, &G4DisplayView::clearSlices);
510
511 // Slice composition mode: intersection vs union.
512 QGroupBox* sliceChoiceBox = new QGroupBox(tr("Slices Style"));
513 sliceSectn = new QRadioButton(tr("&Intersection"), sliceChoiceBox);
514 sliceUnion = new QRadioButton(tr("&Union"), sliceChoiceBox);
515 sliceSectn->setChecked(true);
516
517 connect(sliceSectn, &QRadioButton::toggled, this, &G4DisplayView::slice);
518 connect(sliceUnion, &QRadioButton::toggled, this, &G4DisplayView::slice);
519
520 auto sliceChoiceLayout = new QHBoxLayout;
521 sliceChoiceLayout->setContentsMargins(6, 4, 6, 4);
522 sliceChoiceLayout->setSpacing(8);
523 sliceChoiceLayout->addWidget(sliceSectn);
524 sliceChoiceLayout->addWidget(sliceUnion);
525 sliceChoiceBox->setLayout(sliceChoiceLayout);
526
527 // Slices layout.
528 auto sliceLayout = new QVBoxLayout;
529 sliceLayout->setContentsMargins(0, 0, 0, 0);
530 sliceLayout->setSpacing(3);
531 sliceLayout->addLayout(sliceXLayout);
532 sliceLayout->addLayout(sliceYLayout);
533 sliceLayout->addLayout(sliceZLayout);
534 sliceLayout->addWidget(sliceChoiceBox);
535 sliceLayout->addWidget(clearSliceButton);
536
537 // Connect slice UI signals to slice recomputation.
538 connect(sliceXEdit, &QLineEdit::returnPressed, this, &G4DisplayView::slice);
539 connect(sliceYEdit, &QLineEdit::returnPressed, this, &G4DisplayView::slice);
540 connect(sliceZEdit, &QLineEdit::returnPressed, this, &G4DisplayView::slice);
541
542#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) // Qt ≤ 6.6
543 connect(sliceXActi, &QCheckBox::stateChanged, this, &G4DisplayView::slice);
544 connect(sliceYActi, &QCheckBox::stateChanged, this, &G4DisplayView::slice);
545 connect(sliceZActi, &QCheckBox::stateChanged, this, &G4DisplayView::slice);
546 connect(sliceXInve, &QCheckBox::stateChanged, this, &G4DisplayView::slice);
547 connect(sliceYInve, &QCheckBox::stateChanged, this, &G4DisplayView::slice);
548 connect(sliceZInve, &QCheckBox::stateChanged, this, &G4DisplayView::slice);
549#else // Qt ≥ 6.7
550 connect(sliceXActi, &QCheckBox::checkStateChanged, this, &G4DisplayView::slice);
551 connect(sliceYActi, &QCheckBox::checkStateChanged, this, &G4DisplayView::slice);
552 connect(sliceZActi, &QCheckBox::checkStateChanged, this, &G4DisplayView::slice);
553 connect(sliceXInve, &QCheckBox::checkStateChanged, this, &G4DisplayView::slice);
554 connect(sliceYInve, &QCheckBox::checkStateChanged, this, &G4DisplayView::slice);
555 connect(sliceZInve, &QCheckBox::checkStateChanged, this, &G4DisplayView::slice);
556#endif
557
558 // ------------------------------
559 // Field line precision controls
560 // ------------------------------
561
562 QGroupBox* fieldPrecisionBox = new QGroupBox(tr("Number of Field Points"));
563 field_npoints = new QLineEdit(QString::number(field_NPOINTS), this);
564 field_npoints->setMaximumWidth(40);
565
566 QFont font = field_npoints->font();
567 font.setPointSize(18);
568 field_npoints->setFont(font);
569
570 connect(field_npoints, &QLineEdit::returnPressed, this, &G4DisplayView::field_precision_changed);
571
572 // Buttons + field point count UI combined.
573 auto fieldPointsHBox = new QHBoxLayout;
574 fieldPointsHBox->setContentsMargins(6, 4, 6, 4);
575 fieldPointsHBox->setSpacing(4);
576 fieldPointsHBox->addWidget(field_npoints);
577 fieldPrecisionBox->setLayout(fieldPointsHBox);
578
579 auto buttons_field_HBox = new QHBoxLayout;
580 buttons_field_HBox->setContentsMargins(0, 0, 0, 0);
581 buttons_field_HBox->setSpacing(6);
582 buttons_field_HBox->addWidget(buttons_set1);
583 buttons_field_HBox->addWidget(fieldPrecisionBox);
584 fieldPrecisionBox->setMaximumHeight(3 * buttons_set1->height());
585 fieldPrecisionBox->setMaximumWidth(140);
586
587 // -------------------------
588 // Assemble final tab layout
589 // -------------------------
590
591 auto mainLayout = new QVBoxLayout;
592 mainLayout->setContentsMargins(6, 6, 6, 6);
593 mainLayout->setSpacing(6);
594 mainLayout->addLayout(buttons_field_HBox);
595 mainLayout->addLayout(cameraAndPerspective);
596 mainLayout->addLayout(lightAndProperties);
597 mainLayout->addLayout(sliceLayout);
598 setLayout(mainLayout);
599}
600
601void G4DisplayView::changeCameraDirection() {
602 // Construct the Geant4 command using the current slider values and send it to the UI manager.
603 string command = "/vis/viewer/set/viewpointThetaPhi " +
604 to_string(cameraTheta->value()) + " " +
605 to_string(cameraPhi->value());
606 G4UImanager::GetUIpointer()->ApplyCommand(command);
607}
608
609void G4DisplayView::setCameraDirection(int which) {
610 // Dropdown values are interpreted as degrees; command is issued first, then the slider is synced.
611 string thetaValue = thetaDropdown->currentText().toStdString();
612 string phiValue = phiDropdown->currentText().toStdString();
613
614 string command = "/vis/viewer/set/viewpointThetaPhi " + thetaValue + " " + phiValue;
615 G4UImanager::GetUIpointer()->ApplyCommand(command);
616
617 int thetaDeg = thetaDropdown->currentText().toInt();
618 int phiDeg = phiDropdown->currentText().toInt();
619
620 if (cameraTheta && which == 0) cameraTheta->setValue(thetaDeg);
621 if (cameraPhi && which == 1) cameraPhi->setValue(phiDeg);
622}
623
624void G4DisplayView::set_projection() {
625 // Dropdown selections map to Geant4 projection parameters: orthogonal (o) or perspective (p, angle).
626 string value = perspectiveDropdown->currentText().toStdString();
627
628 string g4perspective = "o";
629 string g4perpvalue = "0";
630 if (value.find("Perspective") != string::npos) {
631 g4perspective = "p";
632 // Second token after space is the angle.
633 g4perpvalue = value.substr(value.find(" ") + 1);
634 }
635
636 string command = "/vis/viewer/set/projection " + g4perspective + " " + g4perpvalue;
637 G4UImanager::GetUIpointer()->ApplyCommand(command);
638}
639
640void G4DisplayView::set_precision() {
641 // Viewer circle segmentation impacts curved primitive smoothness.
642 string value = precisionDropdown->currentText().toStdString();
643
644 string command = "/vis/viewer/set/lineSegmentsPerCircle " + value;
645 G4UImanager::GetUIpointer()->ApplyCommand(command);
646 G4UImanager::GetUIpointer()->ApplyCommand("/vis/viewer/flush");
647}
648
649void G4DisplayView::set_culling() {
650 // Culling configuration uses Geant4 viewer culling commands.
651 string value = cullingDropdown->currentText().toStdString();
652 int index = cullingDropdown->currentIndex() - 2;
653
654 if (value.find("Reset") != string::npos) {
655 G4UImanager::GetUIpointer()->ApplyCommand("/vis/viewer/set/culling global true");
656 G4UImanager::GetUIpointer()->ApplyCommand("/vis/viewer/set/culling density false");
657 }
658 else if (value.find("Daughters") != string::npos) {
659 G4UImanager::GetUIpointer()->ApplyCommand("/vis/viewer/set/culling coveredDaughters true");
660 G4UImanager::GetUIpointer()->ApplyCommand("/vis/viewer/set/culling density false");
661 }
662 else {
663 // Density-based thresholds (unit assumed by Geant4 command expectations).
664 vector<double> density = {0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0};
665 string command = "/vis/viewer/set/culling density true " + to_string(density[index]);
666 G4UImanager::GetUIpointer()->ApplyCommand(command);
667 G4UImanager::GetUIpointer()->ApplyCommand("/vis/viewer/flush");
668 log->info(0, command);
669 }
670}
671
672void G4DisplayView::syncViewToOptions() {
673 if (!gopts) return;
674 const auto g4view = getG4View(gopts);
675 const QString s = QString("{driver: \"%1\", dimension: \"%2\", position: \"%3\", "
676 "segsPerCircle: %4, background: \"%5\", cloudPoints: %6}")
677 .arg(QString::fromStdString(g4view.driver))
678 .arg(QString::fromStdString(g4view.dimension))
679 .arg(QString::fromStdString(g4view.position))
680 .arg(g4view.segsPerCircle)
681 .arg(g4RgbFromColor(backgroundColor))
682 .arg(cloudPoints);
683 gopts->setOptionValueFromString("g4view", s.toStdString());
684}
685
686void G4DisplayView::setBackgroundButtonColor(const QColor& color) {
687 if (!backgroundColorButton) { return; }
688
689 backgroundColorButton->setIcon(colorIcon(color, palette().color(QPalette::Mid)));
690}
691
692void G4DisplayView::setBackgroundDropdownColor(const QColor& color) {
693 if (!backgroundColorDropdown) { return; }
694
695 QSignalBlocker blocker(backgroundColorDropdown);
696 const QString wanted = g4RgbFromColor(color);
697 for (int i = 0; i < backgroundColorDropdown->count(); ++i) {
698 const QColor candidate = colorFromG4Rgb(backgroundColorDropdown->itemData(i).toString().toStdString());
699 if (qAbs(candidate.redF() - color.redF()) < 0.0001 &&
700 qAbs(candidate.greenF() - color.greenF()) < 0.0001 &&
701 qAbs(candidate.blueF() - color.blueF()) < 0.0001) {
702 backgroundColorDropdown->setCurrentIndex(i);
703 return;
704 }
705 }
706
707 backgroundColorDropdown->addItem(tr("custom"), wanted);
708 backgroundColorDropdown->setCurrentIndex(backgroundColorDropdown->count() - 1);
709}
710
711void G4DisplayView::set_background() {
712 const QString g4value = backgroundColorDropdown->currentData().toString();
713 if (g4value.isEmpty()) { return; }
714
715 backgroundColor = colorFromG4Rgb(g4value.toStdString());
716 setBackgroundButtonColor(backgroundColor);
717
718 string command = "/vis/viewer/set/background " + g4value.toStdString();
719 G4UImanager::GetUIpointer()->ApplyCommand(command);
720 syncViewToOptions();
721}
722
723void G4DisplayView::choose_background_color() {
724 const QColor selected = QColorDialog::getColor(backgroundColor, this, tr("Select background color"));
725 if (!selected.isValid()) { return; }
726
727 backgroundColor = selected;
728 setBackgroundButtonColor(backgroundColor);
729 setBackgroundDropdownColor(backgroundColor);
730
731 const QString g4value = g4RgbFromColor(backgroundColor);
732 const string command = "/vis/viewer/set/background " + g4value.toStdString();
733 G4UImanager::GetUIpointer()->ApplyCommand(command);
734 syncViewToOptions();
735}
736
737void G4DisplayView::set_cloud_points() {
738 if (!cloudPointsSpinBox) { return; }
739
740 cloudPoints = cloudPointsSpinBox->value();
741 const string command = "/vis/viewer/set/numberOfCloudPoints " + to_string(cloudPoints);
742 G4UImanager::GetUIpointer()->ApplyCommand(command);
743 syncViewToOptions();
744}
745
747 QWidget::showEvent(event);
748 set_background();
749 set_cloud_points();
750 readCameraFromViewer();
751
752 // Light direction: use g4light values if specified, otherwise follow camera.
753 const auto g4light = getG4Light(gopts);
754 const double toDegrees = 180.0 / M_PI;
755 const double lightThetaDeg = getG4Number(g4light.theta) * toDegrees;
756 const double lightPhiDeg = getG4Number(g4light.phi) * toDegrees;
757
758 const int lt = (lightThetaDeg == 0.0 && lightPhiDeg == 0.0)
759 ? cameraTheta->value()
760 : static_cast<int>(std::round(lightThetaDeg));
761 const int lp = (lightThetaDeg == 0.0 && lightPhiDeg == 0.0)
762 ? cameraPhi->value()
763 : static_cast<int>(std::round(lightPhiDeg));
764
765 QSignalBlocker blt(lightTheta);
766 QSignalBlocker blp(lightPhi);
767 lightTheta->setValue(lt);
768 lightPhi->setValue(lp);
769 lthetaLCD->display(lt);
770 lphiLCD->display(lp);
771}
772
773void G4DisplayView::changeLightDirection() {
774 // Construct the Geant4 command using the current slider values and send it to the UI manager.
775 string command = "/vis/viewer/set/lightsThetaPhi " +
776 to_string(lightTheta->value()) + " " +
777 to_string(lightPhi->value());
778 G4UImanager::GetUIpointer()->ApplyCommand(command);
779}
780
781void G4DisplayView::setLightDirection(int which) {
782 // Dropdown values are interpreted as degrees; command is issued first, then the slider is synced.
783 string thetaValue = lthetaDropdown->currentText().toStdString();
784 string phiValue = lphiDropdown->currentText().toStdString();
785
786 string command = "/vis/viewer/set/lightsThetaPhi " + thetaValue + " " + phiValue;
787 G4UImanager::GetUIpointer()->ApplyCommand(command);
788
789 int thetaDeg = lthetaDropdown->currentText().toInt();
790 int phiDeg = lphiDropdown->currentText().toInt();
791
792 if (lightTheta && which == 0) lightTheta->setValue(thetaDeg);
793 if (lightPhi && which == 1) lightPhi->setValue(phiDeg);
794}
795
796void G4DisplayView::slice() {
797 G4UImanager* g4uim = G4UImanager::GetUIpointer();
798 if (g4uim == nullptr) { return; }
799
800 // Reset existing planes before applying the newly requested slice configuration.
801 g4uim->ApplyCommand("/vis/viewer/clearCutawayPlanes");
802
803 // Select how multiple planes combine.
804 if (sliceSectn->isChecked()) { g4uim->ApplyCommand("/vis/viewer/set/cutawayMode intersection"); }
805 else if (sliceUnion->isChecked()) { g4uim->ApplyCommand("/vis/viewer/set/cutawayMode union"); }
806
807 // For each enabled axis, add a plane at the requested position. Values are interpreted as mm.
808 if (sliceXActi->isChecked()) {
809 string command = "/vis/viewer/addCutawayPlane " + sliceXEdit->text().toStdString() + " 0 0 mm " +
810 to_string(sliceXInve->isChecked() ? -1 : 1) + " 0 0 ";
811 g4uim->ApplyCommand(command);
812 }
813
814 if (sliceYActi->isChecked()) {
815 string command = "/vis/viewer/addCutawayPlane 0 " + sliceYEdit->text().toStdString() + " 0 mm 0 " +
816 to_string(sliceYInve->isChecked() ? -1 : 1) + " 0 ";
817 g4uim->ApplyCommand(command);
818 }
819
820 if (sliceZActi->isChecked()) {
821 string command = "/vis/viewer/addCutawayPlane 0 0 " + sliceZEdit->text().toStdString() + " mm 0 0 " +
822 to_string(sliceZInve->isChecked() ? -1 : 1);
823 g4uim->ApplyCommand(command);
824 }
825
826 g4uim->ApplyCommand("/vis/viewer/rebuild");
827 g4uim->ApplyCommand("/vis/viewer/flush");
828}
829
830void G4DisplayView::clearSlices() {
831 // Clear cutaway planes in the viewer and reset activation UI state.
832 G4UImanager* g4uim = G4UImanager::GetUIpointer();
833 if (g4uim == nullptr) { return; }
834
835 QSignalBlocker blockX(sliceXActi);
836 QSignalBlocker blockY(sliceYActi);
837 QSignalBlocker blockZ(sliceZActi);
838
839 sliceXActi->setChecked(false);
840 sliceYActi->setChecked(false);
841 sliceZActi->setChecked(false);
842
843 g4uim->ApplyCommand("/vis/viewer/clearCutawayPlanes");
844 g4uim->ApplyCommand("/vis/viewer/rebuild");
845 g4uim->ApplyCommand("/vis/viewer/flush");
846}
847
848void G4DisplayView::apply_buttons_set1(int index) {
849 G4UImanager* g4uim = G4UImanager::GetUIpointer();
850 if (g4uim == nullptr) { return; }
851
852 bool button_state = buttons_set1->lastButtonState();
853
854 if (index == 0) {
855 // Hidden edges on/off.
856 string command = string("/vis/viewer/set/hiddenEdge") + (button_state ? " 1" : " 0");
857 g4uim->ApplyCommand(command);
858 g4uim->ApplyCommand("/vis/viewer/flush");
859 }
860 else if (index == 1) {
861 // Anti-aliasing: handled via OpenGL state for viewers where it applies.
862 if (button_state == 0) {
863 glDisable(GL_LINE_SMOOTH);
864 glDisable(GL_POLYGON_SMOOTH);
865 }
866 else {
867 glEnable(GL_LINE_SMOOTH);
868 glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
869 glEnable(GL_POLYGON_SMOOTH);
870 glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
871 }
872 }
873 else if (index == 2) {
874 // Auxiliary edges typically implies hidden edges; keep UI state coherent.
875 string command = string("/vis/viewer/set/auxiliaryEdge") + (button_state ? " 1" : " 0");
876 g4uim->ApplyCommand(command);
877
878 command = string("/vis/viewer/set/hiddenEdge") + (button_state ? " 1" : " 0");
879 g4uim->ApplyCommand(command);
880
881 if (buttons_set1->buttonStatus(0) != button_state) { buttons_set1->toggleButton(0); }
882 }
883 else if (index == 3) {
884 // Magnetic field line visualization model.
885 if (button_state == 0) {
886 string command = string("/vis/scene/activateModel Field 0");
887 g4uim->ApplyCommand(command);
888 g4uim->ApplyCommand("/vis/scene/removeModel Field");
889 }
890 else {
891 string npoints = to_string(field_NPOINTS);
892 applyRootExtentForField(gopts);
893 string command = string("/vis/scene/add/magneticField ") + npoints;
894 g4uim->ApplyCommand(command);
895 }
896 }
897}
898
899void G4DisplayView::set_explode() {
900 if (!explodeSlider) return;
901
902 const double divisor = (explodeIntensityDropdown && explodeIntensityDropdown->currentData().toDouble() > 0)
903 ? explodeIntensityDropdown->currentData().toDouble()
904 : 15.0;
905 const int boom = explodeSlider->value();
906 const double xf = 1.0 + static_cast<double>(boom) / divisor;
907
908 if (explodeValueLabel)
909 explodeValueLabel->setText(QString::number(xf, 'f', 2));
910
911 G4UImanager* g4uim = G4UImanager::GetUIpointer();
912 if (!g4uim) return;
913 g4uim->ApplyCommand("/vis/viewer/set/explodeFactor " + QString::number(xf, 'f', 2).toStdString());
914 g4uim->ApplyCommand("/vis/viewer/flush");
915}
916
917void G4DisplayView::readCameraFromViewer() {
918 auto* vm = G4VisManager::GetInstance();
919 if (!vm) return;
920 const auto* viewer = vm->GetCurrentViewer();
921 if (!viewer) return;
922
923 const G4Vector3D& vp = viewer->GetViewParameters().GetViewpointDirection();
924
925 const double cosTheta = std::clamp(vp.z(), -1.0, 1.0);
926 const double thetaDeg = std::acos(cosTheta) * 180.0 / M_PI;
927 double phiDeg = std::atan2(vp.y(), vp.x()) * 180.0 / M_PI;
928 if (phiDeg < 0.0) phiDeg += 360.0;
929
930 const int thetaInt = static_cast<int>(std::round(thetaDeg));
931 const int phiInt = static_cast<int>(std::round(phiDeg));
932
933 QSignalBlocker bt(cameraTheta);
934 QSignalBlocker bp(cameraPhi);
935 cameraTheta->setValue(thetaInt);
936 cameraPhi->setValue(phiInt);
937 thetaLCD->display(thetaInt);
938 phiLCD->display(phiInt);
939}
940
941void G4DisplayView::field_precision_changed() {
942 G4UImanager* g4uim = G4UImanager::GetUIpointer();
943 if (g4uim == nullptr) { return; }
944
945 // Parse the updated point count from the UI.
946 field_NPOINTS = field_npoints->text().toInt();
947
948 // If field lines are currently active, re-issue the field model to apply the new point count.
949 if (buttons_set1->buttonStatus(3) == 1) {
950 string command = string("vis/scene/activateModel Field 0");
951 g4uim->ApplyCommand(command);
952 g4uim->ApplyCommand("/vis/scene/removeModel Field");
953
954 string npoints = to_string(field_NPOINTS);
955 applyRootExtentForField(gopts);
956 command = string("/vis/scene/add/magneticField ") + npoints;
957 G4UImanager::GetUIpointer()->ApplyCommand(command);
958 }
959}
G4DisplayView(const std::shared_ptr< GOptions > &gopts, std::shared_ptr< GLogger > logger, QWidget *parent=nullptr)
Construct the view-control tab.
void showEvent(QShowEvent *event) override
void buttonPressedIndexChanged(int index)
Option structures and helpers for g4display configuration.
Declaration of the G4DisplayView tab widget.
event
CONSTRUCTOR
G4Light getG4Light(const std::shared_ptr< GOptions > &gopts)
Read the g4light option node and return a projected G4Light struct.
G4View getG4View(const std::shared_ptr< GOptions > &gopts)
Read the g4view option node and return a projected G4View struct.
G4Camera getG4Camera(const std::shared_ptr< GOptions > &gopts)
Read the g4camera option node and return a projected G4Camera struct.
double getG4Number(const string &v, bool warnIfNotUnit=false)
constexpr const char * to_string(randomModel m) noexcept
vector< std::string > getStringVectorFromString(const std::string &input)
Camera angle configuration derived from the g4camera option node.
Light angle configuration derived from the g4light option node.
Viewer configuration derived from the g4view option node.