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:
Apear error:
warning stating that the resulting feature is not simple
How to solved problem?
to make the code readable
Code formatting ... the Community Version - Esri Community
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)
Dear Dan,
i will send new version of code.I try:
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}")
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.
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: