@@ -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 ("\n Name 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+
22402336def _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