src/G4OCCTAssemblyVolume.cc
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: LGPL-2.1-or-later | ||
| 2 | // Copyright (C) 2026 G4OCCT Contributors | ||
| 3 | |||
| 4 | /// @file G4OCCTAssemblyVolume.cc | ||
| 5 | /// @brief Implementation of G4OCCTAssemblyVolume. | ||
| 6 | |||
| 7 | #include "G4OCCT/G4OCCTAssemblyVolume.hh" | ||
| 8 | |||
| 9 | #include "G4OCCT/G4OCCTSensitiveDetectorMap.hh" | ||
| 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 | |||
| 49 | namespace { | ||
| 50 | |||
| 51 | /// Convert an OCCT @p trsf to a Geant4 rotation + translation pair. | ||
| 52 | /// STEP/OCCT and Geant4 both use millimetres; no unit conversion is needed. | ||
| 53 | 24 | std::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 | 48 | G4RotationMatrix rot(CLHEP::HepRep3x3(trsf.Value(1, 1), trsf.Value(1, 2), trsf.Value(1, 3), | |
| 57 | 24 | trsf.Value(2, 1), trsf.Value(2, 2), trsf.Value(2, 3), | |
| 58 | 24 | trsf.Value(3, 1), trsf.Value(3, 2), trsf.Value(3, 3))); | |
| 59 |
4/8✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 24 times.
✗ Branch 7 not taken.
|
24 | G4ThreeVector trans(trsf.Value(1, 4), trsf.Value(2, 4), trsf.Value(3, 4)); |
| 60 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | return {rot, trans}; |
| 61 | 24 | } | |
| 62 | |||
| 63 | /// Compose a TopLoc_Location into a gp_Trsf by walking the datum-power chain. | ||
| 64 | /// | ||
| 65 | /// This follows the approach documented in docs/step_assembly_import.md §4.3. | ||
| 66 | /// For a simple (non-compound) location with power ±1 the loop runs exactly | ||
| 67 | /// once. Compound locations with power > 1 are correctly handled by repeated | ||
| 68 | /// application of the datum transformation. | ||
| 69 | 4 | gp_Trsf LocationToTrsf(const TopLoc_Location& loc) { | |
| 70 | 4 | gp_Trsf result; // identity by default | |
| 71 |
5/8✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
|
8 | for (TopLoc_Location cursor = loc; !cursor.IsIdentity(); cursor = cursor.NextLocation()) { |
| 72 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | gp_Trsf datum = cursor.FirstDatum()->Trsf(); |
| 73 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | Standard_Integer power = cursor.FirstPower(); |
| 74 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (power < 0) { |
| 75 | ✗ | datum.Invert(); | |
| 76 | ✗ | power = -power; | |
| 77 | ✗ | } | |
| 78 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | gp_Trsf step; // identity |
| 79 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
|
8 | for (Standard_Integer k = 0; k < power; ++k) { |
| 80 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | step.Multiply(datum); |
| 81 | 4 | } | |
| 82 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | result.Multiply(step); |
| 83 | 4 | } | |
| 84 | 4 | return result; | |
| 85 | ✗ | } | |
| 86 | |||
| 87 | /// Retrieve the XDE name attribute for @p label, or return an empty string. | ||
| 88 | 44 | G4String GetLabelName(const TDF_Label& label) { | |
| 89 | 44 | Handle(TDataStd_Name) nameAttr; | |
| 90 |
3/6✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44 times.
✗ Branch 5 not taken.
|
44 | if (label.FindAttribute(TDataStd_Name::GetID(), nameAttr)) { |
| 91 |
3/6✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44 times.
✗ Branch 5 not taken.
|
44 | TCollection_AsciiString ascii(nameAttr->Get()); |
| 92 |
1/2✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
|
44 | return G4String(ascii.ToCString()); |
| 93 | 44 | } | |
| 94 | ✗ | return G4String{}; | |
| 95 | 44 | } | |
| 96 | |||
| 97 | /// Read the material name attached to @p label via the XDE material tool. | ||
| 98 | /// Returns an empty string when no material attribute is found. | ||
| 99 | 22 | G4String GetMaterialName(const TDF_Label& label, const Handle(XCAFDoc_MaterialTool) & matTool) { | |
| 100 | 22 | Handle(TCollection_HAsciiString) matName; | |
| 101 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | Handle(TCollection_HAsciiString) matDescription; |
| 102 | 22 | Standard_Real density = 0.0; | |
| 103 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | Handle(TCollection_HAsciiString) densityName; |
| 104 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | Handle(TCollection_HAsciiString) densityValType; |
| 105 |
3/6✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 22 times.
|
22 | if (matTool->GetMaterial(label, matName, matDescription, density, densityName, densityValType)) { |
| 106 | ✗ | if (!matName.IsNull()) { | |
| 107 | ✗ | return G4String(matName->ToCString()); | |
| 108 | } | ||
| 109 | ✗ | } | |
| 110 | 22 | return G4String{}; | |
| 111 | 22 | } | |
| 112 | |||
| 113 | /// Recenter @p shape by moving its bounding-box centroid to the OCCT origin | ||
| 114 | /// using Strategy C from docs/reference_position.md (shape.Moved(), no BRep copy). | ||
| 115 | /// | ||
| 116 | /// On return @p centroid holds the location of the centroid in the original | ||
| 117 | /// shape frame (i.e. the opposite of the applied recentering translation). | ||
| 118 | /// Callers must compose this offset into the placement transformation: | ||
| 119 | /// T_eff = composedTrsf * Translate(centroid) | ||
| 120 | 22 | TopoDS_Shape RecenterShape(const TopoDS_Shape& shape, gp_Vec& centroid) { | |
| 121 | 22 | Bnd_Box bbox; | |
| 122 | 22 | BRepBndLib::AddOptimal(shape, bbox, Standard_False); | |
| 123 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (bbox.IsVoid()) { |
| 124 | ✗ | centroid = gp_Vec(0.0, 0.0, 0.0); | |
| 125 | ✗ | return shape; | |
| 126 | } | ||
| 127 | 22 | Standard_Real xMin = 0.0, yMin = 0.0, zMin = 0.0; | |
| 128 | 22 | Standard_Real xMax = 0.0, yMax = 0.0, zMax = 0.0; | |
| 129 | 22 | bbox.Get(xMin, yMin, zMin, xMax, yMax, zMax); | |
| 130 | |||
| 131 | 22 | centroid = gp_Vec(0.5 * (xMin + xMax), 0.5 * (yMin + yMax), 0.5 * (zMin + zMax)); | |
| 132 | |||
| 133 | 22 | gp_Trsf centerTrsf; | |
| 134 | 22 | centerTrsf.SetTranslation(gp_Vec(-centroid.X(), -centroid.Y(), -centroid.Z())); | |
| 135 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | return shape.Moved(TopLoc_Location(centerTrsf)); |
| 136 | 22 | } | |
| 137 | |||
| 138 | /// Make a name unique within @p usedNames by appending `_1`, `_2`, … as needed. | ||
| 139 | /// | ||
| 140 | /// On the first call with a given @p name the name is returned unchanged. | ||
| 141 | /// On subsequent calls a numeric suffix is appended to avoid duplicates. | ||
| 142 | 22 | G4String MakeUniqueName(const G4String& name, std::map<G4String, int>& usedNames) { | |
| 143 | 28 | auto [it, inserted] = usedNames.emplace(name, 0); | |
| 144 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 5 times.
|
22 | if (inserted) { |
| 145 | 17 | return name; | |
| 146 | } | ||
| 147 | // Generate a unique candidate by incrementing the per-base counter. | ||
| 148 | 5 | G4String candidate; | |
| 149 | 5 | do { | |
| 150 | 6 | ++(it->second); | |
| 151 |
2/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
|
6 | candidate = name + "_" + std::to_string(it->second); |
| 152 |
3/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 time.
|
6 | } while (usedNames.count(candidate) > 0); |
| 153 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | usedNames.emplace(candidate, 0); |
| 154 | 5 | return candidate; | |
| 155 | 27 | } | |
| 156 | |||
| 157 | /// Return a stable string key for @p label suitable for use in a std::map. | ||
| 158 | /// | ||
| 159 | /// `TDF_Label` does not define `operator<`, so it cannot be used directly as a | ||
| 160 | /// std::map key. The colon-separated tag path (e.g. "0:1:1:2") is unique | ||
| 161 | /// within a document and provides a stable ordering. | ||
| 162 | 24 | std::string LabelKey(const TDF_Label& label) { | |
| 163 | 24 | TCollection_AsciiString entry; | |
| 164 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | TDF_Tool::Entry(label, entry); |
| 165 |
2/4✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
|
24 | return std::string(entry.ToCString()); |
| 166 | 24 | } | |
| 167 | |||
| 168 | } // namespace | ||
| 169 | |||
| 170 | // ── BuildContext ────────────────────────────────────────────────────────────── | ||
| 171 | |||
| 172 | /// State threaded through the recursive XDE label traversal. | ||
| 173 | struct G4OCCTAssemblyVolume::BuildContext { | ||
| 174 | /// OCCT shape tool for the XDE document. | ||
| 175 | Handle(XCAFDoc_ShapeTool) shapeTool; | ||
| 176 | /// OCCT material tool for the XDE document. | ||
| 177 | Handle(XCAFDoc_MaterialTool) matTool; | ||
| 178 | /// User-supplied material map. | ||
| 179 | const G4OCCTMaterialMap& materialMap; | ||
| 180 | |||
| 181 | /// Prototype map: XDE label entry string → (logical volume, centroid in original shape frame). | ||
| 182 | /// The centroid is stored alongside the logical volume so that repeated instances can compose | ||
| 183 | /// the correct recentering offset without recomputing the bounding box. | ||
| 184 | std::map<std::string, std::pair<G4OCCTLogicalVolume*, gp_Vec>> prototypeMap; | ||
| 185 | /// Names already used by logical volumes; used to disambiguate duplicates. | ||
| 186 | std::map<G4String, int> usedNames; | ||
| 187 | /// Flat collection of all created logical volumes (output to caller). | ||
| 188 | std::map<G4String, G4OCCTLogicalVolume*>* logicalVolumes{nullptr}; | ||
| 189 | }; | ||
| 190 | |||
| 191 | // ── Recursive import ────────────────────────────────────────────────────────── | ||
| 192 | |||
| 193 | 26 | void G4OCCTAssemblyVolume::ImportLabel(const TDF_Label& label, G4AssemblyVolume* parentAssembly, | |
| 194 | const gp_Trsf& composedTrsf, BuildContext& ctx) { | ||
| 195 | 26 | const Handle(XCAFDoc_ShapeTool) & shapeTool = ctx.shapeTool; | |
| 196 | |||
| 197 | // ── Assembly label: recurse into components ───────────────────────────────── | ||
| 198 |
2/2✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
|
26 | if (shapeTool->IsAssembly(label)) { |
| 199 | 2 | TDF_LabelSequence components; | |
| 200 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | shapeTool->GetComponents(label, components, /*recursive=*/Standard_False); |
| 201 | |||
| 202 |
3/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2 times.
|
6 | for (Standard_Integer i = 1; i <= components.Length(); ++i) { |
| 203 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | 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 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | TopLoc_Location compLoc; |
| 209 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | Handle(XCAFDoc_Location) locAttr; |
| 210 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | if (comp.FindAttribute(XCAFDoc_Location::GetID(), locAttr)) { |
| 211 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | compLoc = locAttr->Get(); |
| 212 | 4 | } | |
| 213 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | gp_Trsf compTrsf = LocationToTrsf(compLoc); |
| 214 | |||
| 215 | // Compose: first apply component's local placement, then the parent's. | ||
| 216 | 4 | gp_Trsf childTrsf = composedTrsf; | |
| 217 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | childTrsf.Multiply(compTrsf); |
| 218 | |||
| 219 | // Resolve reference to the actual shape label, then recurse. | ||
| 220 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | TDF_Label referred; |
| 221 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | if (shapeTool->GetReferredShape(comp, referred)) { |
| 222 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | ImportLabel(referred, parentAssembly, childTrsf, ctx); |
| 223 | 4 | } else { | |
| 224 | ✗ | G4Exception("G4OCCTAssemblyVolume::ImportLabel", "G4OCCT_Asm001", JustWarning, | |
| 225 | "Assembly component has no referred shape; skipping."); | ||
| 226 | } | ||
| 227 | 4 | } | |
| 228 | return; | ||
| 229 | 2 | } | |
| 230 | |||
| 231 | // ── Simple shape label: create or reuse a logical volume ──────────────────── | ||
| 232 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
|
24 | if (shapeTool->IsSimpleShape(label)) { |
| 233 | 24 | const std::string labelKey = LabelKey(label); | |
| 234 | |||
| 235 | 24 | G4OCCTLogicalVolume* lv = nullptr; | |
| 236 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | auto protoIt = ctx.prototypeMap.find(labelKey); |
| 237 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | gp_Vec centroid; |
| 238 | |||
| 239 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 2 times.
|
24 | 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 | 2 | lv = protoIt->second.first; | |
| 243 | 2 | centroid = protoIt->second.second; | |
| 244 | 2 | } else { | |
| 245 | // First encounter: build solid + logical volume. | ||
| 246 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | TopoDS_Shape rawShape = shapeTool->GetShape(label); |
| 247 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
|
22 | 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 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | G4String matKey = GetMaterialName(label, ctx.matTool); |
| 257 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (matKey.empty()) { |
| 258 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | matKey = GetLabelName(label); |
| 259 | 22 | } | |
| 260 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | 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 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | G4Material* material = ctx.materialMap.Resolve(matKey); |
| 267 | |||
| 268 | // Determine unique part name. | ||
| 269 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | G4String rawName = GetLabelName(label); |
| 270 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (rawName.empty()) { |
| 271 | ✗ | rawName = "Part"; | |
| 272 | ✗ | } | |
| 273 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | 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 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | TopoDS_Shape centeredShape = RecenterShape(rawShape, centroid); |
| 279 | |||
| 280 |
4/10✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
22 | auto* solid = new G4OCCTSolid(uniqueName + "_solid", centeredShape); |
| 281 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | 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 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | ctx.prototypeMap.emplace(labelKey, std::make_pair(lv, centroid)); |
| 286 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | if (ctx.logicalVolumes) { |
| 287 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | (*ctx.logicalVolumes)[uniqueName] = lv; |
| 288 | 22 | } | |
| 289 | 22 | } | |
| 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 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | gp_Trsf recenterComp; |
| 300 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | recenterComp.SetTranslation(centroid); |
| 301 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | const gp_Trsf effectiveTrsf = composedTrsf.Multiplied(recenterComp); |
| 302 | |||
| 303 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | auto [rot, trans] = TrsfToG4(effectiveTrsf); |
| 304 |
2/4✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
|
48 | parentAssembly->AddPlacedVolume(lv, trans, &rot); |
| 305 | return; | ||
| 306 | 24 | } | |
| 307 | |||
| 308 | // Unrecognised label category (e.g. free-shape reference) — skip silently. | ||
| 309 | 26 | } | |
| 310 | |||
| 311 | // ── Factory ─────────────────────────────────────────────────────────────────── | ||
| 312 | |||
| 313 | 16 | G4OCCTAssemblyVolume* G4OCCTAssemblyVolume::FromSTEP(const std::string& path, | |
| 314 | const G4OCCTMaterialMap& materialMap) { | ||
| 315 | // ── Open XDE document and read the STEP file ───────────────────────────────── | ||
| 316 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
19 | Handle(TDocStd_Application) app = new TDocStd_Application; |
| 317 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | Handle(TDocStd_Document) doc; |
| 318 |
3/6✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
|
16 | app->NewDocument("MDTV-CAF", doc); |
| 319 | |||
| 320 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | STEPCAFControl_Reader cafReader; |
| 321 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | cafReader.SetNameMode(Standard_True); |
| 322 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | cafReader.SetMatMode(Standard_True); |
| 323 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | cafReader.SetColorMode(Standard_True); |
| 324 | |||
| 325 |
3/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 2 times.
|
16 | if (cafReader.ReadFile(path.c_str()) != IFSelect_RetDone) { |
| 326 |
4/8✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
2 | throw std::runtime_error("G4OCCTAssemblyVolume::FromSTEP: failed to read STEP file: " + path); |
| 327 | } | ||
| 328 |
4/6✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✓ Branch 5 taken 1 time.
|
14 | if (!cafReader.Transfer(doc)) { |
| 329 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
2 | throw std::runtime_error("G4OCCTAssemblyVolume::FromSTEP: failed to transfer STEP document: " + |
| 330 | 1 | path); | |
| 331 | } | ||
| 332 | |||
| 333 |
3/6✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
|
13 | Handle(XCAFDoc_ShapeTool) shapeTool = XCAFDoc_DocumentTool::ShapeTool(doc->Main()); |
| 334 |
3/6✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
|
13 | Handle(XCAFDoc_MaterialTool) matTool = XCAFDoc_DocumentTool::MaterialTool(doc->Main()); |
| 335 | |||
| 336 | // ── Collect top-level (free) shape labels ──────────────────────────────────── | ||
| 337 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | TDF_LabelSequence freeShapes; |
| 338 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
|
13 | shapeTool->GetFreeShapes(freeShapes); |
| 339 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
|
13 | 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 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
|
13 | auto* result = new G4OCCTAssemblyVolume; |
| 346 | |||
| 347 | 13 | BuildContext ctx{ | |
| 348 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | .shapeTool = shapeTool, |
| 349 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | .matTool = matTool, |
| 350 | 13 | .materialMap = materialMap, | |
| 351 | 13 | .logicalVolumes = &result->fLogicalVolumes, | |
| 352 | 26 | }; | |
| 353 | |||
| 354 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | const gp_Trsf identity; // starts as the identity transformation |
| 355 |
3/4✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 13 times.
|
35 | for (Standard_Integer i = 1; i <= freeShapes.Length(); ++i) { |
| 356 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | ImportLabel(freeShapes.Value(i), result, identity, ctx); |
| 357 | 22 | } | |
| 358 | |||
| 359 | 13 | return result; | |
| 360 | 19 | } | |
| 361 | |||
| 362 | // ── SD map application ──────────────────────────────────────────────────────── | ||
| 363 | |||
| 364 | 4 | std::size_t G4OCCTAssemblyVolume::ApplySDMap(const G4OCCTSensitiveDetectorMap& sdMap) { | |
| 365 | 4 | std::size_t count = 0; | |
| 366 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4 times.
|
13 | for (auto& [name, lv] : fLogicalVolumes) { |
| 367 | 18 | G4VSensitiveDetector* sd = sdMap.Resolve(name); | |
| 368 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 7 times.
|
9 | if (sd != nullptr) { |
| 369 | 7 | lv->SetSensitiveDetector(sd); | |
| 370 | 7 | ++count; | |
| 371 | 7 | } | |
| 372 | } | ||
| 373 | 4 | return count; | |
| 374 | } | ||
| 375 |