G4OCCT 0.1.0
Geant4 interface to Open CASCADE Technology (OCCT) geometry definitions
Loading...
Searching...
No Matches
G4OCCTAssemblyVolume.cc
Go to the documentation of this file.
1// SPDX-License-Identifier: LGPL-2.1-or-later
2// Copyright (C) 2026 G4OCCT Contributors
3
6
8
10#include "G4OCCT/G4OCCTSolid.hh"
11
12// OCCT BRep / geometry
13#include <BRepBndLib.hxx>
14#include <Bnd_Box.hxx>
15#include <TopLoc_Location.hxx>
16#include <TopoDS_Shape.hxx>
17#include <gp_Trsf.hxx>
18#include <gp_Vec.hxx>
19
20// OCCT XDE
21#include <IFSelect_ReturnStatus.hxx>
22#include <STEPCAFControl_Reader.hxx>
23#include <TDataStd_Name.hxx>
24#include <TDF_Label.hxx>
25#include <TDF_LabelSequence.hxx>
26#include <TDF_Tool.hxx>
27#include <TDocStd_Application.hxx>
28#include <TDocStd_Document.hxx>
29#include <XCAFDoc_DocumentTool.hxx>
30#include <XCAFDoc_Location.hxx>
31#include <XCAFDoc_MaterialTool.hxx>
32#include <XCAFDoc_ShapeTool.hxx>
33
34// Geant4
35#include <G4Exception.hh>
36#include <G4RotationMatrix.hh>
37#include <G4ThreeVector.hh>
38
39// CLHEP
40#include <CLHEP/Vector/RotationInterfaces.h>
41
42#include <map>
43#include <stdexcept>
44#include <string>
45#include <utility>
46
47// ── Internal helpers ──────────────────────────────────────────────────────────
48
49namespace {
50
53std::pair<G4RotationMatrix, G4ThreeVector> TrsfToG4(const gp_Trsf& trsf) {
54 // CLHEP::HepRotation's 9-argument (row-major) constructor is protected.
55 // Use the public HepRep3x3 constructor instead.
56 G4RotationMatrix rot(CLHEP::HepRep3x3(trsf.Value(1, 1), trsf.Value(1, 2), trsf.Value(1, 3),
57 trsf.Value(2, 1), trsf.Value(2, 2), trsf.Value(2, 3),
58 trsf.Value(3, 1), trsf.Value(3, 2), trsf.Value(3, 3)));
59 G4ThreeVector trans(trsf.Value(1, 4), trsf.Value(2, 4), trsf.Value(3, 4));
60 return {rot, trans};
61}
62
69gp_Trsf LocationToTrsf(const TopLoc_Location& loc) {
70 gp_Trsf result; // identity by default
71 for (TopLoc_Location cursor = loc; !cursor.IsIdentity(); cursor = cursor.NextLocation()) {
72 gp_Trsf datum = cursor.FirstDatum()->Trsf();
73 Standard_Integer power = cursor.FirstPower();
74 if (power < 0) {
75 datum.Invert();
76 power = -power;
77 }
78 gp_Trsf step; // identity
79 for (Standard_Integer k = 0; k < power; ++k) {
80 step.Multiply(datum);
81 }
82 result.Multiply(step);
83 }
84 return result;
85}
86
88G4String GetLabelName(const TDF_Label& label) {
89 Handle(TDataStd_Name) nameAttr;
90 if (label.FindAttribute(TDataStd_Name::GetID(), nameAttr)) {
91 TCollection_AsciiString ascii(nameAttr->Get());
92 return G4String(ascii.ToCString());
93 }
94 return G4String{};
95}
96
99G4String GetMaterialName(const TDF_Label& label, const Handle(XCAFDoc_MaterialTool) & matTool) {
100 Handle(TCollection_HAsciiString) matName;
101 Handle(TCollection_HAsciiString) matDescription;
102 Standard_Real density = 0.0;
103 Handle(TCollection_HAsciiString) densityName;
104 Handle(TCollection_HAsciiString) densityValType;
105 if (matTool->GetMaterial(label, matName, matDescription, density, densityName, densityValType)) {
106 if (!matName.IsNull()) {
107 return G4String(matName->ToCString());
108 }
109 }
110 return G4String{};
111}
112
120TopoDS_Shape RecenterShape(const TopoDS_Shape& shape, gp_Vec& centroid) {
121 Bnd_Box bbox;
122 BRepBndLib::AddOptimal(shape, bbox, Standard_False);
123 if (bbox.IsVoid()) {
124 centroid = gp_Vec(0.0, 0.0, 0.0);
125 return shape;
126 }
127 Standard_Real xMin = 0.0, yMin = 0.0, zMin = 0.0;
128 Standard_Real xMax = 0.0, yMax = 0.0, zMax = 0.0;
129 bbox.Get(xMin, yMin, zMin, xMax, yMax, zMax);
130
131 centroid = gp_Vec(0.5 * (xMin + xMax), 0.5 * (yMin + yMax), 0.5 * (zMin + zMax));
132
133 gp_Trsf centerTrsf;
134 centerTrsf.SetTranslation(gp_Vec(-centroid.X(), -centroid.Y(), -centroid.Z()));
135 return shape.Moved(TopLoc_Location(centerTrsf));
136}
137
142G4String MakeUniqueName(const G4String& name, std::map<G4String, int>& usedNames) {
143 auto [it, inserted] = usedNames.emplace(name, 0);
144 if (inserted) {
145 return name;
146 }
147 // Generate a unique candidate by incrementing the per-base counter.
148 G4String candidate;
149 do {
150 ++(it->second);
151 candidate = name + "_" + std::to_string(it->second);
152 } while (usedNames.count(candidate) > 0);
153 usedNames.emplace(candidate, 0);
154 return candidate;
155}
156
162std::string LabelKey(const TDF_Label& label) {
163 TCollection_AsciiString entry;
164 TDF_Tool::Entry(label, entry);
165 return std::string(entry.ToCString());
166}
167
168} // namespace
169
170// ── BuildContext ──────────────────────────────────────────────────────────────
171
175 Handle(XCAFDoc_ShapeTool) shapeTool;
177 Handle(XCAFDoc_MaterialTool) matTool;
180
184 std::map<std::string, std::pair<G4OCCTLogicalVolume*, gp_Vec>> prototypeMap;
186 std::map<G4String, int> usedNames;
188 std::map<G4String, G4OCCTLogicalVolume*>* logicalVolumes{nullptr};
189};
190
191// ── Recursive import ──────────────────────────────────────────────────────────
192
193void G4OCCTAssemblyVolume::ImportLabel(const TDF_Label& label, G4AssemblyVolume* parentAssembly,
194 const gp_Trsf& composedTrsf, BuildContext& ctx) {
195 const Handle(XCAFDoc_ShapeTool) & shapeTool = ctx.shapeTool;
196
197 // ── Assembly label: recurse into components ─────────────────────────────────
198 if (shapeTool->IsAssembly(label)) {
199 TDF_LabelSequence components;
200 shapeTool->GetComponents(label, components, /*recursive=*/Standard_False);
201
202 for (Standard_Integer i = 1; i <= components.Length(); ++i) {
203 const TDF_Label& comp = components.Value(i); // always a reference label
204
205 // Extract the component's placement in the parent frame.
206 // FindAttribute returns false if no location is stored; in that case
207 // compLoc remains default-constructed (identity).
208 TopLoc_Location compLoc;
209 Handle(XCAFDoc_Location) locAttr;
210 if (comp.FindAttribute(XCAFDoc_Location::GetID(), locAttr)) {
211 compLoc = locAttr->Get();
212 }
213 gp_Trsf compTrsf = LocationToTrsf(compLoc);
214
215 // Compose: first apply component's local placement, then the parent's.
216 gp_Trsf childTrsf = composedTrsf;
217 childTrsf.Multiply(compTrsf);
218
219 // Resolve reference to the actual shape label, then recurse.
220 TDF_Label referred;
221 if (shapeTool->GetReferredShape(comp, referred)) {
222 ImportLabel(referred, parentAssembly, childTrsf, ctx);
223 } else {
224 G4Exception("G4OCCTAssemblyVolume::ImportLabel", "G4OCCT_Asm001", JustWarning,
225 "Assembly component has no referred shape; skipping.");
226 }
227 }
228 return;
229 }
230
231 // ── Simple shape label: create or reuse a logical volume ────────────────────
232 if (shapeTool->IsSimpleShape(label)) {
233 const std::string labelKey = LabelKey(label);
234
235 G4OCCTLogicalVolume* lv = nullptr;
236 auto protoIt = ctx.prototypeMap.find(labelKey);
237 gp_Vec centroid;
238
239 if (protoIt != ctx.prototypeMap.end()) {
240 // Reuse the existing logical volume — instance sharing.
241 // Retrieve the cached centroid to avoid recomputing the bounding box.
242 lv = protoIt->second.first;
243 centroid = protoIt->second.second;
244 } else {
245 // First encounter: build solid + logical volume.
246 TopoDS_Shape rawShape = shapeTool->GetShape(label);
247 if (rawShape.IsNull()) {
248 G4Exception("G4OCCTAssemblyVolume::ImportLabel", "G4OCCT_Asm002", JustWarning,
249 "Simple-shape label has a null OCCT shape; skipping.");
250 return;
251 }
252
253 // Resolve material by XDE material attribute, falling back to the part
254 // (label) name when no material attribute is present in the STEP file.
255 // This accommodates STEP writers that do not write material attributes.
256 G4String matKey = GetMaterialName(label, ctx.matTool);
257 if (matKey.empty()) {
258 matKey = GetLabelName(label);
259 }
260 if (matKey.empty()) {
261 G4Exception("G4OCCTAssemblyVolume::ImportLabel", "G4OCCT_Asm003", FatalException,
262 "Simple-shape label carries neither a STEP material attribute nor a part name. "
263 "All shapes must have materials registered in G4OCCTMaterialMap.");
264 return; // unreachable; silences compiler warning
265 }
266 G4Material* material = ctx.materialMap.Resolve(matKey);
267
268 // Determine unique part name.
269 G4String rawName = GetLabelName(label);
270 if (rawName.empty()) {
271 rawName = "Part";
272 }
273 G4String uniqueName = MakeUniqueName(rawName, ctx.usedNames);
274
275 // Recenter the shape per docs/reference_position.md Strategy C.
276 // The centroid vector records the old centroid position in the original
277 // shape frame; it is composed into the placement below.
278 TopoDS_Shape centeredShape = RecenterShape(rawShape, centroid);
279
280 auto* solid = new G4OCCTSolid(uniqueName + "_solid", centeredShape);
281 lv = new G4OCCTLogicalVolume(solid, material, uniqueName, centeredShape);
282
283 // Cache: store both the logical volume and its centroid so that repeated
284 // instances can reuse the centroid without recomputing the bounding box.
285 ctx.prototypeMap.emplace(labelKey, std::make_pair(lv, centroid));
286 if (ctx.logicalVolumes) {
287 (*ctx.logicalVolumes)[uniqueName] = lv;
288 }
289 }
290
291 // Absorb the recentering offset into composedTrsf so that the solid's
292 // local origin (now the centroid) is placed at the correct world position.
293 //
294 // After recentering, a point at the solid's local origin corresponds to
295 // the centroid in the original frame. The effective placement transform
296 // must first shift by `centroid` (from recentered → original local frame)
297 // and then apply `composedTrsf` (original local → world):
298 // T_eff = composedTrsf * Translate(centroid)
299 gp_Trsf recenterComp;
300 recenterComp.SetTranslation(centroid);
301 const gp_Trsf effectiveTrsf = composedTrsf.Multiplied(recenterComp);
302
303 auto [rot, trans] = TrsfToG4(effectiveTrsf);
304 parentAssembly->AddPlacedVolume(lv, trans, &rot);
305 return;
306 }
307
308 // Unrecognised label category (e.g. free-shape reference) — skip silently.
309}
310
311// ── Factory ───────────────────────────────────────────────────────────────────
312
314 const G4OCCTMaterialMap& materialMap) {
315 // ── Open XDE document and read the STEP file ─────────────────────────────────
316 Handle(TDocStd_Application) app = new TDocStd_Application;
317 Handle(TDocStd_Document) doc;
318 app->NewDocument("MDTV-CAF", doc);
319
320 STEPCAFControl_Reader cafReader;
321 cafReader.SetNameMode(Standard_True);
322 cafReader.SetMatMode(Standard_True);
323 cafReader.SetColorMode(Standard_True);
324
325 if (cafReader.ReadFile(path.c_str()) != IFSelect_RetDone) {
326 throw std::runtime_error("G4OCCTAssemblyVolume::FromSTEP: failed to read STEP file: " + path);
327 }
328 if (!cafReader.Transfer(doc)) {
329 throw std::runtime_error("G4OCCTAssemblyVolume::FromSTEP: failed to transfer STEP document: " +
330 path);
331 }
332
333 Handle(XCAFDoc_ShapeTool) shapeTool = XCAFDoc_DocumentTool::ShapeTool(doc->Main());
334 Handle(XCAFDoc_MaterialTool) matTool = XCAFDoc_DocumentTool::MaterialTool(doc->Main());
335
336 // ── Collect top-level (free) shape labels ────────────────────────────────────
337 TDF_LabelSequence freeShapes;
338 shapeTool->GetFreeShapes(freeShapes);
339 if (freeShapes.IsEmpty()) {
340 throw std::runtime_error(
341 "G4OCCTAssemblyVolume::FromSTEP: STEP file contains no top-level shapes: " + path);
342 }
343
344 // ── Build the Geant4 volume hierarchy ────────────────────────────────────────
345 auto* result = new G4OCCTAssemblyVolume;
346
347 BuildContext ctx{
348 .shapeTool = shapeTool,
349 .matTool = matTool,
350 .materialMap = materialMap,
351 .logicalVolumes = &result->fLogicalVolumes,
352 };
353
354 const gp_Trsf identity; // starts as the identity transformation
355 for (Standard_Integer i = 1; i <= freeShapes.Length(); ++i) {
356 ImportLabel(freeShapes.Value(i), result, identity, ctx);
357 }
358
359 return result;
360}
361
362// ── SD map application ────────────────────────────────────────────────────────
363
365 std::size_t count = 0;
366 for (auto& [name, lv] : fLogicalVolumes) {
367 G4VSensitiveDetector* sd = sdMap.Resolve(name);
368 if (sd != nullptr) {
369 lv->SetSensitiveDetector(sd);
370 ++count;
371 }
372 }
373 return count;
374}
Declaration of G4OCCTAssemblyVolume.
Declaration of G4OCCTSensitiveDetectorMap.
Declaration of G4OCCTSolid.
Extends Geant4's G4AssemblyVolume with an OCCT XDE label reference.
G4OCCTAssemblyVolume()=default
std::size_t ApplySDMap(const G4OCCTSensitiveDetectorMap &sdMap)
static G4OCCTAssemblyVolume * FromSTEP(const std::string &path, const G4OCCTMaterialMap &materialMap)
Extends Geant4's G4LogicalVolume with an associated OCCT shape.
Maps STEP material names to Geant4 G4Material objects.
Maps volume name patterns to Geant4 G4VSensitiveDetector objects.
G4VSensitiveDetector * Resolve(const G4String &volumeName) const
Geant4 solid wrapping an Open CASCADE Technology (OCCT) TopoDS_Shape.
State threaded through the recursive XDE label traversal.
std::map< G4String, G4OCCTLogicalVolume * > * logicalVolumes
Flat collection of all created logical volumes (output to caller).
std::map< G4String, int > usedNames
Names already used by logical volumes; used to disambiguate duplicates.
Handle(XCAFDoc_ShapeTool) shapeTool
OCCT shape tool for the XDE document.
Handle(XCAFDoc_MaterialTool) matTool
OCCT material tool for the XDE document.
std::map< std::string, std::pair< G4OCCTLogicalVolume *, gp_Vec > > prototypeMap
const G4OCCTMaterialMap & materialMap
User-supplied material map.