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