From de2d2c4e3b24947662875e3fcd8b5253ff21d9dd Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Mon, 23 Mar 2026 23:17:21 +0000 Subject: [PATCH] Remove ifcquery build artifacts and add to .gitignore --- .gitignore | 1 + src/ifcquery/build/lib/ifcquery/__init__.py | 20 - src/ifcquery/build/lib/ifcquery/__main__.py | 251 ------------ src/ifcquery/build/lib/ifcquery/clash.py | 167 -------- src/ifcquery/build/lib/ifcquery/contexts.py | 44 --- src/ifcquery/build/lib/ifcquery/cost.py | 45 --- src/ifcquery/build/lib/ifcquery/info.py | 118 ------ src/ifcquery/build/lib/ifcquery/materials.py | 86 ----- src/ifcquery/build/lib/ifcquery/relations.py | 169 --------- src/ifcquery/build/lib/ifcquery/render.py | 377 ------------------- src/ifcquery/build/lib/ifcquery/schedule.py | 56 --- src/ifcquery/build/lib/ifcquery/schema.py | 19 - src/ifcquery/build/lib/ifcquery/select.py | 41 -- src/ifcquery/build/lib/ifcquery/summary.py | 52 --- src/ifcquery/build/lib/ifcquery/tree.py | 68 ---- src/ifcquery/build/lib/ifcquery/validate.py | 15 - 16 files changed, 1 insertion(+), 1528 deletions(-) delete mode 100644 src/ifcquery/build/lib/ifcquery/__init__.py delete mode 100644 src/ifcquery/build/lib/ifcquery/__main__.py delete mode 100644 src/ifcquery/build/lib/ifcquery/clash.py delete mode 100644 src/ifcquery/build/lib/ifcquery/contexts.py delete mode 100644 src/ifcquery/build/lib/ifcquery/cost.py delete mode 100644 src/ifcquery/build/lib/ifcquery/info.py delete mode 100644 src/ifcquery/build/lib/ifcquery/materials.py delete mode 100644 src/ifcquery/build/lib/ifcquery/relations.py delete mode 100644 src/ifcquery/build/lib/ifcquery/render.py delete mode 100644 src/ifcquery/build/lib/ifcquery/schedule.py delete mode 100644 src/ifcquery/build/lib/ifcquery/schema.py delete mode 100644 src/ifcquery/build/lib/ifcquery/select.py delete mode 100644 src/ifcquery/build/lib/ifcquery/summary.py delete mode 100644 src/ifcquery/build/lib/ifcquery/tree.py delete mode 100644 src/ifcquery/build/lib/ifcquery/validate.py diff --git a/.gitignore b/.gitignore index bee04658cd..2e17d464d7 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,4 @@ dev_environment.bat src/ifcopenshell-python/ifcopenshell/express/*.exp src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat +src/ifcquery/build/ diff --git a/src/ifcquery/build/lib/ifcquery/__init__.py b/src/ifcquery/build/lib/ifcquery/__init__.py deleted file mode 100644 index be5327b20d..0000000000 --- a/src/ifcquery/build/lib/ifcquery/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -__version__ = version = "0.0.0" diff --git a/src/ifcquery/build/lib/ifcquery/__main__.py b/src/ifcquery/build/lib/ifcquery/__main__.py deleted file mode 100644 index 5c403b7842..0000000000 --- a/src/ifcquery/build/lib/ifcquery/__main__.py +++ /dev/null @@ -1,251 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -import argparse -import json -import os -import sys - -import ifcopenshell - -from ifcquery import clash as clash_mod -from ifcquery import contexts as contexts_mod -from ifcquery import cost as cost_mod -from ifcquery import info, relations, schedule, schema, select, summary, tree -from ifcquery import materials as materials_mod -from ifcquery import render as render_mod -from ifcquery import validate as validate_mod - - -def parse_element_id(raw: str) -> int: - """Parse an element ID from '#123' or '123' format.""" - raw = raw.strip().lstrip("#") - return int(raw) - - -def format_output(data, fmt: str) -> str: - if fmt == "json": - return json.dumps(data, indent=2, ensure_ascii=False) - elif fmt == "text": - return _format_text(data) - return json.dumps(data, indent=2, ensure_ascii=False) - - -def _format_text(data, indent: int = 0) -> str: - prefix = " " * indent - lines = [] - if isinstance(data, dict): - for key, value in data.items(): - if isinstance(value, (dict, list)): - lines.append(f"{prefix}{key}:") - lines.append(_format_text(value, indent + 1)) - else: - lines.append(f"{prefix}{key}: {value}") - elif isinstance(data, list): - for item in data: - if isinstance(item, dict): - lines.append(_format_text(item, indent)) - lines.append("") - else: - lines.append(f"{prefix}- {item}") - else: - lines.append(f"{prefix}{data}") - return "\n".join(lines) - - -def main(): - parser = argparse.ArgumentParser( - prog="ifcquery", - description="Query and inspect IFC building models", - ) - parser.add_argument("ifc_file", help="Path to the IFC file") - parser.add_argument( - "--format", - choices=["json", "text"], - default="json", - dest="output_format", - help="Output format (default: json)", - ) - - subparsers = parser.add_subparsers(dest="command", required=True) - - subparsers.add_parser("summary", help="Model overview: schema, element counts, project info") - - subparsers.add_parser("tree", help="Spatial hierarchy tree") - - info_parser = subparsers.add_parser("info", help="Deep inspection of a specific element") - info_parser.add_argument("element_id", help="Element step ID (e.g. 123 or #123)") - - select_parser = subparsers.add_parser("select", help="Filter elements using selector syntax") - select_parser.add_argument("query", help="Selector query string") - - relations_parser = subparsers.add_parser("relations", help="Show relationships for an element") - relations_parser.add_argument("element_id", help="Element step ID (e.g. 123 or #123)") - relations_parser.add_argument("--traverse", choices=["up"], help="Traverse hierarchy (up: walk to IfcProject)") - - clash_parser = subparsers.add_parser("clash", help="Check element placement for clashes") - clash_parser.add_argument("element_id", help="Element step ID (e.g. 123 or #123)") - clash_parser.add_argument("--clearance", type=float, help="Minimum clearance distance") - clash_parser.add_argument("--tolerance", type=float, default=0.002, help="Intersection tolerance (default: 0.002)") - clash_parser.add_argument( - "--scope", choices=["storey", "all"], default="storey", help="Scope of elements to check (default: storey)" - ) - - validate_parser = subparsers.add_parser("validate", help="Schema/constraint validation") - validate_parser.add_argument( - "--rules", action="store_true", help="Also check EXPRESS rules (slower, default: false)" - ) - - schedule_parser = subparsers.add_parser("schedule", help="List work plans and tasks from the model") - schedule_parser.add_argument( - "--depth", type=int, default=None, metavar="N", help="Limit subtask expansion to N levels (default: unlimited)" - ) - - cost_parser = subparsers.add_parser("cost", help="List cost schedules and cost items from the model") - cost_parser.add_argument( - "--depth", type=int, default=None, metavar="N", help="Limit cost item expansion to N levels (default: unlimited)" - ) - - subparsers.add_parser("contexts", help="List geometric representation contexts and subcontexts") - - subparsers.add_parser("materials", help="List materials and material sets") - - schema_parser = subparsers.add_parser("schema", help="IFC class documentation") - schema_parser.add_argument("entity_type", help="IFC entity type (e.g. IfcWall)") - - render_parser = subparsers.add_parser("render", help="Render model geometry to a PNG image") - render_parser.add_argument( - "-o", "--output", default="", metavar="FILE", help="Output PNG path (default: .png)" - ) - render_parser.add_argument( - "--selector", default="", metavar="QUERY", help="ifcopenshell selector to restrict rendered elements" - ) - render_parser.add_argument( - "--element", default="", metavar="ID[,ID...]", - help="Comma-separated step IDs of elements to highlight (rest rendered in grey)" - ) - render_parser.add_argument( - "--view", - choices=render_mod.VIEWS, - default="iso", - help="Camera angle (default: iso)", - ) - - args = parser.parse_args() - - try: - model = ifcopenshell.open(args.ifc_file) - except Exception as e: - print(f"Error: Could not open IFC file: {e}", file=sys.stderr) - sys.exit(1) - - if args.command == "summary": - result = summary.summary(model) - elif args.command == "tree": - result = tree.tree(model) - elif args.command == "info": - try: - element_id = parse_element_id(args.element_id) - except ValueError: - print(f"Error: Invalid element ID: {args.element_id}", file=sys.stderr) - sys.exit(1) - try: - element = model.by_id(element_id) - except RuntimeError: - print(f"Error: Element #{element_id} not found", file=sys.stderr) - sys.exit(1) - result = info.info(model, element) - elif args.command == "select": - result = select.select(model, args.query) - elif args.command == "relations": - try: - element_id = parse_element_id(args.element_id) - except ValueError: - print(f"Error: Invalid element ID: {args.element_id}", file=sys.stderr) - sys.exit(1) - try: - element = model.by_id(element_id) - except RuntimeError: - print(f"Error: Element #{element_id} not found", file=sys.stderr) - sys.exit(1) - result = relations.relations(model, element, traverse=args.traverse) - elif args.command == "clash": - try: - element_id = parse_element_id(args.element_id) - except ValueError: - print(f"Error: Invalid element ID: {args.element_id}", file=sys.stderr) - sys.exit(1) - try: - element = model.by_id(element_id) - except RuntimeError: - print(f"Error: Element #{element_id} not found", file=sys.stderr) - sys.exit(1) - try: - result = clash_mod.clash( - model, element, clearance=args.clearance, tolerance=args.tolerance, scope=args.scope - ) - except ImportError: - print("Error: ifcopenshell geometry engine not available (C++ bindings required)", file=sys.stderr) - sys.exit(1) - elif args.command == "validate": - result = validate_mod.validate(model, express_rules=args.rules) - elif args.command == "schedule": - result = schedule.schedule(model, max_depth=args.depth) - elif args.command == "cost": - result = cost_mod.cost(model, max_depth=args.depth) - elif args.command == "contexts": - result = contexts_mod.contexts(model) - elif args.command == "materials": - result = materials_mod.materials(model) - elif args.command == "schema": - result = schema.schema(model, args.entity_type) - elif args.command == "render": - element_ids = None - if args.element: - try: - element_ids = [parse_element_id(part) for part in args.element.split(",")] - except ValueError: - print(f"Error: Invalid element ID(s): {args.element}", file=sys.stderr) - sys.exit(1) - out_path = args.output or (os.path.splitext(args.ifc_file)[0] + ".png") - try: - png_bytes = render_mod.render( - model, - selector=args.selector or None, - element_ids=element_ids, - view=args.view, - ) - except ImportError as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - except ValueError as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - with open(out_path, "wb") as f: - f.write(png_bytes) - print(f"Saved render to {out_path}", file=sys.stderr) - return - - print(format_output(result, args.output_format)) - - -if __name__ == "__main__": - main() diff --git a/src/ifcquery/build/lib/ifcquery/clash.py b/src/ifcquery/build/lib/ifcquery/clash.py deleted file mode 100644 index 7e3583e0aa..0000000000 --- a/src/ifcquery/build/lib/ifcquery/clash.py +++ /dev/null @@ -1,167 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -import multiprocessing -import sys -from typing import Any - -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.util.element - - -def _ref(element: ifcopenshell.entity_instance) -> dict[str, Any]: - """Serialize an element to a compact reference dict.""" - result: dict[str, Any] = {"id": element.id(), "type": element.is_a()} - if hasattr(element, "Name") and element.Name: - result["name"] = element.Name - return result - - -def _get_scope_elements( - model: ifcopenshell.file, element: ifcopenshell.entity_instance, scope: str -) -> tuple[set[ifcopenshell.entity_instance], str]: - """Return set of elements to check against and the effective scope used. - - Returns (elements, effective_scope) where effective_scope may differ from - the requested scope if fallback was needed. - """ - if scope == "storey": - container = ifcopenshell.util.element.get_container(element) - if container is not None: - siblings = set(ifcopenshell.util.element.get_contained(container)) - siblings.discard(element) - return siblings, "storey" - else: - print( - f"Warning: Element #{element.id()} has no spatial container, falling back to --scope all", - file=sys.stderr, - ) - - # scope == "all" or fallback - elements = set(model.by_type("IfcElement")) - elements -= set(model.by_type("IfcFeatureElement")) - elements.discard(element) - return elements, "all" - - -def _build_tree(model: ifcopenshell.file, elements: set[ifcopenshell.entity_instance]) -> ifcopenshell.geom.tree | None: - """Build geometry tree for given elements using iterator. - - Returns None if iterator fails to initialize (no geometry available). - """ - geom_settings = ifcopenshell.geom.settings() - geom_settings.set("use-world-coords", True) - geom_tree = ifcopenshell.geom.tree() - iterator = ifcopenshell.geom.iterator(geom_settings, model, multiprocessing.cpu_count(), include=list(elements)) - if not iterator.initialize(): - return None - while True: - geom_tree.add_element(iterator.get()) - if not iterator.next(): - break - return geom_tree - - -def _format_clash(clash_result, geom_tree: ifcopenshell.geom.tree, model: ifcopenshell.file) -> dict[str, Any]: - """Format a single clash result to dict.""" - # clash result .a/.b are C++ wrapper entity_instances without .Name; - # look up the Python entity from the model by id for proper serialization - other = model.by_id(clash_result.b.id()) - return { - "element": _ref(other), - "type": geom_tree.get_clash_type(clash_result.clash_type), - "distance": clash_result.distance, - "p1": list(clash_result.p1), - "p2": list(clash_result.p2), - } - - -def clash( - model: ifcopenshell.file, - element: ifcopenshell.entity_instance, - clearance: float | None = None, - tolerance: float = 0.002, - scope: str = "storey", -) -> dict[str, Any]: - """Check element for geometric clashes against other elements. - - :param model: The IFC model. - :param element: The element to check. - :param clearance: Minimum clearance distance; if provided, runs clearance check. - :param tolerance: Intersection tolerance in meters (default 0.002). - :param scope: Which elements to check against: "storey" or "all". - :return: Dict with clash results suitable for JSON serialization. - """ - result: dict[str, Any] = {"element": _ref(element)} - - # Get scope elements - scope_elements, effective_scope = _get_scope_elements(model, element, scope) - result["scope"] = effective_scope - - if not scope_elements: - result["pass"] = True - result["checks"] = {"intersection": {"pass": True, "tolerance": tolerance, "clashes": []}} - if clearance is not None: - result["checks"]["clearance"] = {"pass": True, "clearance": clearance, "clashes": []} - return result - - # Build geometry tree for target element + scope elements - all_elements = scope_elements | {element} - geom_tree = _build_tree(model, all_elements) - - if geom_tree is None: - result["pass"] = None - result["error"] = f"No geometry for element #{element.id()}" - return result - - # Run intersection check - intersection_clashes = geom_tree.clash_intersection_many( - [element], list(scope_elements), tolerance=tolerance, check_all=True - ) - intersection_results = [_format_clash(c, geom_tree, model) for c in intersection_clashes] - checks: dict[str, Any] = { - "intersection": { - "pass": len(intersection_results) == 0, - "tolerance": tolerance, - "clashes": intersection_results, - } - } - - all_pass = len(intersection_results) == 0 - - # Run clearance check if requested - if clearance is not None: - clearance_clashes = geom_tree.clash_clearance_many( - [element], list(scope_elements), clearance=clearance, check_all=True - ) - clearance_results = [_format_clash(c, geom_tree, model) for c in clearance_clashes] - checks["clearance"] = { - "pass": len(clearance_results) == 0, - "clearance": clearance, - "clashes": clearance_results, - } - if clearance_results: - all_pass = False - - result["pass"] = all_pass - result["checks"] = checks - return result diff --git a/src/ifcquery/build/lib/ifcquery/contexts.py b/src/ifcquery/build/lib/ifcquery/contexts.py deleted file mode 100644 index 912df173d7..0000000000 --- a/src/ifcquery/build/lib/ifcquery/contexts.py +++ /dev/null @@ -1,44 +0,0 @@ -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -import ifcopenshell - - -def contexts(model: ifcopenshell.file) -> list[dict]: - """Return all geometric representation contexts and subcontexts. - - :param model: The in-memory IFC model. - :return: List of dicts with id, type, context_type, context_identifier, - and (for subcontexts) target_view and parent_context_id. - """ - results = [] - for ctx in model.by_type("IfcGeometricRepresentationContext"): - entry = { - "id": ctx.id(), - "type": ctx.is_a(), - "context_type": getattr(ctx, "ContextType", None), - "context_identifier": getattr(ctx, "ContextIdentifier", None), - } - if ctx.is_a("IfcGeometricRepresentationSubContext"): - entry["target_view"] = ctx.TargetView - parent = ctx.ParentContext - entry["parent_context_id"] = parent.id() if parent else None - results.append(entry) - return results diff --git a/src/ifcquery/build/lib/ifcquery/cost.py b/src/ifcquery/build/lib/ifcquery/cost.py deleted file mode 100644 index ef9559d597..0000000000 --- a/src/ifcquery/build/lib/ifcquery/cost.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.cost as cost_util - - -def _cost_item_to_dict(item: ifcopenshell.entity_instance, max_depth: int | None, depth: int) -> dict[str, Any]: - raw_values = cost_util.get_cost_values(item) - values = [{"formula": v.get("label", ""), "category": v.get("category")} for v in raw_values] - - if max_depth is not None and depth >= max_depth: - child_count = len(cost_util.get_nested_cost_items(item)) - subitems = {"truncated": True, "count": child_count} if child_count else [] - else: - subitems = [_cost_item_to_dict(sub, max_depth, depth + 1) for sub in cost_util.get_nested_cost_items(item)] - - return { - "id": item.id(), - "name": getattr(item, "Name", None), - "values": values, - "subitems": subitems, - } - - -def cost(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]: - """Return a list of IfcCostSchedule entries with nested cost item trees. - - max_depth limits how many levels of subitems are expanded (None = unlimited). - At the cutoff level, subitems is replaced with {"truncated": True, "count": N}. - """ - result = [] - for cost_schedule in model.by_type("IfcCostSchedule"): - items = [_cost_item_to_dict(i, max_depth, depth=1) for i in cost_util.get_root_cost_items(cost_schedule)] - result.append( - { - "id": cost_schedule.id(), - "name": getattr(cost_schedule, "Name", None), - "predefined_type": getattr(cost_schedule, "PredefinedType", None), - "items": items, - } - ) - return result diff --git a/src/ifcquery/build/lib/ifcquery/info.py b/src/ifcquery/build/lib/ifcquery/info.py deleted file mode 100644 index 74823bd826..0000000000 --- a/src/ifcquery/build/lib/ifcquery/info.py +++ /dev/null @@ -1,118 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.element -import ifcopenshell.util.placement - - -def _serialize_attribute(value: Any) -> Any: - """Convert an IFC attribute value to a JSON-serializable form.""" - if isinstance(value, ifcopenshell.entity_instance): - return {"id": value.id(), "type": value.is_a()} - if isinstance(value, tuple): - return [_serialize_attribute(v) for v in value] - return value - - -def _material_to_dict(material: ifcopenshell.entity_instance | None) -> dict[str, Any] | None: - """Convert a material entity to a summary dict.""" - if material is None: - return None - result: dict[str, Any] = { - "id": material.id(), - "type": material.is_a(), - } - if hasattr(material, "Name"): - result["name"] = material.Name - return result - - -def info(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: - """Return deep inspection data for an element.""" - result: dict[str, Any] = { - "id": element.id(), - "type": element.is_a(), - } - - # Direct attributes via get_info() which returns a dict of all attributes - element_info = element.get_info() - attrs = {} - for key, value in element_info.items(): - if key in ("id", "type"): - continue - attrs[key] = _serialize_attribute(value) - result["attributes"] = attrs - - # Property sets and quantity sets - try: - psets = ifcopenshell.util.element.get_psets(element) - if psets: - result["property_sets"] = psets - except Exception: - pass - - # Element type - try: - element_type = ifcopenshell.util.element.get_type(element) - if element_type: - type_info: dict[str, Any] = { - "id": element_type.id(), - "type": element_type.is_a(), - } - if hasattr(element_type, "Name"): - type_info["name"] = element_type.Name - result["element_type"] = type_info - except Exception: - pass - - # Material - try: - material = ifcopenshell.util.element.get_material(element) - mat_dict = _material_to_dict(material) - if mat_dict: - result["material"] = mat_dict - except Exception: - pass - - # Spatial container - try: - container = ifcopenshell.util.element.get_container(element) - if container: - result["container"] = { - "id": container.id(), - "type": container.is_a(), - "name": container.Name if hasattr(container, "Name") else None, - } - except Exception: - pass - - # Placement (as 4x4 matrix) - try: - if hasattr(element, "ObjectPlacement") and element.ObjectPlacement: - matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) - result["placement"] = matrix.tolist() - except Exception: - pass - - return result diff --git a/src/ifcquery/build/lib/ifcquery/materials.py b/src/ifcquery/build/lib/ifcquery/materials.py deleted file mode 100644 index f1b5ac7b2c..0000000000 --- a/src/ifcquery/build/lib/ifcquery/materials.py +++ /dev/null @@ -1,86 +0,0 @@ -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -import ifcopenshell - - -def materials(model: ifcopenshell.file) -> list[dict]: - """Return all materials and material sets from the model. - - :param model: The in-memory IFC model. - :return: List of dicts covering IfcMaterial, IfcMaterialLayerSet, - IfcMaterialConstituentSet, and IfcMaterialProfileSet entities. - """ - results = [] - - for m in model.by_type("IfcMaterial"): - results.append({ - "id": m.id(), - "type": "IfcMaterial", - "name": m.Name, - "category": getattr(m, "Category", None), - }) - - for ls in model.by_type("IfcMaterialLayerSet"): - layers = [] - for layer in (ls.MaterialLayers or []): - layers.append({ - "name": layer.Name, - "thickness": layer.LayerThickness, - "material": layer.Material.Name if layer.Material else None, - "is_ventilated": layer.IsVentilated, - }) - results.append({ - "id": ls.id(), - "type": "IfcMaterialLayerSet", - "name": ls.LayerSetName, - "layers": layers, - }) - - for cs in model.by_type("IfcMaterialConstituentSet"): - constituents = [] - for c in (cs.MaterialConstituents or []): - constituents.append({ - "name": c.Name, - "material": c.Material.Name if c.Material else None, - "fraction": c.Fraction, - }) - results.append({ - "id": cs.id(), - "type": "IfcMaterialConstituentSet", - "name": cs.Name, - "constituents": constituents, - }) - - for ps in model.by_type("IfcMaterialProfileSet"): - profiles = [] - for p in (ps.MaterialProfiles or []): - profiles.append({ - "name": p.Name, - "material": p.Material.Name if p.Material else None, - }) - results.append({ - "id": ps.id(), - "type": "IfcMaterialProfileSet", - "name": ps.Name, - "profiles": profiles, - }) - - return results diff --git a/src/ifcquery/build/lib/ifcquery/relations.py b/src/ifcquery/build/lib/ifcquery/relations.py deleted file mode 100644 index f8f12e9989..0000000000 --- a/src/ifcquery/build/lib/ifcquery/relations.py +++ /dev/null @@ -1,169 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.element -import ifcopenshell.util.system - - -def _ref(element: ifcopenshell.entity_instance) -> dict[str, Any]: - """Serialize an element to a compact reference dict.""" - result: dict[str, Any] = {"id": element.id(), "type": element.is_a()} - if hasattr(element, "Name") and element.Name: - result["name"] = element.Name - return result - - -def _ref_or_none(element: ifcopenshell.entity_instance | None) -> dict[str, Any] | None: - return _ref(element) if element is not None else None - - -def _ref_list(elements) -> list[dict[str, Any]]: - return [_ref(e) for e in elements] - - -def _traverse_up(element: ifcopenshell.entity_instance) -> list[dict[str, Any]]: - """Walk the hierarchy from element up to IfcProject.""" - chain = [_ref(element)] - current = element - while True: - parent = ifcopenshell.util.element.get_parent(current) - if parent is None: - break - chain.append(_ref(parent)) - current = parent - return chain - - -def _all_relations(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: - """Collect all relationships for an element.""" - result: dict[str, Any] = { - "id": element.id(), - "type": element.is_a(), - } - if hasattr(element, "Name") and element.Name: - result["name"] = element.Name - - # Hierarchy (upward) - hierarchy: dict[str, Any] = {} - parent = ifcopenshell.util.element.get_parent(element) - if parent is not None: - hierarchy["parent"] = _ref(parent) - container = ifcopenshell.util.element.get_container(element) - if container is not None: - hierarchy["container"] = _ref(container) - aggregate = ifcopenshell.util.element.get_aggregate(element) - if aggregate is not None: - hierarchy["aggregate"] = _ref(aggregate) - nest = ifcopenshell.util.element.get_nest(element) - if nest is not None: - hierarchy["nest"] = _ref(nest) - filled_void = ifcopenshell.util.element.get_filled_void(element) - if filled_void is not None: - hierarchy["filled_void"] = _ref(filled_void) - voided_element = ifcopenshell.util.element.get_voided_element(element) - if voided_element is not None: - hierarchy["voided_element"] = _ref(voided_element) - if hierarchy: - result["hierarchy"] = hierarchy - - # Children (downward) - children: dict[str, Any] = {} - contained = ifcopenshell.util.element.get_contained(element) - if contained: - children["contained"] = _ref_list(contained) - parts = ifcopenshell.util.element.get_parts(element) - if parts: - children["parts"] = _ref_list(parts) - components = ifcopenshell.util.element.get_components(element) - if components: - children["components"] = _ref_list(components) - openings = list(ifcopenshell.util.element.get_openings(element)) - if openings: - children["openings"] = _ref_list(openings) - if children: - result["children"] = children - - # Type relationship - type_relationship: dict[str, Any] = {} - element_type = ifcopenshell.util.element.get_type(element) - if element_type is not None: - type_relationship["type_of"] = _ref(element_type) - try: - occurrences = ifcopenshell.util.element.get_types(element) - if occurrences: - type_relationship["occurrences"] = _ref_list(occurrences) - except Exception: - pass - if type_relationship: - result["type_relationship"] = type_relationship - - # Groups - groups = ifcopenshell.util.element.get_groups(element) - if groups: - result["groups"] = _ref_list(groups) - - # Systems - systems = ifcopenshell.util.system.get_element_systems(element) - if systems: - result["systems"] = _ref_list(systems) - - # Zones - zones = ifcopenshell.util.system.get_element_zones(element) - if zones: - result["zones"] = _ref_list(zones) - - # Material - material = ifcopenshell.util.element.get_material(element) - if material is not None: - result["material"] = _ref(material) - - # Referenced structures - referenced = ifcopenshell.util.element.get_referenced_structures(element) - if referenced: - result["referenced_structures"] = _ref_list(referenced) - - # Connections - connections: dict[str, Any] = {} - connected_to = ifcopenshell.util.system.get_connected_to(element) - if connected_to: - connections["connected_to"] = _ref_list(connected_to) - connected_from = ifcopenshell.util.system.get_connected_from(element) - if connected_from: - connections["connected_from"] = _ref_list(connected_from) - ports = ifcopenshell.util.system.get_ports(element) - if ports: - connections["ports"] = _ref_list(ports) - if connections: - result["connections"] = connections - - return result - - -def relations( - model: ifcopenshell.file, element: ifcopenshell.entity_instance, traverse: str | None = None -) -> dict[str, Any] | list[dict[str, Any]]: - """Return relationships for an element, or hierarchy chain if traverse='up'.""" - if traverse == "up": - return _traverse_up(element) - return _all_relations(model, element) diff --git a/src/ifcquery/build/lib/ifcquery/render.py b/src/ifcquery/build/lib/ifcquery/render.py deleted file mode 100644 index 709e829e2f..0000000000 --- a/src/ifcquery/build/lib/ifcquery/render.py +++ /dev/null @@ -1,377 +0,0 @@ -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -import multiprocessing -import os -import tempfile - -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.guid -import ifcopenshell.util.selector - -try: - import numpy as np - import pyvista as pv - - _HAS_PYVISTA = True -except ImportError: - _HAS_PYVISTA = False - -VIEWS = ("iso", "top", "south", "north", "east", "west") - - -def _apply_view(plotter: pv.Plotter, view: str) -> None: - """Set the camera to the requested named view. Z is up (IFC convention).""" - if view == "top": - plotter.view_xy() - elif view == "south": - # Camera at -Y looking toward +Y (south face of building) - plotter.view_xz(negative=True) - elif view == "north": - plotter.view_xz(negative=False) - elif view == "east": - plotter.view_yz(negative=False) - elif view == "west": - plotter.view_yz(negative=True) - else: - plotter.view_isometric() - # Ensure Z is world up for elevation views - if view not in ("top",): - plotter.camera.up = (0, 0, 1) - - -def _add_shape( - shape: object, - plotter: pv.Plotter, - highlight_ids: frozenset[int] | None, -) -> None: - """Triangulate and add a geometry shape to the plotter.""" - geom = shape.geometry - verts = np.array(geom.verts, dtype=float).reshape(-1, 3) - if verts.size == 0: - return - - raw_faces = np.array(geom.faces, dtype=int) - if raw_faces.size == 0 or raw_faces.size % 3 != 0: - return # degenerate geometry from kernel — skip silently - faces = raw_faces.reshape(-1, 3) - material_ids = np.array(geom.material_ids, dtype=int) - - is_subject = highlight_ids is not None and shape.product.id() in highlight_ids - - for midx, mat in enumerate(geom.materials): - tri_mask = material_ids == midx - if not np.any(tri_mask): - continue - - sub_faces = faces[tri_mask] - faces_pv = np.hstack([np.full((sub_faces.shape[0], 1), 3, dtype=int), sub_faces]).ravel() - mesh = pv.PolyData(verts, faces_pv) - - if highlight_ids is not None and not is_subject: - color = (180, 180, 180) - opacity = 0.10 - else: - diffuse = np.clip(np.array(mat.diffuse.components), 0.0, 1.0) - color = tuple((diffuse * 255).astype(np.uint8)) - transparency = mat.transparency if mat.transparency == mat.transparency else 0.0 - opacity = float(np.clip(1.0 - transparency, 0.0, 1.0)) - - plotter.add_mesh(mesh, color=color, opacity=opacity, show_edges=False) - - -def _render_iterator( - iterator: object, - highlight_ids: list[int] | None, - view: str, -) -> bytes: - """Drive a geometry iterator into a pyvista plotter and return PNG bytes.""" - plotter = pv.Plotter(off_screen=True, window_size=(1280, 960)) - plotter.background_color = "white" - - while True: - try: - _add_shape(iterator.get(), plotter, highlight_ids=frozenset(highlight_ids) if highlight_ids else None) - except Exception: - pass # skip broken shapes, keep rendering the rest - if not iterator.next(): - break - - plotter.reset_camera() - _apply_view(plotter, view) - - tmp_fd, tmp_path = tempfile.mkstemp(suffix=".png") - os.close(tmp_fd) - try: - plotter.show(screenshot=tmp_path, auto_close=True) - with open(tmp_path, "rb") as f: - return f.read() - finally: - try: - os.unlink(tmp_path) - except OSError: - pass - - -def _build_geom_settings(model: ifcopenshell.file) -> ifcopenshell.geom.settings: - """Build geometry settings, excluding Clearance subcontexts.""" - settings = ifcopenshell.geom.settings() - settings.set("use-world-coords", True) - - clearance_ids = { - c.id() - for c in model.by_type("IfcGeometricRepresentationSubContext") - if c.ContextIdentifier == "Clearance" - } - if clearance_ids: - ctx_ids = [ - c.id() - for c in model.by_type("IfcGeometricRepresentationContext") - if c.id() not in clearance_ids - ] - if ctx_ids: - settings.set("context-ids", ctx_ids) - - return settings - - -def _get_occurrence_class(type_entity) -> str: - """Derive the occurrence IFC class from a type entity class name.""" - type_class = type_entity.is_a() - if type_class.endswith("Type"): - return type_class[:-4] - return "IfcBuildingElementProxy" - - -def _make_type_occurrence(model: ifcopenshell.file, type_entity) -> object | None: - """Create a temporary occurrence for *type_entity* using its RepresentationMaps. - - The occurrence is added to *model* and references the type's existing - RepresentationMap entities via IfcMappedItem. Returns the occurrence entity, - or ``None`` when the type has no usable RepresentationMaps. - - .. note:: - This function is intended for use on a temporary model copy. The - caller is responsible for discarding that copy after rendering. - """ - rep_maps = getattr(type_entity, "RepresentationMaps", None) or [] - if not rep_maps: - return None - - # One IfcMappedItem per RepresentationMap. - mapped_items = [] - for rep_map in rep_maps: - origin = model.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)) - transform = model.create_entity( - "IfcCartesianTransformationOperator3D", - LocalOrigin=origin, - ) - mapped_item = model.create_entity( - "IfcMappedItem", - MappingSource=rep_map, - MappingTarget=transform, - ) - mapped_items.append(mapped_item) - - context = rep_maps[0].MappedRepresentation.ContextOfItems - shape_rep = model.create_entity( - "IfcShapeRepresentation", - ContextOfItems=context, - RepresentationIdentifier="Body", - RepresentationType="MappedRepresentation", - Items=mapped_items, - ) - prod_def_shape = model.create_entity( - "IfcProductDefinitionShape", - Representations=[shape_rep], - ) - - # Identity placement. - pt = model.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)) - z_dir = model.create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)) - x_dir = model.create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)) - axis2 = model.create_entity("IfcAxis2Placement3D", Location=pt, Axis=z_dir, RefDirection=x_dir) - placement = model.create_entity("IfcLocalPlacement", RelativePlacement=axis2) - - occ_class = _get_occurrence_class(type_entity) - try: - occurrence = model.create_entity( - occ_class, - GlobalId=ifcopenshell.guid.new(), - Name=f"_type_preview_{type_entity.id()}", - ObjectPlacement=placement, - Representation=prod_def_shape, - ) - except Exception: - occurrence = model.create_entity( - "IfcBuildingElementProxy", - GlobalId=ifcopenshell.guid.new(), - Name=f"_type_preview_{type_entity.id()}", - ObjectPlacement=placement, - Representation=prod_def_shape, - ) - return occurrence - - -def _render_with_types( - model: ifcopenshell.file, - types: list, - selector_elements: list | None, - element_ids: list[int] | None, - type_highlight_ids: set[int], - view: str, -) -> bytes: - """Render type entities by creating occurrences in a temporary model copy. - - *types* — list of IfcTypeProduct entities to render. - *selector_elements* — non-type elements from the selector (or ``None``). - *element_ids* — original highlight IDs (may contain type IDs). - *type_highlight_ids* — subset of *element_ids* that are type IDs. - """ - tmp_fd, tmp_path = tempfile.mkstemp(suffix=".ifc") - os.close(tmp_fd) - try: - model.write(tmp_path) - tmp = ifcopenshell.open(tmp_path) - - # Map original type step-ID → new occurrence step-ID in the tmp model. - type_id_to_occ_id: dict[int, int] = {} - for t in types: - tmp_type = tmp.by_id(t.id()) - occ = _make_type_occurrence(tmp, tmp_type) - if occ: - type_id_to_occ_id[t.id()] = occ.id() - - if not type_id_to_occ_id: - raise ValueError("Type entities have no RepresentationMaps to render") - - include = [tmp.by_id(occ_id) for occ_id in type_id_to_occ_id.values()] - if selector_elements: - include.extend(tmp.by_id(e.id()) for e in selector_elements) - - settings = _build_geom_settings(tmp) - iterator = ifcopenshell.geom.iterator(settings, tmp, multiprocessing.cpu_count(), include=include) - if not iterator.initialize(): - raise ValueError("Type entities have no renderable geometry") - - # Remap type IDs → occurrence IDs in the highlight list. - new_highlight = None - if element_ids: - new_highlight = [] - for hid in element_ids: - if hid in type_highlight_ids: - mapped = type_id_to_occ_id.get(hid) - if mapped: - new_highlight.append(mapped) - else: - new_highlight.append(hid) - - return _render_iterator(iterator, new_highlight, view) - finally: - try: - os.unlink(tmp_path) - except OSError: - pass - - -def render( - model: ifcopenshell.file, - selector: str | None = None, - element_ids: list[int] | None = None, - view: str = "iso", -) -> bytes: - """Render IFC model geometry to a PNG image. - - Supports both element instances and element types (e.g. ``IfcWallType``). - When type entities are targeted — via *selector* or *element_ids* — a - temporary copy of the model is used to create proxy occurrences that - reference the type's RepresentationMaps; the original model is not - modified. - - :param model: The in-memory IFC model. - :param selector: ifcopenshell selector to restrict rendered elements - (e.g. ``'IfcWall'``, ``'IfcWallType'``, or - ``'IfcBuildingStorey[Name="Ground Floor"]'``). - When omitted the whole model is rendered. - :param element_ids: Step IDs of elements (or types) to highlight. The - rest of the model is rendered in translucent grey so the highlighted - items stand out. - :param view: Camera angle: ``iso``, ``top``, ``south``, ``north``, - ``east``, or ``west``. Defaults to ``iso``. - :return: PNG image as raw bytes. - :raises ImportError: If pyvista is not installed. - :raises ValueError: If the selector matches nothing or the model has no - renderable geometry. - """ - if not _HAS_PYVISTA: - raise ImportError("pyvista is not installed. Install with: pip install pyvista") - - # --- Partition selector results into types and elements --- - if selector: - matched = list(ifcopenshell.util.selector.filter_elements(model, selector)) - if not matched: - raise ValueError(f"Selector {selector!r} matched no elements") - types = [e for e in matched if e.is_a("IfcTypeProduct")] - selector_elements: list | None = [e for e in matched if not e.is_a("IfcTypeProduct")] - else: - types = [] - selector_elements = None # no restriction — render all elements - - # --- Collect any type entities from element_ids --- - type_highlight_ids: set[int] = set() - if element_ids: - for eid in element_ids: - entity = model.by_id(eid) - if entity.is_a("IfcTypeProduct"): - type_highlight_ids.add(eid) - seen = {t.id() for t in types} - if eid not in seen: - types.append(entity) - - # --- Delegate to temp-copy path when any type entities are involved --- - if types: - return _render_with_types(model, types, selector_elements, element_ids, type_highlight_ids, view) - - # --- Regular element rendering --- - settings = _build_geom_settings(model) - - if selector_elements is not None: - if not selector_elements: - raise ValueError(f"Selector {selector!r} matched only type entities (use a type selector or IfcElement)") - iterator = ifcopenshell.geom.iterator( - settings, - model, - multiprocessing.cpu_count(), - include=selector_elements, - ) - else: - exclude = list(model.by_type("IfcOpeningElement")) - iterator = ifcopenshell.geom.iterator( - settings, - model, - multiprocessing.cpu_count(), - exclude=exclude if exclude else None, - ) - - if not iterator.initialize(): - raise ValueError("No renderable geometry found in model (or selector matched nothing)") - - return _render_iterator(iterator, element_ids, view) diff --git a/src/ifcquery/build/lib/ifcquery/schedule.py b/src/ifcquery/build/lib/ifcquery/schedule.py deleted file mode 100644 index b3e3ae45d6..0000000000 --- a/src/ifcquery/build/lib/ifcquery/schedule.py +++ /dev/null @@ -1,56 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.sequence as seq - - -def _task_to_dict(task: ifcopenshell.entity_instance, max_depth: int | None, depth: int) -> dict[str, Any]: - task_time = task.TaskTime - start = None - finish = None - if task_time: - start = task_time.ScheduleStart - finish = task_time.ScheduleFinish - - outputs = [] - for product in seq.get_task_outputs(task): - outputs.append({"id": product.id(), "type": product.is_a(), "name": getattr(product, "Name", None)}) - - if max_depth is not None and depth >= max_depth: - child_count = len(seq.get_nested_tasks(task)) - subtasks = {"truncated": True, "count": child_count} if child_count else [] - else: - subtasks = [_task_to_dict(sub, max_depth, depth + 1) for sub in seq.get_nested_tasks(task)] - - return { - "id": task.id(), - "name": getattr(task, "Name", None), - "start": start, - "finish": finish, - "is_milestone": bool(task.IsMilestone) if hasattr(task, "IsMilestone") else False, - "outputs": outputs, - "subtasks": subtasks, - } - - -def schedule(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]: - """Return a list of IfcWorkSchedule entries with nested task trees. - - max_depth limits how many levels of subtasks are expanded (None = unlimited). - At the cutoff level, subtasks is replaced with {"truncated": True, "count": N}. - """ - result = [] - for work_schedule in model.by_type("IfcWorkSchedule"): - tasks = [_task_to_dict(t, max_depth, depth=1) for t in seq.get_root_tasks(work_schedule)] - result.append( - { - "id": work_schedule.id(), - "name": getattr(work_schedule, "Name", None), - "predefined_type": getattr(work_schedule, "PredefinedType", None), - "tasks": tasks, - } - ) - return result diff --git a/src/ifcquery/build/lib/ifcquery/schema.py b/src/ifcquery/build/lib/ifcquery/schema.py deleted file mode 100644 index ad47486db2..0000000000 --- a/src/ifcquery/build/lib/ifcquery/schema.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.doc - - -def schema(model: ifcopenshell.file, entity_type: str) -> dict[str, Any]: - """Return IFC class documentation for entity_type from model's schema version.""" - schema_name = model.schema - try: - doc = ifcopenshell.util.doc.get_entity_doc(schema_name, entity_type) - except Exception: - return {"error": f"Unknown entity: {entity_type}"} - if not doc: - return {"error": f"Unknown entity: {entity_type}"} - return dict(doc) diff --git a/src/ifcquery/build/lib/ifcquery/select.py b/src/ifcquery/build/lib/ifcquery/select.py deleted file mode 100644 index c28a159b90..0000000000 --- a/src/ifcquery/build/lib/ifcquery/select.py +++ /dev/null @@ -1,41 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.selector - - -def select(model: ifcopenshell.file, query: str) -> list[dict[str, Any]]: - """Filter elements using selector syntax and return matching element summaries.""" - elements = ifcopenshell.util.selector.filter_elements(model, query) - results = [] - for element in sorted(elements, key=lambda e: e.id()): - entry: dict[str, Any] = { - "id": element.id(), - "type": element.is_a(), - "repr": str(element), - } - if hasattr(element, "Name"): - entry["name"] = element.Name - results.append(entry) - return results diff --git a/src/ifcquery/build/lib/ifcquery/summary.py b/src/ifcquery/build/lib/ifcquery/summary.py deleted file mode 100644 index d13f070472..0000000000 --- a/src/ifcquery/build/lib/ifcquery/summary.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -from collections import Counter -from typing import Any - -import ifcopenshell - - -def summary(model: ifcopenshell.file) -> dict[str, Any]: - """Return a model overview with schema, element counts, and project info.""" - # Count elements by IFC type, sorted by count descending - type_counter: Counter[str] = Counter() - total = 0 - for entity in model: - type_counter[entity.is_a()] += 1 - total += 1 - - result: dict[str, Any] = { - "schema": model.schema, - "total_entities": total, - } - - projects = model.by_type("IfcProject") - if projects: - project = projects[0] - result["project"] = { - "id": project.id(), - "name": project.Name, - "description": project.Description, - } - - result["types"] = dict(type_counter.most_common()) - return result diff --git a/src/ifcquery/build/lib/ifcquery/tree.py b/src/ifcquery/build/lib/ifcquery/tree.py deleted file mode 100644 index 1cbbaeb986..0000000000 --- a/src/ifcquery/build/lib/ifcquery/tree.py +++ /dev/null @@ -1,68 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -# IfcQuery - IFC model interrogation CLI -# Copyright (C) 2026 Bruno Postle -# -# This file is part of IfcQuery. -# -# IfcQuery is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcQuery is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcQuery. If not, see . - -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.util.element - - -def _element_summary(element: ifcopenshell.entity_instance) -> dict[str, Any]: - """Return a minimal summary dict for an element.""" - return { - "id": element.id(), - "type": element.is_a(), - "name": element.Name if hasattr(element, "Name") else None, - } - - -def _build_spatial_node(element: ifcopenshell.entity_instance) -> dict[str, Any]: - """Recursively build a spatial tree node.""" - node = _element_summary(element) - - # Get aggregated children (Site in Project, Building in Site, Storey in Building, etc.) - aggregates = [] - for rel in getattr(element, "IsDecomposedBy", []): - for child in rel.RelatedObjects: - aggregates.append(_build_spatial_node(child)) - - # Get contained elements (walls, slabs, etc. in a storey/space) - contained = [] - for rel in getattr(element, "ContainsElements", []): - for child in rel.RelatedElements: - contained.append(_element_summary(child)) - - if aggregates: - node["children"] = aggregates - if contained: - node["elements"] = contained - - return node - - -def tree(model: ifcopenshell.file) -> dict[str, Any] | list[dict[str, Any]]: - """Return the spatial hierarchy tree starting from IfcProject.""" - projects = model.by_type("IfcProject") - if not projects: - return {"error": "No IfcProject found in model"} - if len(projects) == 1: - return _build_spatial_node(projects[0]) - return [_build_spatial_node(p) for p in projects] diff --git a/src/ifcquery/build/lib/ifcquery/validate.py b/src/ifcquery/build/lib/ifcquery/validate.py deleted file mode 100644 index d35d9150af..0000000000 --- a/src/ifcquery/build/lib/ifcquery/validate.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated with the assistance of an AI coding tool. -from __future__ import annotations - -from typing import Any - -import ifcopenshell -import ifcopenshell.validate - - -def validate(model: ifcopenshell.file, express_rules: bool = False) -> dict[str, Any]: - """Validate the model and return a dict with 'valid' bool and 'issues' list.""" - logger = ifcopenshell.validate.json_logger() - ifcopenshell.validate.validate(model, logger, express_rules=express_rules) - issues = [{"level": s["level"], "message": s["message"]} for s in logger.statements] - return {"valid": len(issues) == 0, "issues": issues}