mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-16 21:42:19 +00:00
Remove ifcquery build artifacts and add to .gitignore
This commit is contained in:
@@ -113,3 +113,4 @@ dev_environment.bat
|
|||||||
|
|
||||||
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||||
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||||
|
src/ifcquery/build/
|
||||||
|
|||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
__version__ = version = "0.0.0"
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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: <ifc_file>.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()
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# IfcQuery - IFC model interrogation CLI
|
|
||||||
# Copyright (C) 2026 Bruno Postle <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# IfcQuery - IFC model interrogation CLI
|
|
||||||
# Copyright (C) 2026 Bruno Postle <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
# IfcQuery - IFC model interrogation CLI
|
|
||||||
# Copyright (C) 2026 Bruno Postle <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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 <bruno@postle.net>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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]
|
|
||||||
@@ -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}
|
|
||||||
Reference in New Issue
Block a user