Skip to content

Commit 136bb73

Browse files
sawenzelclaude
andcommitted
Fix undefined volumes placed by the CAD converter's name filter
This fixes a problem in o2-cad-to-tgeo's --include-name filter and adds a self-test. - A part shared by an included and an excluded subtree was cached as dropped on its first visit and then placed on its second, so geom.C did not compile. - In the other visit order the part leaked into the excluded subtree. - An empty leaf placed twice left the same dangling placement. - The pruned expansion of a definition now has its own key, a cached definition that produced nothing is no longer placed, and a dangling placement stops the conversion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3fb1976 commit 136bb73

1 file changed

Lines changed: 115 additions & 7 deletions

File tree

‎Detectors/CADSupport/tools/O2_CADtoTGeo.py‎

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,14 +1963,22 @@ def _self_test_assembly(shape_tool, components):
19631963
return label
19641964

19651965

1966-
def _self_test_convert(shape_tool):
1966+
def _self_test_named(shape_tool, label, name: str):
1967+
"""Give an in-memory label the XCAF name a STEP file would carry."""
1968+
from OCC.Core.TDataStd import TDataStd_Name
1969+
TDataStd_Name.Set(label, name)
1970+
return label
1971+
1972+
1973+
def _self_test_convert(shape_tool, name_filter: Optional["NameFilter"] = None):
19671974
"""Run the production traversal over an in-memory assembly and report what it placed.
19681975
19691976
Returns (report, leaf occurrences), where the occurrences are (definition, world transform
19701977
signature) pairs -- measured by walking the emitted graph, not read back out of the rule.
19711978
"""
19721979
reset_graph()
1973-
report = expand_free_shapes(shape_tool, meshparam=None, scale_to_cm=1.0)
1980+
report = expand_free_shapes(shape_tool, meshparam=None, scale_to_cm=1.0,
1981+
name_filter=name_filter)
19741982
leaves = [occ for occ in enumerate_occurrences(placements, top_defs)
19751983
if occ[0] in logical_volumes]
19761984
return report, leaves
@@ -2237,6 +2245,94 @@ def compound_of(*shapes):
22372245
return tally.failures
22382246

22392247

2248+
def _self_test_dangling() -> List[str]:
2249+
"""Placement children that are neither a logical volume nor an assembly."""
2250+
return sorted({child for _parent, child, _trsf in placements
2251+
if child not in logical_volumes and child not in assemblies})
2252+
2253+
2254+
def run_name_filter_self_test() -> int:
2255+
"""Assert that --include-name keeps exactly the matching subtrees, whatever the visit order.
2256+
2257+
Returns the number of failures; prints one line per check.
2258+
"""
2259+
tally = _Checks()
2260+
report = tally.report
2261+
2262+
print("\nName filters: a part shared by included and excluded subtrees")
2263+
2264+
def disc_like(cable_first: bool):
2265+
# A screw used both by a matching cable and by a non-matching bracket, as in the OT disc.
2266+
doc, st = _self_test_shape_tool()
2267+
screw = _self_test_named(st, _self_test_leaf(st, 1.0), "screw")
2268+
flat = _self_test_named(st, _self_test_leaf(st, 2.0), "A-flat print")
2269+
own = _self_test_named(st, _self_test_leaf(st, 3.0), "bracket plate")
2270+
cable = _self_test_named(st, _self_test_assembly(
2271+
st, [(flat, gp_Trsf()), (screw, _self_test_shift(dx=10.0))]), "A-flat cable")
2272+
bracket = _self_test_named(st, _self_test_assembly(
2273+
st, [(screw, gp_Trsf()), (own, _self_test_shift(dx=5.0))]), "bracket")
2274+
parts = [(cable, _self_test_shift(dy=100.0)), (bracket, gp_Trsf())]
2275+
_self_test_assembly(st, parts if cable_first else parts[::-1])
2276+
st.UpdateAssemblies()
2277+
return doc, st
2278+
2279+
include = NameFilter.from_patterns(["A-flat"], [])
2280+
2281+
# --- 1. the shared screw is first met outside the included subtree ------------------------
2282+
_doc, st = disc_like(cable_first=False)
2283+
_rep, occ = _self_test_convert(st, include)
2284+
report(not _self_test_dangling(),
2285+
"no placement points at a volume that was never defined",
2286+
f"dangling: {_self_test_dangling()}")
2287+
report(len(occ) == 2, "the cable's two parts are placed, the bracket's are not",
2288+
f"{len(occ)} placed")
2289+
2290+
# --- 2. the same model visited in the other order gives the same answer -------------------
2291+
_doc, st = disc_like(cable_first=True)
2292+
_rep, occ = _self_test_convert(st, include)
2293+
report(not _self_test_dangling() and len(occ) == 2,
2294+
"visit order does not matter: the screw met first inside the cable is not placed in "
2295+
"the bracket too", f"{len(occ)} placed, dangling: {_self_test_dangling()}")
2296+
2297+
# --- 3. one assembly needed both whole and pruned ------------------------------------------
2298+
_doc, st = _self_test_shape_tool()
2299+
screw = _self_test_named(st, _self_test_leaf(st, 1.0), "screw")
2300+
flat = _self_test_named(st, _self_test_leaf(st, 2.0), "A-flat print")
2301+
module = _self_test_named(st, _self_test_assembly(
2302+
st, [(screw, gp_Trsf()), (flat, _self_test_shift(dx=20.0))]), "module")
2303+
wrap = _self_test_named(st, _self_test_assembly(st, [(module, gp_Trsf())]), "A-flat wrap")
2304+
_self_test_assembly(st, [(module, gp_Trsf()), (wrap, _self_test_shift(dz=50.0))])
2305+
st.UpdateAssemblies()
2306+
_rep, occ = _self_test_convert(st, include)
2307+
report(not _self_test_dangling() and len(occ) == 3,
2308+
"an assembly placed whole inside a match and pruned outside it gives 2 + 1 parts",
2309+
f"{len(occ)} placed, dangling: {_self_test_dangling()}")
2310+
2311+
# --- 4. an empty leaf placed twice is dropped twice, not dangled the second time ----------
2312+
from OCC.Core.BRep import BRep_Builder
2313+
from OCC.Core.TopoDS import TopoDS_Compound
2314+
_doc, st = _self_test_shape_tool()
2315+
comp = TopoDS_Compound()
2316+
BRep_Builder().MakeCompound(comp)
2317+
empty = st.AddShape(comp, False)
2318+
good = _self_test_leaf(st, 3.0)
2319+
_self_test_assembly(st, [(empty, gp_Trsf()), (empty, _self_test_shift(dx=5.0)),
2320+
(good, _self_test_shift(dx=10.0))])
2321+
st.UpdateAssemblies()
2322+
_rep, occ = _self_test_convert(st)
2323+
report(not _self_test_dangling() and len(occ) == 1,
2324+
"an empty leaf instanced twice leaves no placement behind",
2325+
f"{len(occ)} placed, dangling: {_self_test_dangling()}")
2326+
2327+
# --- 5. the negative control: without a filter everything is placed ------------------------
2328+
_doc, st = disc_like(cable_first=False)
2329+
_rep, occ = _self_test_convert(st)
2330+
report(len(occ) == 4, "without a filter all four parts are placed", f"{len(occ)} placed")
2331+
2332+
print(f"\n{tally.checks} checks, {tally.failures} failure(s)")
2333+
return tally.failures
2334+
2335+
22402336
def _recognized_inner_wall(face, rec) -> Optional[bool]:
22412337
"""Decide, by measurement, which side of a RECOGNIZED quadric is outside the solid.
22422338
@@ -3586,11 +3682,17 @@ def expand_definition(
35863682
include_subtree=subtree_included,
35873683
)
35883684

3589-
def_key = f"{def_lid}@{occ_path}" if clip_enabled else def_lid
3590-
if not clip_enabled and def_lid in visited_defs:
3591-
return def_lid
3592-
if not clip_enabled:
3593-
visited_defs.add(def_lid)
3685+
# Outside an included subtree an --include-name filter prunes the definition, so the pruned
3686+
# expansion gets its own key; the whole one keeps the bare label entry.
3687+
pruned = name_filter is not None and name_filter.has_include and not subtree_included
3688+
if clip_enabled:
3689+
def_key = f"{def_lid}@{occ_path}"
3690+
else:
3691+
def_key = f"{def_lid}@pruned" if pruned else def_lid
3692+
if def_key in visited_defs:
3693+
# A definition that produced nothing (pruned away, empty) must not be placed.
3694+
return def_key if (def_key in logical_volumes or def_key in assemblies) else None
3695+
visited_defs.add(def_key)
35943696

35953697
if nm and def_key not in def_names:
35963698
def_names[def_key] = nm
@@ -4000,6 +4102,11 @@ def expand_free_shapes(
40004102
set(logical_volumes))
40014103
report_duplicate_placements(dup_report, def_names)
40024104
verify_placement_invariant(placements, top_defs, set(logical_volumes), emitted)
4105+
dangling = sorted({child for _parent, child, _trsf in placements
4106+
if child not in logical_volumes and child not in assemblies})
4107+
if dangling:
4108+
raise RuntimeError(f"{len(dangling)} placement target(s) were never defined, e.g. "
4109+
f"{dangling[:5]}; geom.C would not compile")
40034110
return dup_report
40044111

40054112

@@ -4637,6 +4744,7 @@ def main():
46374744
+ run_planar_trim_self_test()
46384745
+ run_duplicate_placement_self_test()
46394746
+ run_multibody_leaf_self_test()
4747+
+ run_name_filter_self_test()
46404748
+ run_in_field_media_self_test()
46414749
+ run_bom_token_self_test()) else 0)
46424750
if args.step is None:

0 commit comments

Comments
 (0)