gtree
Loading...
Searching...
No Matches
gtree.cc
Go to the documentation of this file.
1// Implementation of the GTree Qt widget and its internal per-volume cache model.
2// Doxygen documentation is authoritative in the header; this file uses short
3// non-Doxygen comments to summarize behavior.
4
5// c++
6#include <cmath>
7#include <set>
8#include <sstream>
9
10// geant4
11#include "G4BooleanSolid.hh"
12
13// Qt
14#include <QHeaderView>
15#include <QTextEdit>
16#include <QVBoxLayout>
17#include <QColorDialog>
18#include <QMessageBox>
19#include <QSignalBlocker>
20#include <QStringList>
21#include <QTimer>
22
23// gtree
24#include "gtree.h"
25#include "gtree_options.h"
26
27// gemc
28#include "gsystemConventions.h"
29
30// geant4
31#include "G4VisAttributes.hh"
32#include "G4Material.hh"
33#include "G4VisManager.hh"
34#include "G4VViewer.hh"
35#include "G4VSceneHandler.hh"
36#include "G4VGraphicsSystem.hh"
37#include "G4UImanager.hh"
38#include "gtouchable.h"
39#include "gutilities.h"
40
41namespace {
42
43std::vector<std::string> splitParams(const std::string& s) {
44 std::vector<std::string> result;
45 std::istringstream ss(s);
46 std::string tok;
47 while (std::getline(ss, tok, ',')) {
48 const auto a = tok.find_first_not_of(" \t");
49 const auto b = tok.find_last_not_of(" \t");
50 if (a != std::string::npos)
51 result.push_back(tok.substr(a, b - a + 1));
52 }
53 return result;
54}
55
56using PDesc = std::vector<const char*>;
57
58const PDesc& solidParamDescs(const std::string& solid, std::size_t n) {
59 static const std::unordered_map<std::string, PDesc> kFixed = {
60 {"G4Box", {"dx: half length in x",
61 "dy: half length in y",
62 "dz: half length in z"}},
63 {"G4Tubs", {"rin: inner radius",
64 "rout: outer radius",
65 "length: half length in z",
66 "phi start: starting phi angle",
67 "phi total: total phi angle"}},
68 {"G4Cons", {"rin1: inner radius at -dz",
69 "rout1: outer radius at -dz",
70 "rin2: inner radius at +dz",
71 "rout2: outer radius at +dz",
72 "length: half length in z",
73 "phi start: starting phi angle",
74 "phi total: total phi angle"}},
75 {"G4Trd", {"dx1: half length in x at -dz",
76 "dx2: half length in x at +dz",
77 "dy1: half length in y at -dz",
78 "dy2: half length in y at +dz",
79 "z: half length in z"}},
80 {"G4Sphere", {"rmin: inner radius",
81 "rmax: outer radius",
82 "sphi: starting phi angle",
83 "dphi: delta phi angle",
84 "stheta: starting theta angle",
85 "dtheta: delta theta angle"}},
86 {"G4Paraboloid", {"dz: half length in z",
87 "r1: radius at -dz",
88 "r2: radius at +dz"}},
89 {"G4EllipticalTube", {"dx: half length in x",
90 "dy: half length in y",
91 "dz: half length in z"}},
92 };
93 static const std::unordered_map<std::size_t, PDesc> kTrap = {
94 {4, {"pz: length along Z",
95 "py: length along Y",
96 "px: length along X (wider side)",
97 "pltx: length along X (narrower side)"}},
98 {11, {"pDz: half Z length",
99 "pTheta: polar angle of line joining base centres",
100 "pPhi: azimuthal angle of line joining base centres",
101 "pDy1: half Y length at -dz",
102 "pDx1: half X length at smaller Y, base at -dz",
103 "pDx2: half X length at bigger Y, base at -dz",
104 "pAlp1: angle between Y-axis and centre line at -dz",
105 "pDy2: half Y length at +dz",
106 "pDx3: half X length at smaller Y, base at +dz",
107 "pDx4: half X length at bigger Y, base at +dz",
108 "pAlp2: angle between Y-axis and centre line at +dz"}},
109 };
110 static const PDesc kEmpty;
111
112 if (solid == "G4Trap") {
113 const auto it = kTrap.find(n);
114 return it != kTrap.end() ? it->second : kEmpty;
115 }
116 const auto it = kFixed.find(solid);
117 return it != kFixed.end() ? it->second : kEmpty;
118}
119
120// Format a "number*unit" or bare "number" token: max 3 decimal places,
121// values with |x| < 1e-7 are shown as 0.
122static QString formatVal(const std::string& token) {
123 const auto star = token.find('*');
124 const std::string numPart = (star == std::string::npos) ? token : token.substr(0, star);
125 const std::string unit = (star == std::string::npos) ? "" : token.substr(star);
126 try {
127 std::size_t pos = 0;
128 double v = std::stod(numPart, &pos);
129 if (pos != numPart.size()) return QString::fromStdString(token);
130 if (std::abs(v) < 1e-7) v = 0.0;
131 QString s = QString::number(v, 'f', 3);
132 while (s.endsWith('0') && s.contains('.')) s.chop(1);
133 if (s.endsWith('.')) s.chop(1);
134 return s + QString::fromStdString(unit);
135 } catch (...) {
136 return QString::fromStdString(token);
137 }
138}
139
140QString formatParameters(const std::string& solid, const std::string& paramsStr) {
141 if (paramsStr.empty()) return {};
142 const auto vals = splitParams(paramsStr);
143 if (vals.empty()) return {};
144
145 QString html = "Parameters:";
146
147 // Polycone: fixed header (phiStart, phiTotal, nplanes) then arrays
148 if (solid == "G4Polycone" && vals.size() >= 3) {
149 const char* kPcFixed[3] = {
150 "phi start: starting phi angle",
151 "phi total: total phi angle",
152 "nplanes: number of planes"
153 };
154 for (int i = 0; i < 3 && i < (int)vals.size(); ++i)
155 html += QString("<br>&nbsp;&nbsp;<i>%1</i>: %2").arg(kPcFixed[i], formatVal(vals[i]));
156 int nplanes = 0;
157 try { nplanes = std::stoi(vals[2]); } catch (...) {}
158 int idx = 3;
159 for (int p = 0; p < nplanes && idx < (int)vals.size(); ++p, ++idx)
160 html += QString("<br>&nbsp;&nbsp;<i>z[%1]</i>: %2").arg(p).arg(formatVal(vals[idx]));
161 for (int p = 0; p < nplanes && idx < (int)vals.size(); ++p, ++idx)
162 html += QString("<br>&nbsp;&nbsp;<i>rin[%1]</i>: %2").arg(p).arg(formatVal(vals[idx]));
163 for (int p = 0; p < nplanes && idx < (int)vals.size(); ++p, ++idx)
164 html += QString("<br>&nbsp;&nbsp;<i>rout[%1]</i>: %2").arg(p).arg(formatVal(vals[idx]));
165 return html;
166 }
167
168 // G4Trap from 8 vertices (24 params)
169 if (solid == "G4Trap" && vals.size() == 24) {
170 const char* kAxes[3] = {"x", "y", "z"};
171 for (int v = 0; v < 8; ++v)
172 for (int c = 0; c < 3; ++c)
173 html += QString("<br>&nbsp;&nbsp;<i>v%1%2</i>: %3")
174 .arg(v + 1).arg(kAxes[c]).arg(formatVal(vals[v * 3 + c]));
175 return html;
176 }
177
178 // All other solids (including G4Trap with 4 or 11 params)
179 const auto& descs = solidParamDescs(solid, vals.size());
180 for (std::size_t i = 0; i < vals.size(); ++i) {
181 const QString desc = i < descs.size()
182 ? QString::fromStdString(descs[i])
183 : QString("missing parameters description");
184 html += QString("<br>&nbsp;&nbsp;<i>%1</i>: %2").arg(desc, formatVal(vals[i]));
185 }
186 return html;
187}
188
189std::string resolveBooleanOperandName(const std::string& systemName,
190 const std::string& operand,
191 const std::unordered_map<std::string, const GVolume*>& gvolumes) {
192 if (gvolumes.find(operand) != gvolumes.end()) return operand;
193
194 const std::string qualified = systemName.empty() ? operand : systemName + "/" + operand;
195 if (gvolumes.find(qualified) != gvolumes.end()) return qualified;
196
197 return operand;
198}
199
200std::string leafName(const std::string& volumeName) {
201 const auto slash = volumeName.find_last_of('/');
202 return slash == std::string::npos ? volumeName : volumeName.substr(slash + 1);
203}
204
205QString joinBooleanTokens(const std::vector<std::string>& tokens) {
206 QStringList parts;
207 for (const auto& token : tokens) {
208 parts << QString::fromStdString(token).toHtmlEscaped();
209 }
210 return parts.join(QStringLiteral(" "));
211}
212
213QString formatOperationLine(const std::vector<std::string>& tokens,
214 const std::string& systemName,
215 const std::unordered_map<std::string, const GVolume*>& gvolumes) {
216 if (tokens.size() != 3) return joinBooleanTokens(tokens);
217
218 const auto left = leafName(resolveBooleanOperandName(systemName, tokens[0], gvolumes));
219 const auto right = leafName(resolveBooleanOperandName(systemName, tokens[2], gvolumes));
220 return QString("%1 %2 %3").arg(QString::fromStdString(left).toHtmlEscaped(),
221 QString::fromStdString(tokens[1]).toHtmlEscaped(),
222 QString::fromStdString(right).toHtmlEscaped());
223}
224
225void appendNestedBooleanOperationLines(QString& html,
226 const std::string& volumeName,
227 const std::string& systemName,
228 const std::unordered_map<std::string, const GVolume*>& gvolumes,
229 std::set<std::string>& visited) {
230 const auto fullName = resolveBooleanOperandName(systemName, volumeName, gvolumes);
231 if (!visited.insert(fullName).second) return;
232
233 const auto gvolIt = gvolumes.find(fullName);
234 if (gvolIt == gvolumes.end()) return;
235
236 const auto solidsOpr = gvolIt->second->getSolidsOpr();
237 if (solidsOpr.empty() || solidsOpr == UNINITIALIZEDSTRINGQUANTITY) return;
238
239 const auto tokens = gutilities::getStringVectorFromString(solidsOpr);
240 if (tokens.size() != 3) return;
241
242 html += QString("<br>&nbsp;&nbsp;%1 = %2")
243 .arg(QString::fromStdString(leafName(fullName)).toHtmlEscaped(),
244 formatOperationLine(tokens, systemName, gvolumes));
245
246 appendNestedBooleanOperationLines(html, tokens[0], systemName, gvolumes, visited);
247 appendNestedBooleanOperationLines(html, tokens[2], systemName, gvolumes, visited);
248}
249
250QString formatBooleanOperationDescription(const std::string& fullName,
251 const std::string& solidsOpr,
252 const std::unordered_map<std::string, const GVolume*>& gvolumes) {
253 if (solidsOpr.empty() || solidsOpr == UNINITIALIZEDSTRINGQUANTITY) return {};
254
255 const auto tokens = gutilities::getStringVectorFromString(solidsOpr);
256 if (tokens.size() != 3) {
257 return QObject::tr("Boolean Operation: %1").arg(QString::fromStdString(solidsOpr).toHtmlEscaped());
258 }
259
260 const auto slash = fullName.find_last_of('/');
261 const std::string systemName = slash == std::string::npos ? "" : fullName.substr(0, slash);
262 QString html = QObject::tr("Boolean Operation: %1").arg(formatOperationLine(tokens, systemName, gvolumes));
263
264 std::set<std::string> visited{fullName};
265 appendNestedBooleanOperationLines(html, tokens[0], systemName, gvolumes, visited);
266 appendNestedBooleanOperationLines(html, tokens[2], systemName, gvolumes, visited);
267 return html;
268}
269
270} // anonymous namespace
271
272
273// Cache a volume's hierarchy, material, visualization attributes, and pygemc descriptor.
274G4Ttree_item::G4Ttree_item(G4Volume* g4volume, const GVolume* gvolume) {
275 if (g4volume == nullptr || g4volume->getLogical() == nullptr || g4volume->getSolid() == nullptr) {
276 mother = MOTHEROFUSALL;
277 material = "unavailable";
278 color = QColor::fromRgbF(1.0, 1.0, 1.0);
279 opacity = 1.0;
280 is_visible = false;
281 return;
282 }
283
284 auto pvolume = g4volume->getPhysical();
285 auto lvolume = g4volume->getLogical();
286 auto svolume = g4volume->getSolid();
287
288 std::string lname = lvolume->GetName();
289 if (lname != ROOTWORLDGVOLUMENAME) {
290 auto mlvolume = pvolume != nullptr ? pvolume->GetMotherLogical() : nullptr;
291 mother = mlvolume != nullptr ? mlvolume->GetName() : MOTHEROFUSALL;
292 material = lvolume->GetMaterial()->GetName();
293 }
294 else {
295 mother = MOTHEROFUSALL;
296 material = "G4_Galactic";
297 }
298
299 // Read visualization attributes from the logical volume.
300 auto visAttributes = lvolume->GetVisAttributes();
301 if (visAttributes != nullptr) {
302 auto gcolor = visAttributes->GetColour();
303 color = QColor::fromRgbF(gcolor.GetRed(), gcolor.GetGreen(), gcolor.GetBlue());
304 opacity = gcolor.GetAlpha();
305 is_visible = visAttributes->IsVisible();
306 }
307 else {
308 // No vis attributes assigned (e.g. imported CAD/GDML volumes): fall back to opaque white, visible.
309 color = QColor::fromRgbF(1.0, 1.0, 1.0);
310 opacity = 1.0;
311 is_visible = true;
312 }
313
314 // Physics quantities (mass, volume, density) are computed lazily on first
315 // request: see compute_physics_quantities().
316 logical_ptr = lvolume;
317 solid_ptr = svolume;
318
319 if (gvolume) {
320 solidType = gvolume->getType();
321 parameters = gvolume->getParameters();
322 position = gvolume->getPos();
323 rotation = gvolume->getRot();
324 motherVolume = gvolume->getMotherName();
325 solidsOpr = gvolume->getSolidsOpr();
326 volDescription = gvolume->getDescription();
327 }
328}
329
330
331// Compute mass, volume, and density on first request. Geant4 estimates a boolean
332// solid's cubic volume by Monte Carlo with 1M points by default — seconds per solid —
333// so boolean solids use a bounded 100k-point estimate (~1% accuracy, plenty for
334// display), and the mass is not propagated to daughters (whose solids may themselves
335// be booleans). This keeps volume selection interactive on boolean-heavy systems.
336void G4Ttree_item::compute_physics_quantities() const {
337 if (physics_computed) return;
338 physics_computed = true;
339 if (logical_ptr == nullptr || solid_ptr == nullptr) return;
340
341 if (dynamic_cast<G4BooleanSolid*>(solid_ptr) != nullptr) {
342 volume = solid_ptr->EstimateCubicVolume(100000, 0.001) / CLHEP::cm3;
343 density = logical_ptr->GetMaterial()->GetDensity() / (CLHEP::g / CLHEP::cm3);
344 mass = volume * density;
345 }
346 else {
347 volume = solid_ptr->GetCubicVolume() / CLHEP::cm3;
348 mass = logical_ptr->GetMass(false, false) / CLHEP::g;
349 density = volume > 0 ? mass / volume : 0.0;
350 }
351}
352
353
354// Extract a short display name from a full "system/volume" name.
355std::string G4Ttree_item::vname_from_v4name(std::string v4name) {
356 // return name after '/'
357 return v4name.substr(v4name.find_last_of('/') + 1);
358}
359
360// Extract the system prefix from a full "system/volume" name.
361std::string G4Ttree_item::system_from_v4name(std::string v4name) {
362 // return name before '/'
363 return v4name.substr(0, v4name.find_last_of('/'));
364}
365
366
367// Construct the widget, build the internal model, create the UI, and connect signals.
368GTree::GTree(const std::shared_ptr<GOptions>& gopt,
369 std::unordered_map<std::string, G4Volume*> g4volumes_map,
370 std::unordered_map<std::string, const GVolume*> gvolumes_map_in,
371 QWidget* parent) :
372 QWidget(parent),
373 GBase(gopt, GTREE_LOGGER),
374 gvolumes_map(std::move(gvolumes_map_in)) {
375 // Build the internal representation used to populate the UI tree.
376 build_tree(g4volumes_map);
377
378 // create the UI
379 treeWidget = new QTreeWidget(this);
380 treeWidget->setColumnCount(3);
381 QStringList headers;
382 headers << "Visibility" << "Color" << "Name";
383 treeWidget->setHeaderLabels(headers);
384 treeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
385 treeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
386 treeWidget->header()->setSectionResizeMode(2, QHeaderView::Stretch);
387 treeWidget->setRootIsDecorated(true);
388 treeWidget->setAlternatingRowColors(true);
389
390 auto* mainLayout = new QHBoxLayout(this);
391 mainLayout->addWidget(treeWidget, /*stretch*/ 3);
392
393 // Right: property panel has ~same width. Increase stretch to increase
394 rightPanel = right_widget();
395 mainLayout->addWidget(rightPanel, /*stretch*/ 3);
396
397 setLayout(mainLayout);
398
399 // populate the tree from g4_systems_tree
400 populateTree();
401
402 // react to checkboxes
403 connect(treeWidget, &QTreeWidget::itemChanged,
404 this, &GTree::onItemChanged);
405
406
407 // react to clicks / selection to update the right panel
408 connect(treeWidget, &QTreeWidget::itemClicked,
409 this, &GTree::onTreeItemClicked);
410
411 connect(treeWidget, &QTreeWidget::currentItemChanged,
412 this, &GTree::onCurrentItemChanged);
413
414 // connect GQTButtonsWidget signal button_pressed to slot changeStyle()
415 connect(styleButtons->buttonsWidget,
416 SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem*)),
417 this, SLOT(changeStyle()));
418
419
420 // connect slider to slot
421 connect(opacitySlider, &QSlider::valueChanged,
422 this, &GTree::onOpacitySliderChanged);
423
424
425 log->debug(NORMAL, SFUNCTION_NAME, "GTree added");
426}
427
428// Build the Qt tree view from the internal system/volume model.
429void GTree::populateTree() {
430 // for each system
431 for (auto& [systemName, volMap] : g4_systems_tree) {
432 // top-level item for the system
433 auto* systemItem = new QTreeWidgetItem(treeWidget);
434 systemItem->setText(2, QString::fromStdString(systemName));
435 systemItem->setFlags(systemItem->flags() | Qt::ItemIsUserCheckable);
436 systemItem->setCheckState(0, Qt::Checked);
437
438 // We'll build the volume hierarchy inside this system using a lookup map.
439 std::map<std::string, QTreeWidgetItem*> itemLookup;
440
441 // --------------------------------------------------------------------
442 // 1st pass: create items WITHOUT parents, configure text/data/checkbox
443 // --------------------------------------------------------------------
444 for (auto& [volName, vptr] : volMap) {
445 const G4Ttree_item* vitem = vptr.get();
446
447 auto* item = new QTreeWidgetItem; // no parent yet
448 item->setText(2, QString::fromStdString(G4Ttree_item::vname_from_v4name(volName)));
449 item->setData(2, Qt::UserRole, QString::fromStdString(volName)); // store full v4 name
450
451 // checkbox for visibility:
452 item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
453 item->setCheckState(0, vitem->get_visibility() ? Qt::Checked : Qt::Unchecked);
454
455 itemLookup[volName] = item;
456 }
457
458 // --------------------------------------------------------------------
459 // 2nd pass: attach items to their parent (system or mother)
460 // --------------------------------------------------------------------
461 for (auto& [volName, vptr] : volMap) {
462 const G4Ttree_item* vitem = vptr.get();
463 auto mother = vitem->get_mother(); // full v4 name of mother
464 auto* thisItem = itemLookup[volName];
465
466 QTreeWidgetItem* parentItem = systemItem;
467
468 // If a mother exists and is present in this system's lookup, attach under it.
469 if (!mother.empty() && mother != "root") {
470 auto itM = itemLookup.find(mother);
471 if (itM != itemLookup.end()) {
472 parentItem = itM->second;
473 }
474 }
475
476 parentItem->addChild(thisItem);
477 }
478
479 // --------------------------------------------------------------------
480 // 3rd pass: create color buttons now that items are in the tree
481 // --------------------------------------------------------------------
482 for (auto& [volName, vptr] : volMap) {
483 const G4Ttree_item* vitem = vptr.get();
484 QTreeWidgetItem* item = itemLookup[volName];
485
486 auto* colorBtn = new QPushButton(treeWidget);
487 QColor c = vitem->get_color();
488 colorBtn->setFixedSize(20, 20);
489 colorBtn->setFlat(true); // no 3D/bevel look
490 colorBtn->setText(QString()); // no text
491
492 colorBtn->setStyleSheet(
493 QString("QPushButton { background-color: %1; border: 1px solid %2; }")
494 .arg(c.name(), palette().color(QPalette::Mid).name())
495 );
496
497 // Store the full volume name as a property so the slot can retrieve it.
498 colorBtn->setProperty("volumeName", QString::fromStdString(volName));
499 connect(colorBtn, &QPushButton::clicked, this, &GTree::onColorButtonClicked);
500
501 treeWidget->setItemWidget(item, 1, colorBtn);
502 }
503 }
504
505 treeWidget->expandAll();
506}
507
508
509// Build the internal system/volume model from the provided volume map.
510void GTree::build_tree(std::unordered_map<std::string, G4Volume*> g4volumes_map) {
511 // loop over map
512 for (auto [name, g4volume] : g4volumes_map) {
513 if (g4volume == nullptr || g4volume->getLogical() == nullptr ||
514 g4volume->getSolid() == nullptr || g4volume->getPhysical() == nullptr) {
515 log->info(2, "Skipping non-placed helper volume <", name, "> from tree");
516 continue;
517 }
518
519 // skip the Geant4 world / ROOTWORLDGVOLUMENAME
520 // auto lvolume = g4volume->getLogical();
521 //
522 // if (lvolume && lvolume->GetName() == ROOTWORLDGVOLUMENAME) {
523 // log->info(2, "Skipping world volume >", name, "< from tree");
524 // continue;
525 // }
526
527 auto system_name = G4Ttree_item::system_from_v4name(name);
528
529 // ensure the system exists
530 auto& system_tree = g4_systems_tree[system_name];
531 const GVolume* gvol = nullptr;
532 auto it = gvolumes_map.find(name);
533 if (it != gvolumes_map.end()) gvol = it->second;
534 system_tree[name] = std::make_unique<G4Ttree_item>(g4volume, gvol);
535
536 // do not log mass/volume/density here: reading them triggers the (potentially
537 // Monte-Carlo) physics computation for every volume in the tree
538 log->info(2, "Adding ", name, " to tree, system_name is ", system_name);
539 }
540}
541
542// Apply visibility checkbox changes to the selected item and its direct children.
543void GTree::onItemChanged(QTreeWidgetItem* item, int column) {
544 if (column != 0) return; // we care about visibility column only
545
546 // is this a volume item? (has stored v4 name in UserRole)
547 QVariant v = item->data(2, Qt::UserRole);
548
549 // --------------------------------------------------------------------
550 // SYSTEM item: no UserRole data → propagate to direct daughters
551 // --------------------------------------------------------------------
552 if (!v.isValid()) {
553 bool visible = (item->checkState(0) == Qt::Checked);
554
555 QSignalBlocker blocker(treeWidget); // avoid recursive itemChanged
556
557 const int nChildren = item->childCount();
558 for (int i = 0; i < nChildren; ++i) {
559 QTreeWidgetItem* child = item->child(i);
560 QVariant cv = child->data(2, Qt::UserRole);
561 if (!cv.isValid())
562 continue; // skip non-volume children (shouldn't happen)
563
564 const QString fullName = cv.toString();
565
566 // sync child checkbox
567 child->setCheckState(0, visible ? Qt::Checked : Qt::Unchecked);
568
569 // apply to Geant4
570 set_visibility(fullName.toStdString(), visible);
571 }
572
573 return;
574 }
575
576 // --------------------------------------------------------------------
577 // VOLUME item: apply to itself and its direct daughter volumes
578 // --------------------------------------------------------------------
579 const QString fullName = v.toString();
580 bool visible = (item->checkState(0) == Qt::Checked);
581
582 QSignalBlocker blocker(treeWidget); // avoid recursive itemChanged
583
584 // 1) apply to this volume
585 set_visibility(fullName.toStdString(), visible);
586
587 // 2) apply same visibility to its direct daughters
588 const int nChildren = item->childCount();
589 for (int i = 0; i < nChildren; ++i) {
590 QTreeWidgetItem* child = item->child(i);
591 QVariant cv = child->data(2, Qt::UserRole);
592 if (!cv.isValid())
593 continue; // skip if not a volume
594
595 const QString childName = cv.toString();
596
597 // sync child checkbox
598 child->setCheckState(0, visible ? Qt::Checked : Qt::Unchecked);
599
600 // apply to Geant4
601 set_visibility(childName.toStdString(), visible);
602 }
603}
604
605
606// Open the color dialog and apply a new RGB color to the selected volume.
607void GTree::onColorButtonClicked() {
608 auto* btn = qobject_cast<QPushButton*>(sender());
609 if (!btn)
610 return;
611
612 const QString volName = btn->property("volumeName").toString();
613 if (volName.isEmpty())
614 return;
615
616 QColor initial = Qt::white;
617
618 QColor c = QColorDialog::getColor(initial, this, tr("Select color"));
619 if (!c.isValid())
620 return;
621
622 // Update button appearance
623 btn->setStyleSheet(
624 QString("QPushButton { background-color: %1; border: 1px solid %2; }")
625 .arg(c.name(), palette().color(QPalette::Mid).name())
626 );
627
628 // tell your model
629 set_color(volName.toStdString(), c);
630}
631
632
633// Send a Geant4 UI command to toggle visibility for a specific volume.
634void GTree::set_visibility(const std::string& volumeName, bool visible) {
635 std::string vis_int = visible ? "1" : "0";
636
637 std::string command = "/vis/geometry/set/visibility " + volumeName + " -1 " + vis_int;
638
639 // World visibility uses a different depth value.
640 if (volumeName == ROOTWORLDGVOLUMENAME) {
641 command = "/vis/geometry/set/visibility " + volumeName + " 0 " + vis_int;
642 }
643
645}
646
647// Send a Geant4 UI command to set RGB color for a specific volume.
648void GTree::set_color(const std::string& volumeName, const QColor& c) {
649 G4Ttree_item* item = findTreeItem(volumeName);
650 double currentOpacity = item ? item->get_opacity() : 1.0;
651 if (item) item->set_color(c);
652
653 std::string command = "/vis/geometry/set/colour " + volumeName + " 0 "
654 + std::to_string(c.redF()) + " "
655 + std::to_string(c.greenF()) + " "
656 + std::to_string(c.blueF()) + " "
657 + std::to_string(currentOpacity);
658
660}
661
662// Return number of direct children in the Qt tree for the given item.
663int GTree::get_ndaughters(QTreeWidgetItem* item) const {
664 if (!item) return 0;
665 return item->childCount();
666}
667
668// Find the cached model record for a volume by its full name.
669G4Ttree_item* GTree::findTreeItem(const std::string& fullName) {
670 for (const auto& [systemName, volMap] : g4_systems_tree) {
671 auto it = volMap.find(fullName);
672 if (it != volMap.end()) {
673 return it->second.get();
674 }
675 }
676 return nullptr;
677}
678
679
680// Update the right-side panel according to the clicked item.
681void GTree::onTreeItemClicked(QTreeWidgetItem* item, int /*column*/) {
682 if (!bottomPanel)
683 return;
684
685 if (!item) {
686 bottomPanel->setVisible(false);
687 current_volume_name.clear();
688 return;
689 }
690
691 bottomPanel->setVisible(true);
692
693 // Is it a volume? (volume items store the full v4 name in UserRole)
694 QVariant v = item->data(2, Qt::UserRole);
695 bool isVolume = v.isValid();
696
697 // Type label
698 if (isVolume) {
699 typeLabel->setText(QStringLiteral("<b>G4 Volume</b>"));
700 current_volume_name = v.toString().toStdString();
701 }
702 else {
703 typeLabel->setText(QStringLiteral("<b>System</b>"));
704 current_volume_name.clear();
705 }
706
707 // Number of direct daughters
708 int nd = get_ndaughters(item);
709 daughtersLabel->setText(tr("Daughters: %1").arg(nd));
710
711 // Name (column 2 text)
712 QString itemName = item->text(2);
713 nameLabel->setText(tr("Name: %1").arg(itemName));
714
715 // Material / density / mass
716 if (isVolume) {
717 styleButtons->setVisible(true);
718
719 const std::string fullName = v.toString().toStdString();
720 const G4Ttree_item* titem = findTreeItem(fullName);
721
722 if (titem) {
723 materialLabel->setText(
724 tr("Material: %1").arg(QString::fromStdString(titem->get_material()))
725 );
726 auto mass = titem->get_mass();
727 if (mass < 1000) {
728 massLabel->setText(tr("Mass: %1 g").arg(mass));
729 }
730 else {
731 massLabel->setText(tr("Mass: %1 kg").arg(mass / 1000));
732 }
733 auto volume = titem->get_volume();
734 if (volume < 1000000) {
735 volumeLabel->setText(tr("Volume: %1 cm3").arg(volume));
736 }
737 else {
738 volumeLabel->setText(tr("Volume: %1 m3").arg(volume / 1000000));
739 }
740
741 densityLabel->setText(
742 tr("Density: %1 g / cm3").arg(titem->get_density())
743 );
744
745 // pygemc descriptor fields
746 const auto solidT = titem->get_solidType();
747 solidTypeLabel->setText(solidT.empty() ? QString()
748 : tr("Solid: %1").arg(QString::fromStdString(solidT)));
749 const auto params = titem->get_parameters();
750 const bool hasParams = !params.empty();
751 parametersLabel->setVisible(hasParams);
752 if (hasParams) parametersLabel->setHtml(formatParameters(solidT, params));
753 const auto pos = titem->get_position();
754 positionLabel->setText(pos.empty() ? QString()
755 : tr("Position: %1").arg(QString::fromStdString(pos)));
756 const auto rot = titem->get_rotation();
757 rotationLabel->setText(rot.empty() ? QString()
758 : tr("Rotation: %1").arg(QString::fromStdString(rot)));
759 const auto mom = titem->get_motherVolume();
760 motherLabel->setText(mom.empty() ? QString() : tr("Mother: %1").arg(QString::fromStdString(mom)));
761 const auto boolDesc =
762 formatBooleanOperationDescription(fullName, titem->get_solidsOpr(), gvolumes_map);
763 if (!boolDesc.isEmpty()) {
764 descriptionLabel->setText(boolDesc);
765 }
766 else {
767 const auto desc = titem->get_volDescription();
768 descriptionLabel->setText(desc.empty() ? QString()
769 : tr("Description: %1")
770 .arg(QString::fromStdString(desc)));
771 }
772
773 // Sync the slider position to the cached alpha channel.
774 double op = titem->get_opacity();
775 int sliderVal = static_cast<int>(op * 100.0 + 0.5);
776 {
777 QSignalBlocker blocker(opacitySlider);
778 opacitySlider->setValue(sliderVal);
779 }
780 opacityLabel->setText(QString::number(op, 'f', 2));
781 opacitySlider->setVisible(true);
782 }
783
784 // Update the inspect and draw-overlaps buttons with the selected volume's leaf name.
785 if (inspectButton) {
786 const QString leaf = QString::fromStdString(G4Ttree_item::vname_from_v4name(current_volume_name));
787 inspectButton->setText(tr("Inspect %1").arg(leaf));
788 inspectButton->setVisible(true);
789 }
790 if (drawOverlapsButton) {
791 const QString leaf = QString::fromStdString(G4Ttree_item::vname_from_v4name(current_volume_name));
792 drawOverlapsButton->setText(tr("Draw Logical Overlaps %1").arg(leaf));
793 drawOverlapsButton->setVisible(true);
794 }
795 }
796 else {
797 styleButtons->setVisible(false);
798 opacitySlider->setVisible(false);
799 if (inspectButton) inspectButton->setVisible(false);
800 if (drawOverlapsButton) drawOverlapsButton->setVisible(false);
801 // Systems don't have a single material etc.
802 materialLabel->setText(tr(""));
803 massLabel->setText(tr(""));
804 volumeLabel->setText(tr(""));
805 densityLabel->setText(tr(""));
806 solidTypeLabel->setText(tr(""));
807 parametersLabel->clear();
808 parametersLabel->setVisible(false);
809 positionLabel->setText(tr(""));
810 rotationLabel->setText(tr(""));
811 motherLabel->setText(tr(""));
812 descriptionLabel->setText(tr(""));
813 }
814}
815
816// Keep the right-side panel updated when selection changes via keyboard navigation.
817void GTree::onCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) {
818 Q_UNUSED(previous);
819 if (!current)
820 return;
821
822 // reset styleButtons
823 styleButtons->reset_buttons();
824
825 // Reuse the same logic as mouse clicks
826 onTreeItemClicked(current, 0);
827}
828
829// Apply a representation command based on the currently selected style button.
830void GTree::changeStyle() {
831
832 // No-op if no volume is selected (system selected or nothing selected).
833 if (current_volume_name.empty())
834 return;
835
836 int button_index = styleButtons->button_pressed();
837
838 if (button_index == 3) {
839 // Action button: deselect it immediately (it is not a persistent style toggle).
840 {
841 QSignalBlocker blocker(styleButtons->buttonsWidget);
842 styleButtons->reset_buttons();
843 }
844 centreTwinkle();
845 return;
846 }
847
848 std::string command;
849
850 if (button_index == 0) {
851 command = "/vis/geometry/set/forceWireframe " + current_volume_name + " 0 1 ";
852 }
853 else if (button_index == 1) {
854 command = "/vis/geometry/set/forceSolid " + current_volume_name + " 0 1 ";
855 }
856 else if (button_index == 2) {
857 command = "/vis/geometry/set/forceCloud " + current_volume_name + " 0 1 ";
858 }
859 else {
860 // Unknown button index: avoid issuing an empty or malformed UI command.
861 return;
862 }
863
865}
866
867
868// Centre the viewer on the selected volume and kick off the twinkle animation.
869void GTree::centreTwinkle() {
870 G4Ttree_item* item = findTreeItem(current_volume_name);
871 if (!item) return;
872
873 twinkleVolumeName = current_volume_name;
874 twinkleSavedColor = item->get_color();
875 twinkleSavedOpacity = item->get_opacity();
876 twinkleTick = 0;
877
878 gutilities::apply_uimanager_commands("/vis/viewer/centreOn " + twinkleVolumeName);
879
880 if (!twinkleTimer) {
881 twinkleTimer = new QTimer(this);
882 connect(twinkleTimer, &QTimer::timeout, this, &GTree::onTwinkleStep);
883 }
884 if (twinkleTimer->isActive())
885 twinkleTimer->stop();
886
887 twinkleTimer->start(180);
888}
889
890
891// Cycle through flash colors, then restore the original colour and alpha.
892void GTree::onTwinkleStep() {
893 constexpr int kSteps = 5;
894 static const QColor kFlash[kSteps] = {
895 QColor(255, 50, 50), // red
896 QColor(255, 220, 0), // yellow
897 QColor( 50, 255, 50), // green
898 QColor( 0, 220, 255), // cyan
899 QColor(220, 50, 255), // magenta
900 };
901
902 if (twinkleTick < kSteps) {
903 set_color(twinkleVolumeName, kFlash[twinkleTick]);
904 ++twinkleTick;
905 } else {
906 twinkleTimer->stop();
907 G4Ttree_item* item = findTreeItem(twinkleVolumeName);
908 if (item) {
909 item->set_color(twinkleSavedColor);
910 item->set_opacity(twinkleSavedOpacity);
911 }
912 const double r = twinkleSavedColor.redF();
913 const double g = twinkleSavedColor.greenF();
914 const double b = twinkleSavedColor.blueF();
915 const std::string cmd = "/vis/geometry/set/colour " + twinkleVolumeName + " 0 "
916 + std::to_string(r) + " "
917 + std::to_string(g) + " "
918 + std::to_string(b) + " "
919 + std::to_string(twinkleSavedOpacity);
921 }
922}
923
924
925// Open the selected volume in a new viewer window of the same driver type.
926void GTree::inspectVolume() {
927 if (current_volume_name.empty()) return;
928
929 G4UImanager* uim = G4UImanager::GetUIpointer();
930 if (!uim) return;
931
932 // Resolve the active driver and save the viewer name so we can return focus
933 // to the original window after opening the inspect window.
934 std::string driverName = "OGLSQt";
935 std::string originalViewerName;
936 auto* vm = G4VisManager::GetInstance();
937 if (vm) {
938 const auto* viewer = vm->GetCurrentViewer();
939 if (viewer) {
940 originalViewerName = viewer->GetName();
941 const auto* sh = viewer->GetSceneHandler();
942 if (sh) {
943 const auto* gs = sh->GetGraphicsSystem();
944 if (gs) driverName = gs->GetNickname();
945 }
946 }
947 }
948
949 const std::string leafName = G4Ttree_item::vname_from_v4name(current_volume_name);
950
951 // Open a new window, populate it with only the target volume, and flush.
952 // /vis/open creates a new viewer whose scene handler attaches to the current (full) scene.
953 // /vis/scene/create makes a new empty scene but does NOT re-attach the handler.
954 // /vis/sceneHandler/attach must come before /vis/scene/add/volume to wire the new handler
955 // to the new empty scene; otherwise the first flush renders the full detector.
956 uim->ApplyCommand("/vis/open " + driverName);
957 uim->ApplyCommand("/vis/scene/create");
958 uim->ApplyCommand("/vis/sceneHandler/attach");
959 uim->ApplyCommand("/vis/scene/add/volume " + current_volume_name + " -1");
960 uim->ApplyCommand("/vis/viewer/set/background 1 1 1 1");
961 uim->ApplyCommand("/vis/viewer/set/lineSegmentsPerCircle 100");
962 // 2D label: large black text centred near the top of the window.
963 // "! !" tells Geant4 to use the colour and layout set by /vis/set/text*.
964 uim->ApplyCommand("/vis/set/textColour black");
965 uim->ApplyCommand("/vis/set/textLayout centre");
966 uim->ApplyCommand("/vis/scene/add/text2D 0 0.85 36 ! ! " + leafName);
967 uim->ApplyCommand("/vis/viewer/flush");
968
969 // Return Geant4's "current viewer" to the original window so all GUI controls
970 // (camera, scene properties, tree widget) continue to act on the main view.
971 if (!originalViewerName.empty())
972 uim->ApplyCommand("/vis/viewer/select " + originalViewerName);
973}
974
975
976// Warn that /vis/drawLogicalVolume is not yet usable due to a Geant4 11.4.2 TOOLSSG bug.
977void GTree::drawOverlapsWarning() {
978 QMessageBox::warning(this,
979 tr("Not yet implemented"),
980 tr("Draw Logical Overlaps will be implemented when Geant4 fixes the\n"
981 "G4ToolsSGSceneHandler::GetOrCreateNode \"World mis-match\" crash\n"
982 "triggered by /vis/drawLogicalVolume in Geant4 11.4.2."));
983}
984
985
986// Convert slider position to alpha and apply it to the currently selected volume.
987void GTree::onOpacitySliderChanged(int value) {
988 if (current_volume_name.empty())
989 return; // no volume selected
990
991 double opacity = value / 100.0;
992
993 if (opacityLabel) {
994 opacityLabel->setText(QString::number(opacity, 'f', 2));
995 }
996
997 set_opacity(current_volume_name, opacity);
998}
999
1000// Update alpha for a volume while preserving cached RGB components.
1001void GTree::set_opacity(const std::string& volumeName, double opacity) {
1002
1003 // find current color for this volume from our tree model
1004 G4Ttree_item* item = findTreeItem(volumeName);
1005 if (!item) return;
1006
1007 QColor c = item->get_color();
1008 double r = c.redF();
1009 double g = c.greenF();
1010 double b = c.blueF();
1011
1012 // Geant4: r g b alpha
1013 std::string command = "/vis/geometry/set/colour " + volumeName + " 0 "
1014 + std::to_string(r) + " "
1015 + std::to_string(g) + " "
1016 + std::to_string(b) + " "
1017 + std::to_string(opacity);
1018
1019 // Keep the model synchronized with the command we are sending.
1020 item->set_color(c);
1021 item->set_opacity(opacity);
1022
1024}
Lightweight per-volume record used by GTree to populate the UI.
Definition gtree.h:43
std::string get_solidsOpr() const
Return the boolean solid operation descriptor.
Definition gtree.h:219
std::string get_motherVolume() const
Return the mother volume name.
Definition gtree.h:217
static std::string vname_from_v4name(std::string v4name)
Extract the "leaf" volume name from a full volume name.
Definition gtree.cc:355
double get_opacity() const
Return the cached opacity (alpha) in [0,1].
Definition gtree.h:182
std::string get_volDescription() const
Return the volume description.
Definition gtree.h:221
std::string get_rotation() const
Return the placement rotation string with units.
Definition gtree.h:215
double get_density() const
Return the density, computing it on first request.
Definition gtree.h:197
void set_color(const QColor &c)
Update the cached color.
Definition gtree.h:237
void set_opacity(double opacity)
Update the cached opacity.
Definition gtree.h:243
double get_volume() const
Return the volume, computing it on first request.
Definition gtree.h:191
bool get_visibility() const
Return the cached visibility state.
Definition gtree.h:176
double get_mass() const
Return the mass, computing it on first request.
Definition gtree.h:185
QColor get_color() const
Return the cached RGB color.
Definition gtree.h:173
std::string get_solidType() const
Return the solid type string (e.g. "G4Box").
Definition gtree.h:209
static std::string system_from_v4name(std::string v4name)
Extract the system name from a full volume name.
Definition gtree.cc:361
std::string get_position() const
Return the placement position string with units.
Definition gtree.h:213
std::string get_parameters() const
Return the solid parameters string with units.
Definition gtree.h:211
std::string get_material() const
Return the cached material name.
Definition gtree.h:203
G4Ttree_item(G4Volume *g4volume, const GVolume *gvolume=nullptr)
Construct a cached record for a single geometry volume.
Definition gtree.cc:274
std::string get_mother() const
Return the cached mother name.
Definition gtree.h:170
G4VSolid * getSolid() const noexcept
G4VPhysicalVolume * getPhysical() const noexcept
G4LogicalVolume * getLogical() const noexcept
GBase(const std::shared_ptr< GOptions > &gopt, std::string logger_name="")
std::shared_ptr< GLogger > log
GTree(const std::shared_ptr< GOptions > &gopt, std::unordered_map< std::string, G4Volume * > g4volumes_map, std::unordered_map< std::string, const GVolume * > gvolumes_map={}, QWidget *parent=nullptr)
Construct the geometry tree widget.
Definition gtree.cc:368
std::string getDescription() const
std::string getRot() const
std::string getPos() const
std::string getParameters() const
std::string getType() const
std::string getMotherName() const
std::string getSolidsOpr() const
#define SFUNCTION_NAME
NORMAL
#define ROOTWORLDGVOLUMENAME
#define MOTHEROFUSALL
Option-set definition entry point for the GTree module.
constexpr const char * GTREE_LOGGER
#define UNINITIALIZEDSTRINGQUANTITY
void apply_uimanager_commands(const std::string &commands)
vector< std::string > getStringVectorFromString(const std::string &input)