Select to view content in your preferred language

Multipatch Union3D

604
6
08-06-2025 11:49 AM
KiroAndreevGDI
Emerging Contributor
import arcpy
import os
from collections import defaultdict
from arcpy import AddFieldDelimiters, Describe
import time

start = time.time()
# === Setup ===
gdb_path = r"C:\Users\Administrator\Desktop\Downloaded\QC02002-ESA-MOD-BIM-ARC-06-V06-00001 (1).gdb"
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True

columns = os.path.join(gdb_path, "Spaces_BIMFileToGeodatab")
footprint_fc = "Columns_Footprint"
footprint_path = os.path.join(gdb_path, footprint_fc)
count_over = os.path.join(gdb_path, "count_over")
count_over_tab = os.path.join(gdb_path, "columns_tab")
singlePart = os.path.join(gdb_path, "singlePart")
arcpy.management.MultipartToSinglepart(columns, singlePart)
# === Geometry repair and footprint ===
arcpy.management.RepairGeometry(singlePart)
arcpy.ddd.MultiPatchFootprint(singlePart, footprint_path)
arcpy.analysis.CountOverlappingFeatures(footprint_path, count_over, "", count_over_tab)

# === Prepare ===
union_all = []
union_one = []

if arcpy.Exists(count_over_tab):
fields = [f.name for f in arcpy.ListFields(count_over_tab) if f.type != "OID"]
if len(fields) < 2:
print("‌‌ Table does not have at least two fields.")
else:
key_field = fields[0]
value_field = fields[1]
print(f"Using: key = {key_field}, value = {value_field}")

result_dict = defaultdict(list)
with arcpy.da.SearchCursor(count_over_tab, [key_field, value_field]) as cursor:
for key, value in cursor:
result_dict[key].append(value)

# === Get OID field and prepare layer ===
oid_field = Describe(singlePart).OIDFieldName
field_name = AddFieldDelimiters(singlePart, oid_field)
arcpy.MakeFeatureLayer_management(singlePart, "columns_layer")

# === Export all single-feature groups at once ===
single_oids = [str(oid_list[0]) for oid_list in result_dict.values() if len(oid_list) == 1]
if single_oids:
where_clause = f"{field_name} IN ({','.join(single_oids)})"
single_fc = os.path.join(gdb_path, "single_all")
arcpy.conversion.FeatureClassToFeatureClass(
in_features=singlePart,
out_path=gdb_path,
out_name="single_all",
where_clause=where_clause
)
print(f"‌‌ Exported {len(single_oids)} single-feature groups.")
union_one.append(single_fc)

# === Process multi-feature groups using selections ===
for group_id, oid_list in result_dict.items():
if len(oid_list) > 1:
oid_values = ", ".join(str(oid) for oid in oid_list)
where_clause = f"{field_name} IN ({oid_values})"
arcpy.SelectLayerByAttribute_management("columns_layer", "NEW_SELECTION", where_clause)

group_fc = os.path.join(gdb_path, f"group_{group_id}")
arcpy.CopyFeatures_management("columns_layer", group_fc)
count_group = int(arcpy.management.GetCount(group_fc)[0])
print(f"‌‌ Exported group {group_id} with {count_group} features.")

if count_group < 2:
print(f"‌‌ Skipping group {group_id} — not enough features.")
continue

enclosed_fc = os.path.join(gdb_path, f"enclosed_{group_id}")
try:
arcpy.ddd.EncloseMultiPatch(group_fc, enclosed_fc)
print(f"‌‌ Enclosed: {enclosed_fc}")
except Exception as e:
print(f"‌‌ Enclose failed for group {group_id}: {e}")
continue

union_fc = os.path.join(gdb_path, f"union_{group_id}")
try:
arcpy.ddd.Union3D(enclosed_fc, union_fc, '', 'DISABLE', 'ENABLE')
print(f"‌‌ Union3D completed: {union_fc}")
count = int(arcpy.management.GetCount(union_fc)[0])
print(f"‌‌ Union result contains {count} feature(s)")
if count > 0:
union_all.append(union_fc)
except Exception as e:
print(f"‌‌ Union3D failed for group {group_id}: {e}")

try:
# === Final Union: union_all + union_one ===
union_full = []

# Union All
if union_all:
merge_all = os.path.join(gdb_path, "merge_all")
enclosed_all = os.path.join(gdb_path, "enclosed_all")
union_all_fc = os.path.join(gdb_path, "union_all")
arcpy.management.Merge(union_all, merge_all)
arcpy.ddd.EncloseMultiPatch(merge_all, enclosed_all)
arcpy.ddd.Union3D(enclosed_all, union_all_fc, '', 'DISABLE', 'ENABLE')
union_full.append(union_all_fc)
count_all = int(arcpy.management.GetCount(union_all_fc)[0])
print(f"‌‌ Union ALL result contains {count_all} feature(s)")

# Union One
if union_one:
merge_one = os.path.join(gdb_path, "merge_one")
enclosed_one = os.path.join(gdb_path, "enclosed_one")
union_one_fc = os.path.join(gdb_path, "union_one")
arcpy.management.Merge(union_one, merge_one)
arcpy.ddd.EncloseMultiPatch(merge_one, enclosed_one)
arcpy.ddd.Union3D(enclosed_one, union_one_fc, '', 'DISABLE', 'ENABLE')
union_full.append(union_one_fc)
count_one = int(arcpy.management.GetCount(union_one_fc)[0])
print(f"‌‌ Union ONE result contains {count_one} feature(s)")

# Final Full Union
if union_full:
merge_full = os.path.join(gdb_path, "merge_full")
enclosed_full = os.path.join(gdb_path, "enclosed_full")
union_full_fc = os.path.join(gdb_path, "union_full")
arcpy.management.Merge(union_full, merge_full)
arcpy.ddd.EncloseMultiPatch(merge_full, enclosed_full)
arcpy.ddd.Union3D(enclosed_full, union_full_fc, '', 'DISABLE', 'ENABLE')
count_full = int(arcpy.management.GetCount(union_full_fc)[0])
print(f"‌‌ FINAL Union FULL result contains {count_full} feature(s)")

except Exception as e:
print(f"‌‌ Final Union3D process failed: {e}")
# Elapsed time in minutes
elapsed_minutes = (end - start) / 60
print("Elapsed time:", round(elapsed_minutes, 2), "minutes")

Hello,

i would like to create union from feature class which have more of 2000 features.My iterations is:

  1. create feature class from gdb
  2. create MultipartToSinglepart
  3. RepairGeometry of  single part
  4. create MultiPatchFootprint
  5. CountOverlappingFeatures
  6. create Enclosed
  7. create union of two, tree... pair multipatch which is overloading(first encode)
  8. merge single feature class
  9. create encode of merget sigle feature class
  10. create union of encode

Apear error:

warning stating that the resulting feature is not simple

How to solved problem?

 



      

0 Kudos
6 Replies
DanPatterson
MVP Esteemed Contributor

to make the code readable

Code formatting ... the Community Version - Esri Community


... sort of retired...
DanPatterson
MVP Esteemed Contributor

Union 3D (3D Analyst)—ArcGIS Pro | Documentation

A warning stating that the resulting feature is not simple and could not be created is raised if two or more multipatch features share only an edge or a vertex. This message indicates that the features were not merged because they did not share a volume of space.

DISABLE  output all may be needed if features aren't overlapping.

Before you try scripting, have you tried the process manually to see at which step it is failing?

( PS  code formatting is still missing indentation, so sometimes, it is best to apply the code formatting to a new copy of the code)


... sort of retired...
KiroA74
Emerging Contributor

Dear Dan,

i will send new version of code.I try:

  1. To create Union 3d to find which multipatch is overlopping
  2. Create Union whith single output
  3. Create Feature Class where is not overlopping features

Problem is with not overlopping features.It can not to Union as single output.This multipatch features is or not Closed or not Simple.

How to repair Not Ovrlopping features to be for creating Union with single output?

Best Regard

for i, fc in enumerate(merge_enclose):
    union_over_fc = os.path.join(gdb_path, f"unionOver_{i}")
    union_over_final_fc = os.path.join(gdb_path, f"unionOver_final_{i}")
    enclose_over_final = os.path.join(gdb_path, f"encloseOver_final_{i}")
    table_union_over = os.path.join(gdb_path, f"table_union_Over_{i}")
    
    try:
        # --- Step 1: Make union directly on input
        arcpy.ddd.Union3D(fc, union_over_fc, '', 'ENABLE', 'DISABLE', table_union_over)
        
        # --- Step 2: Enclose and union again
        grid_size = calculate_grid_size(fc)
        print(grid_size)
        arcpy.ddd.EncloseMultiPatch(union_over_fc, enclose_over_final)
        arcpy.ddd.Union3D(enclose_over_final, union_over_final_fc, '', 'DISABLE', 'ENABLE')
        
        # --- Step 3: Collect IDs from table_union_over
        ids = [row[0] for row in arcpy.da.SearchCursor(table_union_over, ["Input_ID"])]
        print(f"Found {len(ids)} overlapping IDs")
        
        # --- Step 4: Find the OID field of fc
        oid_field = arcpy.Describe(fc).OIDFieldName
        print(f"OID field in {fc}: {oid_field}")
        
        # --- Step 5: Apply filter + export
        if not ids:
            print("No overlaps found, copying all features.")
            out_fc = os.path.join(gdb_path, f"_NotOver_{i}")
            arcpy.management.CopyFeatures(fc, out_fc)
        else:
            values_sql = ",".join(map(str, ids))
            expression = f"NOT {oid_field} IN ({values_sql})"
            print(f"Using SQL: {expression}")
            
            out_fc = os.path.join(gdb_path, f"_NotOver_{i}")
            arcpy.conversion.FeatureClassToFeatureClass(fc, gdb_path, f"space_NotOver_{i}", expression)
            print(f"Created {out_fc}")
    
    except Exception as e:
        print(f"❌ Union3D failed for {fc}: {e}")
0 Kudos
DanPatterson
MVP Esteemed Contributor

Skip the script for now.

Can you run the whole process manually using the standard tools in Arctoolbox?

If you get the same error message, then there is no solution to the problem.  It may be something in one of the iterations of the input data that causes the failure.


... sort of retired...
KiroA74
Emerging Contributor
import arcpy
import os
from collections import defaultdict
from collections import Counter
import re

def calculate_grid_size(fc, fraction=1000.0, min_size=0.01):
    desc = arcpy.Describe(fc)
    extent = desc.extent

    XMax, XMin = extent.XMax, extent.XMin
    YMax, YMin = extent.YMax, extent.YMin
    ZMax, ZMin = extent.ZMax, extent.ZMin

    x_size = XMax - XMin if XMax is not None and XMin is not None else 0
    y_size = YMax - YMin if YMax is not None and YMin is not None else 0

    if ZMax is not None and ZMin is not None:
        z_size = ZMax - ZMin
        min_dim = min(x_size, y_size, z_size)
    else:
        # Fallback: 2D only
        min_dim = min(x_size, y_size)

    grid_size = max(min_dim / fraction, min_size)
    return grid_size

gdb_path = r"F:\INS\Downloaded\MultipatchALL\QC02002-ESA-MOD-BIM-ARC-06-V06-00001 (1).gdb"
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True

fc = os.path.join(gdb_path, "Walls_BIMFileToGeodatab")
merge_inputs=[]

def process_recursive(fc, gdb_path, iteration=0, merge_inputs=None):
    if merge_inputs is None:
        merge_inputs = []

    suffix = "" if iteration == 0 else f"_{iteration}"
    over = os.path.join(gdb_path, f"over{suffix}")
    over_enclose = os.path.join(gdb_path, f"over_enclose{suffix}")
    over_union = os.path.join(gdb_path, f"over_union{suffix}")
    over_tab = os.path.join(gdb_path, f"over_tab{suffix}")
    not_over = os.path.join(gdb_path, f"not_over{suffix}")
    not_over_enclose = os.path.join(gdb_path, f"not_over_enclose{suffix}")
    not_over_union = os.path.join(gdb_path, f"not_over_union{suffix}")
    not_over_tab = os.path.join(gdb_path, f"not_over_tab{suffix}")
    not_simple = os.path.join(gdb_path, f"not_simple{suffix}")
    single = os.path.join(gdb_path, f"single{suffix}")
    single_enclose= os.path.join(gdb_path, f"single_enclose{suffix}")
    single_union= os.path.join(gdb_path, f"single_union{suffix}")
    not_simple_fp = os.path.join(gdb_path, f"not_simple_fp{suffix}")
    not_simple_fp_3D = os.path.join(gdb_path, f"not_simple_fp_3D{suffix}")
    not_simple_multipatch = os.path.join(gdb_path, f"not_simple_multipatch{suffix}")
    not_simple_enclose = os.path.join(gdb_path, f"not_simple_enclose{suffix}")
    not_simple_union = os.path.join(gdb_path, f"not_simple_union{suffix}")
    merge = os.path.join(gdb_path, "merge")
    merge_enclose = os.path.join(gdb_path, "merge_enclose")
    merge_union = os.path.join(gdb_path, "merge_union")

    print(f"\n🔄 Iteration {iteration} → Processing {os.path.basename(fc)}")

        # --- Step 1: Enclose + Union3D on input ---
    grid_size = calculate_grid_size(fc)
    arcpy.ddd.EncloseMultiPatch(fc, over_enclose, grid_size)
    arcpy.ddd.Union3D(over_enclose, over, '', 'ENABLE', 'DISABLE', over_tab)
    
    # --- Step 2: Remove overlapping features ---
    oid_field = arcpy.Describe(fc).OIDFieldName
    
    if arcpy.Exists(over_tab):
        over_ids = {row[0] for row in arcpy.da.SearchCursor(over_tab, ["Input_ID"])}
    else:
        over_ids = set()
    
    if over_ids:
        id_list = ",".join(map(str, over_ids))
        where_clause = f'"{oid_field}" NOT IN ({id_list})'
    else:
        where_clause = "1=1"
    
    arcpy.FeatureClassToFeatureClass_conversion(fc, gdb_path, os.path.basename(not_over), where_clause)
    
    # --- Step 3: Enclose + Union3D on not_over ---
    grid_size = calculate_grid_size(not_over)
    arcpy.ddd.EncloseMultiPatch(not_over, not_over_enclose, grid_size)
    arcpy.ddd.Union3D(not_over_enclose, not_over_union, '', 'ENABLE', 'DISABLE', not_over_tab)
    
    # --- Step 4: Capture warnings ---
    warnings = arcpy.GetMessages(1)
    problematic_oids = []
    for line in warnings.split("\n"):
        if "NOT simple" in line and "OID" in line:
            nums = re.findall(r"\d+", line.split("OID")[-1])
            problematic_oids.extend(map(int, nums))
    
    print(f"⚠️ Found NOT simple OIDs: {problematic_oids}")
    
    # --- Step 5: Choose correct dataset for export ---
    if arcpy.Exists(not_over_union):
        export_source = not_over_union
    else:
        export_source = not_over
        print("⚠️ Using not_over instead of not_over_union (Union3D output missing)")
    
    oid_field = arcpy.Describe(export_source).OIDFieldName
    
    if problematic_oids:
        id_list = ",".join(map(str, problematic_oids))
        where_clause = f'"{oid_field}" IN ({id_list})'
        arcpy.FeatureClassToFeatureClass_conversion(export_source, gdb_path, os.path.basename(not_simple), where_clause)
        print(f"✅ Created 'not_simple' feature class")
    
        where_clause = f'"{oid_field}" NOT IN ({id_list})'
        arcpy.FeatureClassToFeatureClass_conversion(export_source, gdb_path, os.path.basename(single), where_clause)
        print(f"✅ Created 'single' feature class")
    else:
        arcpy.FeatureClassToFeatureClass_conversion(export_source, gdb_path, os.path.basename(single))
        print(f"✅ All features are simple → only 'single' created")
               # Collect merge candidates
    
    
    if arcpy.Exists(single):
        grid_size = calculate_grid_size(single)
        arcpy.ddd.EncloseMultiPatch(single, single_enclose, grid_size)
        arcpy.ddd.Union3D(single_enclose, single_union, '', 'DISABLE', 'ENABLE')
        problematic_single_oids = []
        # Collect all messages
        warnings_single = arcpy.GetMessages()
    
        for line in warnings_single.split("\n"):
            # Check for "NOT simple" or "not closed"
            if ("NOT simple" in line or "Non closed" in line) and "OID" in line:
                nums = re.findall(r"\d+", line.split("OID")[-1])
                problematic_single_oids.extend(map(int, nums))
    
        if problematic_single_oids:
            print(
                f"⚠️ Found problematic multipatch OIDs: {problematic_single_oids}. "
                f"Some geometries are not simple or not closed."
            )
        else:
            print("✅ All multipatches are valid (simple and closed).")
         # ✅ Check feature count in single_union
        count = int(arcpy.management.GetCount(single_union)[0])
        if count == 1:
            print("✅ Union result OK (single feature).")
            merge_inputs.append(single_union)
        else:
            print(f"❌ Union result NOK — expected 1 feature, got {count}.")

        
    if arcpy.Exists(over):
        grid_size = calculate_grid_size(over)
        arcpy.ddd.EncloseMultiPatch(over, over_enclose, grid_size)
        arcpy.ddd.Union3D(over_enclose, over_union, '', 'DISABLE', 'ENABLE')
        problematic_over_oids = []
        # Collect all messages
        warnings_over = arcpy.GetMessages()
    
        for line in warnings_over.split("\n"):
            # Check for "NOT simple" or "not closed"
            if ("NOT simple" in line or "Non closed" in line) and "OID" in line:
                nums = re.findall(r"\d+", line.split("OID")[-1])
                problematic_over_oids.extend(map(int, nums))
    
        if problematic_over_oids:
            print(
                f"⚠️ Found problematic multipatch OIDs: {problematic_over_oids}. "
                f"Some geometries are not simple or not closed."
            )
        else:
            print("✅ All multipatches are valid (simple and closed).")
         # ✅ Check feature count in over_union
        count = int(arcpy.management.GetCount(over_union)[0])
        if count == 1:
            print("✅ Union result OK (over feature).")
            merge_inputs.append(over_union)
        else:
            print(f"❌ Union result NOK — expected 1 feature, got {count}.")
    
    if arcpy.Exists(not_simple):
        arcpy.ddd.MultiPatchFootprint(not_simple, not_simple_fp)
        arcpy.cartography.SmoothSharedEdges(not_simple_fp, "", 0.001)
        if "Height" not in [f.name for f in arcpy.ListFields(not_simple_fp)]:
            arcpy.management.AddField(not_simple_fp, "Height", "DOUBLE")
        
        arcpy.management.CalculateField(not_simple_fp, "Height", "!Z_MAX! - !Z_MIN!", "PYTHON3")
     
        arcpy.ddd.FeatureTo3DByAttribute(not_simple_fp, not_simple_fp_3D, "Z_MIN", "Height")   
    
        layer_name="not_simple_fp_3D_layer"
    
        arcpy.management.MakeFeatureLayer(not_simple_fp_3D, layer_name)
    
        arcpy.management.ApplySymbologyFromLayer(layer_name, r"F:\INS\Walls_3D_layer.lyrx")
        
        arcpy.ddd.Layer3DToFeatureClass(
            layer_name,
            not_simple_multipatch
        )


    else:
        print("⚠️ Skipping not_simple processing → dataset not created")
    if arcpy.Exists(not_simple_multipatch):
        if int(arcpy.management.GetCount(not_simple_multipatch)[0]) > 0:
            # Recursive call
            return process_recursive(not_simple_multipatch, gdb_path, iteration + 1, merge_inputs)
    
    
    # Final merge
    if merge_inputs:
        arcpy.management.Merge(merge_inputs, merge)
        grid_size = calculate_grid_size(merge)
        arcpy.ddd.EncloseMultiPatch(merge, merge_enclose, grid_size)
        arcpy.ddd.Union3D(merge_enclose, merge_union, '', 'DISABLE', 'ENABLE')
        print(f"✅ Final union created: {merge_union}")
    elif iteration == 0:
        print("⚠️ No valid datasets found for merge → skipping")

    return merge_inputs

final_multipatch=process_recursive(fc, gdb_path)

 

I have new python script:

  1. Create Union 3D with single output from Overloping Multipatch
  2. Find and create Union 3D with single output from Single Multipatch.
  3. Not Simple geometry convert to multipatch and with function SmoothSharedEdges corect shared Multipatches.
  4. All process is on recursion.
  5. I must to use  arcpy.management.ApplySymbologyFromLayer(layer_name, r"F:\INS\Walls_3D_layer.lyrx") where Walls_3D_layer.lyrx is created manual. 
  6. How to create rcpy.management.ApplySymbologyFromLayer(layer_name, r"F:\INS\Walls_3D_layer.lyrx")  generic with programming?
0 Kudos
KiroA74
Emerging Contributor

Dear,

in attched file send Layer Merged_All_3D.lyrx.I would like to ask can solved my problem.I need to create Multipatch Union 3D with single output where geometry is on single row.Geometry must to be same as source.No Dissolve.

Best Regard

0 Kudos