diff --git a/docker-compose.yml b/docker-compose.yml index 9cc269e..0eb81b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,5 @@ services: - ./:/app - /tmp/.X11-unix:/tmp/.X11-unix:ro - ${XAUTHORITY:-$HOME/.Xauthority}:/tmp/.Xauthority:ro + - ~/maps:/app/data:rw # Inherit CMD from Dockerfile: python3 -m pointcloud_tools.gui diff --git a/pointcloud_tools/gui.py b/pointcloud_tools/gui.py index ac25fb2..7448c8f 100644 --- a/pointcloud_tools/gui.py +++ b/pointcloud_tools/gui.py @@ -484,7 +484,7 @@ def build_ifc(self): self.ifc_path = tk.StringVar() ctk.CTkLabel(f, textvariable=self.ifc_path).grid(row=2, column=1, sticky="w") - ctk.CTkLabel(f, text="Processes (blank=auto)").grid(row=3, column=0, sticky='w') + ctk.CTkLabel(f, text="IFC workers (blank=auto)").grid(row=3, column=0, sticky='w') self.ifc_proc = tk.StringVar() ctk.CTkEntry(f, textvariable=self.ifc_proc, width=120).grid(row=3, column=1, sticky='w') @@ -512,7 +512,11 @@ def do_ifc_convert(self): if not mesh_out: return self.cfg["local_initialdir"] = os.path.dirname(mesh_out) - proc = int(self.ifc_proc.get()) if self.ifc_proc.get().strip() else None + try: + proc = int(self.ifc_proc.get()) if self.ifc_proc.get().strip() else None + except ValueError: + messagebox.showerror("Error", "IFC workers must be a whole number") + return types = list_element_types(infile) @@ -521,28 +525,40 @@ def do_ifc_convert(self): vars = {} for i, t in enumerate(sorted(types.keys())): var = tk.BooleanVar(value=True) - tk.Checkbutton(sel, text=t, variable=var).grid(row=i, column=0, sticky="w") + tk.Checkbutton(sel, text=f"{t} ({types[t]})", variable=var).grid( + row=i, column=0, sticky="w" + ) vars[t] = var def on_ok(): sel.include = [t for t, v in vars.items() if v.get()] sel.destroy() tk.Button(sel, text="OK", command=on_ok).grid(row=len(vars), column=0) sel.wait_window() - include = getattr(sel, 'include', None) + if not hasattr(sel, 'include'): + return + include = sel.include + if not include: + messagebox.showerror("Error", "No IFC element types selected") + return + + if not confirm_overwrite_gui(mesh_out): + return self.log_text.delete("1.0", tk.END) def worker(): try: with contextlib.redirect_stdout(self.stdout_redirector): - ifc_to_mesh( + mesh = ifc_to_mesh( infile, mesh_out, show_result=self.ifc_show.get(), num_processes=proc, include_types=include, - confirm_func=confirm_overwrite_gui, + confirm_func=lambda _: True, ) + if mesh is None: + raise RuntimeError("IFC conversion did not produce a mesh") def done(): self.add_recent_file(mesh_out) messagebox.showinfo("Done", "Mesh generated") @@ -832,17 +848,35 @@ def task(prev): vars = {} for i, t in enumerate(sorted(types.keys())): var = tk.BooleanVar(value=True) - tk.Checkbutton(sel, text=t, variable=var).grid(row=i, column=0, sticky="w") + tk.Checkbutton(sel, text=f"{t} ({types[t]})", variable=var).grid( + row=i, column=0, sticky="w" + ) vars[t] = var def on_ok(): sel.include = [t for t, v in vars.items() if v.get()] sel.destroy() tk.Button(sel, text="OK", command=on_ok).grid(row=len(vars), column=0) sel.wait_window() - include = getattr(sel, 'include', None) + if not hasattr(sel, 'include'): + return + include = sel.include + if not include: + messagebox.showerror("Error", "No IFC element types selected") + return + + if not confirm_overwrite_gui(out): + return def task(prev): - ifc_to_mesh(ifc, out, show_result=False, include_types=include, confirm_func=confirm_overwrite_gui) + mesh = ifc_to_mesh( + ifc, + out, + show_result=False, + include_types=include, + confirm_func=lambda _: True, + ) + if mesh is None: + raise RuntimeError("IFC conversion did not produce a mesh") return out desc = f"IFC->Mesh {os.path.basename(ifc)}" elif op == "Downsample PCD": diff --git a/pointcloud_tools/ifc.py b/pointcloud_tools/ifc.py index 012e1d6..a251f6e 100644 --- a/pointcloud_tools/ifc.py +++ b/pointcloud_tools/ifc.py @@ -1,54 +1,62 @@ +import os +import time +from collections import defaultdict + import ifcopenshell import ifcopenshell.geom import open3d as o3d import numpy as np -import time -import concurrent.futures -import os -import multiprocessing +import psutil from .utils import confirm_overwrite_cli -def process_batch(batch_data): - """Process a batch of IFC products in a separate process.""" - batch_id, product_ids, ifc_path, include_types = batch_data - - try: - ifc_file = ifcopenshell.open(ifc_path) - settings = ifcopenshell.geom.settings() - settings.set(settings.USE_WORLD_COORDS, True) - - # Create lists to store meshes and their element types - mesh_data = [] - - for product_id in product_ids: - try: - product = ifc_file.by_id(product_id) - - # Skip products without representation - if not product.Representation: - continue - - # Get element type for filtering - element_type = product.is_a() - - # Skip if this element type should not be included - if include_types and element_type not in include_types: - continue - - shape = ifcopenshell.geom.create_shape(settings, product) - verts = np.array(shape.geometry.verts).reshape(-1, 3) - faces = np.array(shape.geometry.faces).reshape(-1, 3) - - if len(verts) > 0 and len(faces) > 0: - # Store mesh data and element type - mesh_data.append((verts, faces, element_type)) - except Exception as e: - continue - - return batch_id, mesh_data - - except Exception as e: - return batch_id, [] +# Memory threshold for visualization (in GB) +MEMORY_WARNING_THRESHOLD_GB = 1.0 +MEMORY_ABORT_THRESHOLD_GB = 4.0 +PROGRESS_INTERVAL_SECONDS = 5.0 +MESH_CHUNK_ELEMENT_LIMIT = 500 +MESH_CHUNK_VERTEX_LIMIT = 500_000 + + +def _available_cpu_count(): + if hasattr(os, "sched_getaffinity"): + try: + return max(1, len(os.sched_getaffinity(0))) + except OSError: + pass + return max(1, os.cpu_count() or 1) + + +def _resolve_ifc_workers(requested_workers): + if requested_workers is None: + return max(1, _available_cpu_count() - 1) + return max(1, requested_workers) + + +def _memory_available_gb(): + return psutil.virtual_memory().available / (1024**3) + + +def _count_element_types(ifc_file): + element_count = defaultdict(int) + for product in ifc_file.by_type("IfcProduct"): + if product.Representation: + element_count[product.is_a()] += 1 + return dict(element_count) + + +def _print_element_type_table(ifc_path, element_count): + print(f"\nElement types in {os.path.basename(ifc_path)}:") + print("-" * 50) + print(f"{'Element Type':<30} | {'Count':<10}") + print("-" * 50) + + for element_type, count in sorted(element_count.items()): + print(f"{element_type:<30} | {count:<10}") + + print("-" * 50) + print(f"Total element types: {len(element_count)}") + print(f"Total elements with geometry: {sum(element_count.values())}") + def ifc_to_mesh( ifc_path, @@ -59,7 +67,7 @@ def ifc_to_mesh( confirm_func=confirm_overwrite_cli, ): """ - Extract a combined mesh from an IFC file using parallel processing. + Extract a combined mesh from an IFC file using IfcOpenShell's geometry iterator. Parameters: ----------- @@ -70,233 +78,254 @@ def ifc_to_mesh( show_result : bool Whether to display the visual results (default: True) num_processes : int or None - Number of parallel processes to use (default: CPU count - 1) + Number of geometry workers to use. Kept for API compatibility. include_types : list or None List of IFC element types to include in processing (default: None, meaning all types) """ # Leave include_types as None to trigger interactive mode later - + print(f"Loading IFC file: {ifc_path}") - + # First analyze and list all element types in the file print("\n========== IFC ELEMENT TYPES ANALYSIS ==========") ifc_file = ifcopenshell.open(ifc_path) - - # Dictionary to count element types - element_count = {} - - # Count all products with geometry - for product in ifc_file.by_type("IfcProduct"): - if product.Representation: - element_type = product.is_a() - if element_type not in element_count: - element_count[element_type] = 0 - element_count[element_type] += 1 - - # Print results in a table format - print(f"\nElement types in {os.path.basename(ifc_path)}:") - print("-" * 50) - print(f"{'Element Type':<30} | {'Count':<10}") - print("-" * 50) - - for element_type, count in sorted(element_count.items()): - print(f"{element_type:<30} | {count:<10}") - - print("-" * 50) - print(f"Total element types: {len(element_count)}") - print(f"Total elements with geometry: {sum(element_count.values())}") + element_count = _count_element_types(ifc_file) + _print_element_type_table(ifc_path, element_count) print("=" * 45) print() - + # Ask user for element types to include if not provided if include_types is None: element_types = sorted(element_count.keys()) print("\nSelect element types to INCLUDE (comma-separated numbers or 'all'):") for i, t in enumerate(element_types, 1): print(f"{i}. {t}") - user = input('> ').strip().lower() + user = input("> ").strip().lower() include_types = [] - if user not in ('', 'all'): + if user not in ("", "all"): try: - indices = [int(x.strip()) - 1 for x in user.split(',')] + indices = [int(x.strip()) - 1 for x in user.split(",")] for idx in indices: if 0 <= idx < len(element_types): include_types.append(element_types[idx]) except ValueError: - print('Invalid selection, including all types.') + print("Invalid selection, including all types.") else: include_types = None elif include_types: + include_types = [t for t in include_types if t] print(f"Including only element types: {', '.join(include_types)}") - + else: + print("No IFC element types selected.") + return None + + if include_types: + unknown_types = sorted(set(include_types) - set(element_count)) + if unknown_types: + print(f"Warning: selected types not found: {', '.join(unknown_types)}") + total_products = sum(element_count.get(t, 0) for t in include_types) + skipped_by_filter = sum(element_count.values()) - total_products + else: + total_products = sum(element_count.values()) + skipped_by_filter = 0 + + if total_products == 0: + print("No matching IFC elements with geometry representations.") + return None + + if not confirm_func(mesh_output_path): + print("Aborted by user") + return None + + worker_count = _resolve_ifc_workers(num_processes) + print(f"Using {worker_count} IFC geometry worker(s)") + print(f"Found {total_products} matching IFC elements with geometry representations") + if skipped_by_filter: + print(f"Skipped {skipped_by_filter} elements outside the selected types") + start_time = time.time() - - # Determine number of processes - if num_processes is None: - num_processes = max(1, multiprocessing.cpu_count() - 1) - - print(f"Using {num_processes} parallel processes") - - # Load IFC file in main process - ifc_file = ifcopenshell.open(ifc_path) - - # Get products with geometry - products = [] - for product in ifc_file.by_type("IfcProduct"): - if product.Representation: - products.append(product.id()) - - total_products = len(products) - print(f"Found {total_products} IFC elements with geometry representations") - - # Create batches for parallel processing - batch_size = max(1, len(products) // (num_processes * 2)) - batches = [] - - for i in range(0, len(products), batch_size): - batch_products = products[i:i + batch_size] - batches.append((len(batches), batch_products, ifc_path, include_types)) - - print(f"Divided work into {len(batches)} batches") - - # Process batches in parallel - print(f"Processing geometry in parallel...") processed_elements = 0 - excluded_elements = 0 - - # Dictionary to store meshes by element type - meshes_by_type = {} - - with concurrent.futures.ProcessPoolExecutor(max_workers=num_processes) as executor: - futures = {executor.submit(process_batch, batch): batch[0] for batch in batches} - - for future in concurrent.futures.as_completed(futures): - try: - batch_id, mesh_data = future.result() - - # Process the mesh data - for verts, faces, element_type in mesh_data: - # Create mesh for this element - element_mesh = o3d.geometry.TriangleMesh() - element_mesh.vertices = o3d.utility.Vector3dVector(verts) - element_mesh.triangles = o3d.utility.Vector3iVector(faces) - - # Add to dictionary by type - if element_type not in meshes_by_type: - meshes_by_type[element_type] = [] - - meshes_by_type[element_type].append(element_mesh) - processed_elements += 1 - - # Report progress - percent_complete = (batch_id + 1) / len(batches) * 100 - elapsed = time.time() - start_time - print(f"Batch {batch_id+1}/{len(batches)} complete ({percent_complete:.1f}%) - " - f"{processed_elements}/{total_products} elements processed - {elapsed:.1f} seconds elapsed") - - except Exception as e: - print(f"Error processing batch {futures[future]}: {str(e)}") - - print(f"Geometry extraction complete - {processed_elements}/{total_products} elements processed successfully") - if include_types is not None: - skipped = total_products - processed_elements - print(f"Skipped approximately {skipped} elements not in selected types") - - # Check if we have any geometry - if not meshes_by_type: - print("Error: No valid geometry found in the IFC file") - - # Debug information - print("\nDebug information:") - print(f"IFC file exists: {os.path.exists(ifc_path)}") - print(f"IFC file size: {os.path.getsize(ifc_path) / (1024*1024):.2f} MB") - print(f"Total products with representation: {len(products)}") - - # Try a single element for testing - if len(products) > 0: - print("\nAttempting to process one element in main process for debugging:") - try: - test_product = ifc_file.by_id(products[0]) - settings = ifcopenshell.geom.settings() - settings.set(settings.USE_WORLD_COORDS, True) - shape = ifcopenshell.geom.create_shape(settings, test_product) - print(f"Test element has {len(shape.geometry.verts)/3} vertices and {len(shape.geometry.faces)/3} faces") - except Exception as e: - print(f"Error processing test element: {str(e)}") - - return None - - # Prepare for visualization and mesh generation - print(f"Found {len(meshes_by_type)} different element types") - - # Create a list of colored meshes for visualization - colored_meshes = [] + failed_elements = 0 + processed_by_type = defaultdict(int) + color_by_type = {} combined_mesh = o3d.geometry.TriangleMesh() - - # Color palette for different element types (using distinct colors) + chunk_vertices = [] + chunk_faces = [] + chunk_colors = [] + chunk_vertex_count = 0 + chunk_element_count = 0 + colors = [ - [1, 0, 0], # Red - [0, 1, 0], # Green - [0, 0, 1], # Blue - [1, 1, 0], # Yellow - [1, 0, 1], # Magenta - [0, 1, 1], # Cyan - [0.5, 0, 0], # Dark red - [0, 0.5, 0], # Dark green - [0, 0, 0.5], # Dark blue + [1, 0, 0], # Red + [0, 1, 0], # Green + [0, 0, 1], # Blue + [1, 1, 0], # Yellow + [1, 0, 1], # Magenta + [0, 1, 1], # Cyan + [0.5, 0, 0], # Dark red + [0, 0.5, 0], # Dark green + [0, 0, 0.5], # Dark blue [0.5, 0.5, 0], # Olive [0.5, 0, 0.5], # Purple [0, 0.5, 0.5], # Teal - [1, 0.5, 0], # Orange - [0.5, 1, 0], # Light green - [0, 0.5, 1], # Light blue + [1, 0.5, 0], # Orange + [0.5, 1, 0], # Light green + [0, 0.5, 1], # Light blue ] - - # Print out all element types found in the model - print("\nElement types present in the model:") - for element_type in sorted(meshes_by_type.keys()): - print(f" - {element_type}: {len(meshes_by_type[element_type])} elements") - # Color and combine meshes by type - print("\nPreparing colored visualization...") - combined_mesh = o3d.geometry.TriangleMesh() # Initialize combined mesh - - for i, (element_type, meshes) in enumerate(meshes_by_type.items()): - color_index = i % len(colors) - type_mesh = o3d.geometry.TriangleMesh() - - # Combine all meshes of this type - for mesh in meshes: - type_mesh += mesh - - # Compute normals and color it - type_mesh.compute_vertex_normals() - type_mesh.paint_uniform_color(colors[color_index]) - - # Add to visualization list - colored_meshes.append(type_mesh) - - # Add to combined mesh for output - combined_mesh += type_mesh - - print(f" - {element_type}: {len(meshes)} elements (Color: {colors[color_index]})") - - # Create a coordinate frame for reference - coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=1.0) - - # STEP 1: Visualize the colored mesh model - print("\nSTEP 1: Displaying the extracted mesh model with colors by element type.") - print("Colors represent different IFC element types as listed above.") - print("Close the window when finished inspecting to continue.") - if not confirm_func(mesh_output_path): - print("Aborted by user") + def color_for_type(element_type): + if element_type not in color_by_type: + color_by_type[element_type] = colors[len(color_by_type) % len(colors)] + return color_by_type[element_type] + + def flush_chunk(): + nonlocal combined_mesh, chunk_vertex_count, chunk_element_count + if not chunk_vertices: + return + + chunk_mesh = o3d.geometry.TriangleMesh() + chunk_mesh.vertices = o3d.utility.Vector3dVector(np.vstack(chunk_vertices)) + chunk_mesh.triangles = o3d.utility.Vector3iVector(np.vstack(chunk_faces)) + chunk_mesh.vertex_colors = o3d.utility.Vector3dVector(np.vstack(chunk_colors)) + combined_mesh += chunk_mesh + + chunk_vertices.clear() + chunk_faces.clear() + chunk_colors.clear() + chunk_vertex_count = 0 + chunk_element_count = 0 + + settings = ifcopenshell.geom.settings() + settings.set(settings.USE_WORLD_COORDS, True) + include_filter = include_types if include_types else None + iterator = ifcopenshell.geom.iterator( + settings, ifc_file, worker_count, include=include_filter + ) + + print("Processing geometry with IfcOpenShell iterator...") + try: + has_geometry = iterator.initialize() + except Exception as e: + print(f"Error initializing IFC geometry iterator: {str(e)}") return None - # Write the combined mesh to file (not the list) - o3d.io.write_triangle_mesh(mesh_output_path, combined_mesh) - # Add all meshes and the coordinate frame to the visualization - visualization_objects = colored_meshes + [coordinate_frame] + if not has_geometry: + print("Error: No valid geometry found in the IFC file") + return None + + last_report = time.time() + while True: + try: + shape = iterator.get() + geometry = shape.geometry + verts = np.asarray(geometry.verts, dtype=np.float64).reshape(-1, 3) + faces = np.asarray(geometry.faces, dtype=np.int32).reshape(-1, 3) + + if len(verts) > 0 and len(faces) > 0: + element_type = shape.type or ifc_file.by_id(shape.id).is_a() + color = np.asarray(color_for_type(element_type), dtype=np.float64) + chunk_vertices.append(verts) + chunk_faces.append(faces + chunk_vertex_count) + chunk_colors.append(np.tile(color, (len(verts), 1))) + chunk_vertex_count += len(verts) + chunk_element_count += 1 + processed_by_type[element_type] += 1 + processed_elements += 1 + + if ( + chunk_element_count >= MESH_CHUNK_ELEMENT_LIMIT + or chunk_vertex_count >= MESH_CHUNK_VERTEX_LIMIT + ): + flush_chunk() + except MemoryError: + raise + except Exception as e: + failed_elements += 1 + if failed_elements <= 10: + print(f"Warning: failed to process IFC geometry: {str(e)}") + + now = time.time() + if now - last_report >= PROGRESS_INTERVAL_SECONDS: + percent_complete = processed_elements / total_products * 100 + elapsed = now - start_time + available_memory_gb = _memory_available_gb() + print( + f"Processed {processed_elements}/{total_products} geometries " + f"({percent_complete:.1f}%) - {elapsed:.1f}s elapsed - " + f"{available_memory_gb:.1f} GB memory available" + ) + last_report = now + if available_memory_gb < MEMORY_ABORT_THRESHOLD_GB: + raise MemoryError( + "IFC conversion stopped because available memory dropped " + f"to {available_memory_gb:.1f} GB." + ) + + try: + if not iterator.next(): + break + except Exception as e: + print(f"Error advancing IFC geometry iterator: {str(e)}") + break + + flush_chunk() + + print( + f"Geometry extraction complete - {processed_elements}/{total_products} " + "elements processed successfully" + ) + if failed_elements: + print(f"Skipped {failed_elements} elements that failed geometry conversion") + + if processed_elements == 0 or len(combined_mesh.vertices) == 0: + print("Error: No valid geometry found in the IFC file") + print("\nDebug information:") + print(f"IFC file exists: {os.path.exists(ifc_path)}") + print(f"IFC file size: {os.path.getsize(ifc_path) / (1024*1024):.2f} MB") + print(f"Matching products with representation: {total_products}") + return None + + available_memory_gb = _memory_available_gb() + vertex_count = len(combined_mesh.vertices) + triangle_count = len(combined_mesh.triangles) + estimated_mesh_size = ( + vertex_count * (3 * 8 + 3 * 8) + triangle_count * 3 * 4 + ) / (1024**3) + + print("\nMemory Info:") + print(f" Available: {available_memory_gb:.1f} GB") + print(f" Mesh vertices: {vertex_count}") + print(f" Mesh triangles: {triangle_count}") + print(f" Estimated in-memory mesh size: {estimated_mesh_size:.1f} GB") + + if available_memory_gb < MEMORY_ABORT_THRESHOLD_GB: + print("\nWARNING: Low memory condition detected!") + print( + f" Only {available_memory_gb:.1f} GB available " + f"(need ~{MEMORY_ABORT_THRESHOLD_GB} GB)" + ) + print(" Proceeding without visualization to minimize memory usage") + show_result = False + elif available_memory_gb < MEMORY_WARNING_THRESHOLD_GB * 2: + print("\nWARNING: Memory is constrained. Mesh visualization may be slow.") + + print("\nElement types present in the output mesh:") + for element_type in sorted(processed_by_type): + print( + f" - {element_type}: {processed_by_type[element_type]} elements " + f"(Color: {color_by_type[element_type]})" + ) + + # Write the combined mesh to file + print(f"\nWriting mesh to {mesh_output_path}...") + if not o3d.io.write_triangle_mesh(mesh_output_path, combined_mesh): + raise RuntimeError(f"Failed to write mesh to {mesh_output_path}") + if show_result: + print("\nDisplaying the extracted mesh model with colors by element type.") + combined_mesh.compute_vertex_normals() + coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=1.0) + visualization_objects = [combined_mesh, coordinate_frame] o3d.visualization.draw_geometries(visualization_objects) total_time = time.time() - start_time @@ -304,75 +333,78 @@ def ifc_to_mesh( return combined_mesh + def list_element_types(ifc_path): """ List all element types present in an IFC file without processing the geometry. Useful for determining which element types to filter. - + Parameters: ----------- ifc_path : str Path to the IFC file to analyze - + Returns: -------- dict Dictionary of element types and their counts """ print(f"Analyzing IFC file: {ifc_path}") - + try: # Load IFC file ifc_file = ifcopenshell.open(ifc_path) - - # Dictionary to count element types - element_count = {} - - # Count all products with geometry - for product in ifc_file.by_type("IfcProduct"): - if product.Representation: - element_type = product.is_a() - if element_type not in element_count: - element_count[element_type] = 0 - element_count[element_type] += 1 - - # Print results - print(f"\nElement types in {os.path.basename(ifc_path)}:") - print("-" * 50) - print(f"{'Element Type':<30} | {'Count':<10}") - print("-" * 50) - - for element_type, count in sorted(element_count.items()): - print(f"{element_type:<30} | {count:<10}") - - print("-" * 50) - print(f"Total element types: {len(element_count)}") - print(f"Total elements with geometry: {sum(element_count.values())}") - + element_count = _count_element_types(ifc_file) + _print_element_type_table(ifc_path, element_count) + return element_count - + except Exception as e: print(f"Error analyzing IFC file: {str(e)}") return {} + if __name__ == "__main__": - import sys import argparse # Set up argument parser for better CLI usage parser = argparse.ArgumentParser(description="Extract a mesh from an IFC file") parser.add_argument("ifc_path", help="Path to the IFC file") - parser.add_argument("--mesh", "-m", default="model.ply", help="Output mesh file path") - parser.add_argument("--processes", "-n", type=int, default=None, help="Number of parallel processes") - parser.add_argument("--include", "-i", nargs='+', default=[], help="List of element types to include (e.g. IfcWall IfcColumn)") - parser.add_argument("--list-types", "-l", action="store_true", help="List all element types in the IFC file and exit") - parser.add_argument("--no-display", "-nd", action="store_true", help="Don't display visual results") - parser.add_argument("--interactive-filter", action="store_true", - help="Interactively select element types to include after listing") - + parser.add_argument( + "--mesh", "-m", default="model.ply", help="Output mesh file path" + ) + parser.add_argument( + "--processes", + "-n", + type=int, + default=None, + help="Number of IFC geometry workers", + ) + parser.add_argument( + "--include", + "-i", + nargs="+", + default=[], + help="List of element types to include (e.g. IfcWall IfcColumn)", + ) + parser.add_argument( + "--list-types", + "-l", + action="store_true", + help="List all element types in the IFC file and exit", + ) + parser.add_argument( + "--no-display", "-nd", action="store_true", help="Don't display visual results" + ) + parser.add_argument( + "--interactive-filter", + action="store_true", + help="Interactively select element types to include after listing", + ) + # Parse arguments args = parser.parse_args() - + # If list-types flag is set, only list element types and exit if args.list_types: list_element_types(args.ifc_path) @@ -387,13 +419,17 @@ def list_element_types(ifc_path): print(f"{i}. {element_type}") # Ask user which types to include - print("\nEnter the numbers of element types to INCLUDE (comma-separated, or 'all'):") + print( + "\nEnter the numbers of element types to INCLUDE (comma-separated, or 'all'):" + ) user_input = input("> ").strip().lower() include_types = [] if user_input not in ("", "all"): try: - selected_indices = [int(idx.strip()) - 1 for idx in user_input.split(",")] + selected_indices = [ + int(idx.strip()) - 1 for idx in user_input.split(",") + ] for idx in selected_indices: if 0 <= idx < len(element_types): include_types.append(sorted(element_types)[idx]) @@ -403,7 +439,7 @@ def list_element_types(ifc_path): print("Invalid input. Proceeding with all types.") else: include_types = None - + # Process the file with interactively selected filters ifc_to_mesh( args.ifc_path, @@ -411,7 +447,7 @@ def list_element_types(ifc_path): not args.no_display, args.processes, include_types, - confirm_overwrite_cli + confirm_overwrite_cli, ) else: # If no include list provided, run interactively @@ -422,5 +458,5 @@ def list_element_types(ifc_path): not args.no_display, args.processes, incl, - confirm_overwrite_cli - ) \ No newline at end of file + confirm_overwrite_cli, + ) diff --git a/requirements.txt b/requirements.txt index e1c9606..55f04cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ numpy paramiko customtkinter pyvista +psutil