mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +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.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