From b1f1de954dd4b8e2e9473aa442a22e7ced915f54 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Mon, 9 Feb 2026 22:59:11 +0000 Subject: [PATCH] Rename ifcapi to ifcedit, README and black --- src/ifcedit/README.md | 208 +++++++++++++++ .../ifcapi => ifcedit/ifcedit}/__init__.py | 10 +- .../ifcapi => ifcedit/ifcedit}/__main__.py | 16 +- .../ifcapi => ifcedit/ifcedit}/coerce.py | 10 +- .../ifcapi => ifcedit/ifcedit}/discover.py | 36 +-- src/{ifcapi/ifcapi => ifcedit/ifcedit}/run.py | 12 +- src/{ifcapi => ifcedit}/pyproject.toml | 4 +- src/{ifcapi => ifcedit}/tests/__init__.py | 0 src/{ifcapi => ifcedit}/tests/conftest.py | 0 src/{ifcapi => ifcedit}/tests/test_coerce.py | 2 +- .../tests/test_discover.py | 2 +- src/{ifcapi => ifcedit}/tests/test_main.py | 32 +-- src/{ifcapi => ifcedit}/tests/test_run.py | 2 +- src/ifcquery/README.md | 252 ++++++++++++++++++ src/ifcquery/ifcquery/clash.py | 16 +- src/ifcquery/tests/test_clash.py | 4 +- 16 files changed, 528 insertions(+), 78 deletions(-) create mode 100644 src/ifcedit/README.md rename src/{ifcapi/ifcapi => ifcedit/ifcedit}/__init__.py (64%) rename src/{ifcapi/ifcapi => ifcedit/ifcedit}/__main__.py (92%) rename src/{ifcapi/ifcapi => ifcedit/ifcedit}/coerce.py (94%) rename src/{ifcapi/ifcapi => ifcedit/ifcedit}/discover.py (92%) rename src/{ifcapi/ifcapi => ifcedit/ifcedit}/run.py (91%) rename src/{ifcapi => ifcedit}/pyproject.toml (95%) rename src/{ifcapi => ifcedit}/tests/__init__.py (100%) rename src/{ifcapi => ifcedit}/tests/conftest.py (100%) rename src/{ifcapi => ifcedit}/tests/test_coerce.py (99%) rename src/{ifcapi => ifcedit}/tests/test_discover.py (98%) rename src/{ifcapi => ifcedit}/tests/test_main.py (69%) rename src/{ifcapi => ifcedit}/tests/test_run.py (98%) create mode 100644 src/ifcquery/README.md diff --git a/src/ifcedit/README.md b/src/ifcedit/README.md new file mode 100644 index 0000000000..1179455464 --- /dev/null +++ b/src/ifcedit/README.md @@ -0,0 +1,208 @@ +# ifcedit + +A CLI wrapper that exposes all 350+ `ifcopenshell.api` mutation functions as +shell commands. Functions are auto-discovered at runtime via introspection -- +no hardcoded list to maintain. + +## Installation + +```bash +pip install ifcedit +``` + +Requires `ifcopenshell`. + +## Usage + +``` +ifcedit [options] [--format json|text] +``` + +Three subcommands: `list` to discover functions, `docs` to read their +documentation, and `run` to execute them. + +## Subcommands + +### list + +Discover available API modules and their functions. + +**List all modules:** + +```bash +ifcedit list +``` + +```json +[ + { + "module": "root", + "description": "Functions for creating project-level entities", + "functions": ["create_entity", "remove_product", "copy_class"], + "count": 3 + }, + { + "module": "spatial", + "description": "Functions for managing spatial relationships", + "functions": ["assign_container", "unassign_container"], + "count": 2 + } +] +``` + +**List functions in a module:** + +```bash +ifcedit list root +``` + +```json +[ + { + "name": "create_entity", + "description": "Create an IFC entity with optional initial attributes", + "params": [ + {"name": "ifc_class", "type": "str", "required": true}, + {"name": "name", "type": "Optional[str]"} + ] + } +] +``` + +### docs + +Show full documentation for a specific function, including parameter +descriptions from docstrings and return type. + +```bash +ifcedit docs root.create_entity +``` + +```json +{ + "module": "root", + "function": "create_entity", + "description": "Create an IFC entity with optional initial attributes", + "long_description": "This function creates a new entity instance...", + "params": [ + { + "name": "ifc_class", + "type": "str", + "required": true, + "description": "The IFC class name (e.g. 'IfcWall', 'IfcProject')" + }, + { + "name": "name", + "type": "Optional[str]", + "description": "Optional name attribute" + } + ], + "return_type": "ifcopenshell.entity_instance", + "return_description": "The newly created entity instance" +} +``` + +### run + +Execute an API function against an IFC file. Parameters are passed as +`--key value` pairs after the function name. + +```bash +ifcedit run model.ifc root.create_entity --ifc_class IfcWall --name "My Wall" +``` + +```json +{ + "ok": true, + "result": {"id": 42, "type": "IfcWall", "name": "My Wall"} +} +``` + +**Options:** + +- `-o, --output ` -- write to a different file instead of overwriting the input +- `--dry-run` -- validate parameters without executing or saving + +```bash +# Save to a new file +ifcedit run model.ifc root.create_entity -o out.ifc --ifc_class IfcWall + +# Validate without executing +ifcedit run model.ifc root.create_entity --dry-run --ifc_class IfcWall +``` + +Dry-run output shows the resolved parameters: + +```json +{ + "ok": true, + "dry_run": true, + "module": "root", + "function": "create_entity", + "args": {"ifc_class": "IfcWall", "name": "My Wall"} +} +``` + +## Parameter type coercion + +CLI strings are automatically converted to the types expected by each API +function, using the function's type annotations: + +| Type | CLI input | Python value | +|------|-----------|--------------| +| `str` | `"hello"` | `"hello"` | +| `int` | `"42"` or `"#42"` | `42` | +| `float` | `"3.14"` | `3.14` | +| `bool` | `"true"`, `"1"`, `"yes"` | `True` | +| `Optional[X]` | `"none"` | `None` | +| `entity_instance` | `"42"` or `"#42"` | resolved from model by step ID | +| `list[entity_instance]` | `"5,6,7"` or `"[5, 6, 7]"` | list of resolved entities | +| `dict` | `'{"key": "val"}'` | parsed JSON object | +| `Literal["A", "B"]` | `"A"` | validated against allowed values | + +## Examples + +```bash +# Create a project +ifcedit run model.ifc root.create_entity --ifc_class IfcProject --name "My Project" + +# Assign an element to a storey +ifcedit run model.ifc spatial.assign_container --products 10 --relating_structure 4 + +# Assign multiple elements at once +ifcedit run model.ifc aggregate.assign_object --products "5,6,7" --relating_object 1 + +# Add a property set +ifcedit run model.ifc pset.add_pset --product 10 --name "Pset_WallCommon" + +# Edit properties +ifcedit run model.ifc pset.edit_pset --pset 15 \ + --properties '{"IsExternal": true, "FireRating": "2HR"}' +``` + +## Error handling + +Errors are reported in the JSON response: + +```json +{ + "ok": false, + "error": "Entity #999 not found in model" +} +``` + +Exit code is 0 on success, 1 on error. + +## Relationship to ifcquery + +`ifcedit` and `ifcquery` are complementary tools: + +- **ifcquery** reads and inspects IFC models (summary, tree, info, select, relations, clash) +- **ifcedit** modifies IFC models by wrapping `ifcopenshell.api` functions + +A typical workflow: inspect with `ifcquery`, look up the right API function +with `ifcedit docs`, then apply changes with `ifcedit run`. + +## License + +LGPLv3+ -- see the IfcOpenShell project license. diff --git a/src/ifcapi/ifcapi/__init__.py b/src/ifcedit/ifcedit/__init__.py similarity index 64% rename from src/ifcapi/ifcapi/__init__.py rename to src/ifcedit/ifcedit/__init__.py index 4dc5c7b399..82399cf853 100644 --- a/src/ifcapi/ifcapi/__init__.py +++ b/src/ifcedit/ifcedit/__init__.py @@ -1,19 +1,19 @@ -# IfcApi - CLI wrapper for ifcopenshell.api mutation functions +# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions # Copyright (C) 2025 Bruno Postle # -# This file is part of IfcApi. +# This file is part of IfcEdit. # -# IfcApi is free software: you can redistribute it and/or modify +# IfcEdit 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. # -# IfcApi is distributed in the hope that it will be useful, +# IfcEdit 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 IfcApi. If not, see . +# along with IfcEdit. If not, see . __version__ = version = "0.0.0" diff --git a/src/ifcapi/ifcapi/__main__.py b/src/ifcedit/ifcedit/__main__.py similarity index 92% rename from src/ifcapi/ifcapi/__main__.py rename to src/ifcedit/ifcedit/__main__.py index 8f5ea59697..9b307086c8 100644 --- a/src/ifcapi/ifcapi/__main__.py +++ b/src/ifcedit/ifcedit/__main__.py @@ -1,20 +1,20 @@ -# IfcApi - CLI wrapper for ifcopenshell.api mutation functions +# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions # Copyright (C) 2025 Bruno Postle # -# This file is part of IfcApi. +# This file is part of IfcEdit. # -# IfcApi is free software: you can redistribute it and/or modify +# IfcEdit 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. # -# IfcApi is distributed in the hope that it will be useful, +# IfcEdit 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 IfcApi. If not, see . +# along with IfcEdit. If not, see . from __future__ import annotations @@ -24,8 +24,8 @@ import sys import ifcopenshell -from ifcapi.discover import function_docs, list_functions, list_modules -from ifcapi.run import run_api +from ifcedit.discover import function_docs, list_functions, list_modules +from ifcedit.run import run_api def format_output(data, fmt: str) -> str: @@ -138,7 +138,7 @@ def _parse_extra_args(extra: list[str]) -> dict[str, str]: def main(): parser = argparse.ArgumentParser( - prog="ifcapi", + prog="ifcedit", description="CLI wrapper for ifcopenshell.api IFC model mutation functions", ) parser.add_argument( diff --git a/src/ifcapi/ifcapi/coerce.py b/src/ifcedit/ifcedit/coerce.py similarity index 94% rename from src/ifcapi/ifcapi/coerce.py rename to src/ifcedit/ifcedit/coerce.py index 5de58fc306..0c0062679a 100644 --- a/src/ifcapi/ifcapi/coerce.py +++ b/src/ifcedit/ifcedit/coerce.py @@ -1,20 +1,20 @@ -# IfcApi - CLI wrapper for ifcopenshell.api mutation functions +# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions # Copyright (C) 2025 Bruno Postle # -# This file is part of IfcApi. +# This file is part of IfcEdit. # -# IfcApi is free software: you can redistribute it and/or modify +# IfcEdit 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. # -# IfcApi is distributed in the hope that it will be useful, +# IfcEdit 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 IfcApi. If not, see . +# along with IfcEdit. If not, see . from __future__ import annotations diff --git a/src/ifcapi/ifcapi/discover.py b/src/ifcedit/ifcedit/discover.py similarity index 92% rename from src/ifcapi/ifcapi/discover.py rename to src/ifcedit/ifcedit/discover.py index 305be7162d..7b5c2878ec 100644 --- a/src/ifcapi/ifcapi/discover.py +++ b/src/ifcedit/ifcedit/discover.py @@ -1,20 +1,20 @@ -# IfcApi - CLI wrapper for ifcopenshell.api mutation functions +# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions # Copyright (C) 2025 Bruno Postle # -# This file is part of IfcApi. +# This file is part of IfcEdit. # -# IfcApi is free software: you can redistribute it and/or modify +# IfcEdit 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. # -# IfcApi is distributed in the hope that it will be useful, +# IfcEdit 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 IfcApi. If not, see . +# along with IfcEdit. If not, see . from __future__ import annotations @@ -55,12 +55,14 @@ def list_modules() -> list[dict]: description = "" if mod.__doc__: description = mod.__doc__.strip().split("\n")[0] - modules.append({ - "module": child.name, - "description": description, - "functions": list(all_names), - "count": len(all_names), - }) + modules.append( + { + "module": child.name, + "description": description, + "functions": list(all_names), + "count": len(all_names), + } + ) return modules @@ -80,11 +82,13 @@ def list_functions(module: str) -> list[dict]: if fn.__doc__: description = fn.__doc__.strip().split("\n")[0] params = _extract_params(fn) - functions.append({ - "name": name, - "description": description, - "params": params, - }) + functions.append( + { + "name": name, + "description": description, + "params": params, + } + ) return functions diff --git a/src/ifcapi/ifcapi/run.py b/src/ifcedit/ifcedit/run.py similarity index 91% rename from src/ifcapi/ifcapi/run.py rename to src/ifcedit/ifcedit/run.py index 1e894a8005..74f1074a3f 100644 --- a/src/ifcapi/ifcapi/run.py +++ b/src/ifcedit/ifcedit/run.py @@ -1,20 +1,20 @@ -# IfcApi - CLI wrapper for ifcopenshell.api mutation functions +# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions # Copyright (C) 2025 Bruno Postle # -# This file is part of IfcApi. +# This file is part of IfcEdit. # -# IfcApi is free software: you can redistribute it and/or modify +# IfcEdit 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. # -# IfcApi is distributed in the hope that it will be useful, +# IfcEdit 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 IfcApi. If not, see . +# along with IfcEdit. If not, see . from __future__ import annotations @@ -24,7 +24,7 @@ import typing import ifcopenshell -from ifcapi.coerce import coerce_value +from ifcedit.coerce import coerce_value def run_api( diff --git a/src/ifcapi/pyproject.toml b/src/ifcedit/pyproject.toml similarity index 95% rename from src/ifcapi/pyproject.toml rename to src/ifcedit/pyproject.toml index 917f6a5791..411c0a95c8 100644 --- a/src/ifcapi/pyproject.toml +++ b/src/ifcedit/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "ifcapi" +name = "ifcedit" version = "0.0.0" authors = [ { name="Bruno Postle", email="bruno@postle.net" }, @@ -23,7 +23,7 @@ Documentation = "https://docs.ifcopenshell.org" Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues" [tool.setuptools.packages.find] -include = ["ifcapi*"] +include = ["ifcedit*"] exclude = ["test*"] [tool.ruff] diff --git a/src/ifcapi/tests/__init__.py b/src/ifcedit/tests/__init__.py similarity index 100% rename from src/ifcapi/tests/__init__.py rename to src/ifcedit/tests/__init__.py diff --git a/src/ifcapi/tests/conftest.py b/src/ifcedit/tests/conftest.py similarity index 100% rename from src/ifcapi/tests/conftest.py rename to src/ifcedit/tests/conftest.py diff --git a/src/ifcapi/tests/test_coerce.py b/src/ifcedit/tests/test_coerce.py similarity index 99% rename from src/ifcapi/tests/test_coerce.py rename to src/ifcedit/tests/test_coerce.py index 3764235771..5f39efa964 100644 --- a/src/ifcapi/tests/test_coerce.py +++ b/src/ifcedit/tests/test_coerce.py @@ -5,7 +5,7 @@ import pytest import ifcopenshell -from ifcapi.coerce import coerce_value +from ifcedit.coerce import coerce_value class TestStringCoercion: diff --git a/src/ifcapi/tests/test_discover.py b/src/ifcedit/tests/test_discover.py similarity index 98% rename from src/ifcapi/tests/test_discover.py rename to src/ifcedit/tests/test_discover.py index 7e78573c2a..e2d42f6c4e 100644 --- a/src/ifcapi/tests/test_discover.py +++ b/src/ifcedit/tests/test_discover.py @@ -1,4 +1,4 @@ -from ifcapi.discover import function_docs, list_functions, list_modules +from ifcedit.discover import function_docs, list_functions, list_modules class TestListModules: diff --git a/src/ifcapi/tests/test_main.py b/src/ifcedit/tests/test_main.py similarity index 69% rename from src/ifcapi/tests/test_main.py rename to src/ifcedit/tests/test_main.py index 2ef021cf05..ecf04ad046 100644 --- a/src/ifcapi/tests/test_main.py +++ b/src/ifcedit/tests/test_main.py @@ -5,10 +5,10 @@ import sys import pytest -def run_ifcapi(*args): - """Run ifcapi as a subprocess and return (stdout, stderr, returncode).""" +def run_ifcedit(*args): + """Run ifcedit as a subprocess and return (stdout, stderr, returncode).""" result = subprocess.run( - [sys.executable, "-m", "ifcapi", *args], + [sys.executable, "-m", "ifcedit", *args], capture_output=True, text=True, ) @@ -17,7 +17,7 @@ def run_ifcapi(*args): class TestListCommand: def test_list_all_modules(self): - stdout, stderr, rc = run_ifcapi("list") + stdout, stderr, rc = run_ifcedit("list") assert rc == 0 data = json.loads(stdout) assert isinstance(data, list) @@ -26,7 +26,7 @@ class TestListCommand: assert "spatial" in module_names def test_list_module_functions(self): - stdout, stderr, rc = run_ifcapi("list", "root") + stdout, stderr, rc = run_ifcedit("list", "root") assert rc == 0 data = json.loads(stdout) assert isinstance(data, list) @@ -34,14 +34,14 @@ class TestListCommand: assert "create_entity" in names def test_list_text_format(self): - stdout, stderr, rc = run_ifcapi("--format", "text", "list") + stdout, stderr, rc = run_ifcedit("--format", "text", "list") assert rc == 0 assert "root" in stdout class TestDocsCommand: def test_docs_create_entity(self): - stdout, stderr, rc = run_ifcapi("docs", "root.create_entity") + stdout, stderr, rc = run_ifcedit("docs", "root.create_entity") assert rc == 0 data = json.loads(stdout) assert data["module"] == "root" @@ -49,18 +49,18 @@ class TestDocsCommand: assert "params" in data def test_docs_invalid_path(self): - stdout, stderr, rc = run_ifcapi("docs", "invalid_path") + stdout, stderr, rc = run_ifcedit("docs", "invalid_path") assert rc != 0 assert "module.function" in stderr def test_docs_unknown_function(self): - stdout, stderr, rc = run_ifcapi("docs", "root.nonexistent") + stdout, stderr, rc = run_ifcedit("docs", "root.nonexistent") assert rc != 0 class TestRunCommand: def test_create_entity(self, model_file): - stdout, stderr, rc = run_ifcapi( + stdout, stderr, rc = run_ifcedit( "run", model_file, "root.create_entity", "--ifc_class", "IfcWall", "--name", "CLIWall" ) assert rc == 0, f"stderr: {stderr}" @@ -70,9 +70,7 @@ class TestRunCommand: assert data["result"]["name"] == "CLIWall" def test_dry_run(self, model_file): - stdout, stderr, rc = run_ifcapi( - "run", model_file, "root.create_entity", "--dry-run", "--ifc_class", "IfcWall" - ) + stdout, stderr, rc = run_ifcedit("run", model_file, "root.create_entity", "--dry-run", "--ifc_class", "IfcWall") assert rc == 0 data = json.loads(stdout) assert data["ok"] is True @@ -80,9 +78,7 @@ class TestRunCommand: def test_output_to_different_file(self, model_file, tmp_path): output = str(tmp_path / "output.ifc") - stdout, stderr, rc = run_ifcapi( - "run", model_file, "root.create_entity", "-o", output, "--ifc_class", "IfcSlab" - ) + stdout, stderr, rc = run_ifcedit("run", model_file, "root.create_entity", "-o", output, "--ifc_class", "IfcSlab") assert rc == 0, f"stderr: {stderr}" data = json.loads(stdout) assert data["ok"] is True @@ -92,10 +88,10 @@ class TestRunCommand: assert os.path.exists(output) def test_run_error_bad_function(self, model_file): - stdout, stderr, rc = run_ifcapi("run", model_file, "root.nonexistent") + stdout, stderr, rc = run_ifcedit("run", model_file, "root.nonexistent") assert rc != 0 def test_run_invalid_function_path(self, model_file): - stdout, stderr, rc = run_ifcapi("run", model_file, "invalid_path") + stdout, stderr, rc = run_ifcedit("run", model_file, "invalid_path") assert rc != 0 assert "module.function" in stderr diff --git a/src/ifcapi/tests/test_run.py b/src/ifcedit/tests/test_run.py similarity index 98% rename from src/ifcapi/tests/test_run.py rename to src/ifcedit/tests/test_run.py index 8b0369342e..09d0571128 100644 --- a/src/ifcapi/tests/test_run.py +++ b/src/ifcedit/tests/test_run.py @@ -1,7 +1,7 @@ import ifcopenshell.api.pset import ifcopenshell.api.root -from ifcapi.run import run_api, serialize_result +from ifcedit.run import run_api, serialize_result class TestRunApi: diff --git a/src/ifcquery/README.md b/src/ifcquery/README.md new file mode 100644 index 0000000000..e7e30608e9 --- /dev/null +++ b/src/ifcquery/README.md @@ -0,0 +1,252 @@ +# ifcquery + +A CLI tool for querying and inspecting IFC building models. All output is +structured JSON (or human-readable text), making it easy to pipe into other +tools or scripts. + +## Installation + +```bash +pip install ifcquery +``` + +Requires `ifcopenshell`. The `clash` subcommand additionally requires the +IfcOpenShell C++ geometry bindings (`ifcopenshell.geom`). + +## Usage + +``` +ifcquery [options] [--format json|text] +``` + +The `--format` flag controls output. Default is `json`; use `text` for +indented human-readable output. + +## Subcommands + +### summary + +Get a model overview: schema version, entity counts, and project info. + +```bash +ifcquery model.ifc summary +``` + +```json +{ + "schema": "IFC4", + "total_entities": 1847, + "project": { + "id": 1, + "name": "Office Building", + "description": null + }, + "types": { + "IfcWall": 42, + "IfcSlab": 12, + "IfcWindow": 36 + } +} +``` + +### tree + +Display the spatial hierarchy from IfcProject down through sites, buildings, +storeys, and their contained elements. + +```bash +ifcquery model.ifc tree +``` + +```json +{ + "id": 1, + "type": "IfcProject", + "name": "Office Building", + "children": [ + { + "id": 2, + "type": "IfcSite", + "name": "Default Site", + "children": [ + { + "id": 3, + "type": "IfcBuilding", + "name": "Main Building", + "children": [ + { + "id": 4, + "type": "IfcBuildingStorey", + "name": "Ground Floor", + "elements": [ + {"id": 10, "type": "IfcWall", "name": "Wall001"}, + {"id": 11, "type": "IfcSlab", "name": "Floor001"} + ] + } + ] + } + ] + } + ] +} +``` + +### info + +Get detailed information about a specific element by step ID. + +```bash +ifcquery model.ifc info 10 +ifcquery model.ifc info '#10' +``` + +Returns attributes, property sets, type relationship, material assignment, +spatial container, and placement matrix. + +```json +{ + "id": 10, + "type": "IfcWall", + "attributes": { + "Name": "Wall001", + "Description": null, + "ObjectType": "LOADBEARING" + }, + "property_sets": { + "Pset_WallCommon": { + "IsExternal": true, + "FireRating": "2HR" + } + }, + "element_type": {"id": 50, "type": "IfcWallType", "name": "Standard"}, + "material": {"id": 60, "type": "IfcMaterial", "name": "Concrete"}, + "container": {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"}, + "placement": [ + [1.0, 0.0, 0.0, 5.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ] +} +``` + +### select + +Filter elements using the ifcopenshell selector syntax. + +```bash +ifcquery model.ifc select 'IfcWall' +ifcquery model.ifc select 'IfcWall, IfcSlab' +``` + +```json +[ + {"id": 10, "type": "IfcWall", "name": "Wall001"}, + {"id": 11, "type": "IfcWall", "name": "Wall002"}, + {"id": 20, "type": "IfcSlab", "name": "Floor001"} +] +``` + +Results are sorted by ID. + +### relations + +Show all relationships for an element, organized by category: hierarchy, +children, type relationships, groups, systems, material, and connections. + +```bash +ifcquery model.ifc relations 10 +``` + +```json +{ + "id": 10, + "type": "IfcWall", + "name": "Wall001", + "hierarchy": { + "parent": {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"}, + "container": {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"} + }, + "children": { + "openings": [{"id": 30, "type": "IfcOpeningElement", "name": "Opening01"}] + }, + "type_relationship": { + "type_of": {"id": 50, "type": "IfcWallType", "name": "Standard"} + }, + "material": {"id": 60, "type": "IfcMaterial", "name": "Concrete"} +} +``` + +Empty categories are omitted from output. + +Use `--traverse up` to walk the spatial hierarchy from the element up to +IfcProject: + +```bash +ifcquery model.ifc relations 10 --traverse up +``` + +```json +[ + {"id": 10, "type": "IfcWall", "name": "Wall001"}, + {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"}, + {"id": 3, "type": "IfcBuilding", "name": "Main Building"}, + {"id": 2, "type": "IfcSite", "name": "Default Site"}, + {"id": 1, "type": "IfcProject", "name": "Office Building"} +] +``` + +### clash + +Check a single element for geometric intersections and clearance violations +against other elements. + +```bash +ifcquery model.ifc clash 10 +ifcquery model.ifc clash 10 --clearance 0.5 +ifcquery model.ifc clash 10 --scope all --tolerance 0.001 +``` + +Options: + +- `--clearance ` -- minimum clearance distance to check +- `--tolerance ` -- intersection tolerance (default: 0.002) +- `--scope {storey,all}` -- check against same-storey elements or all elements (default: storey) + +```json +{ + "element": {"id": 10, "type": "IfcWall", "name": "Wall001"}, + "scope": "storey", + "pass": false, + "checks": { + "intersection": { + "pass": false, + "tolerance": 0.002, + "clashes": [ + { + "element": {"id": 11, "type": "IfcWall", "name": "Wall002"}, + "type": "intersection", + "distance": 0.0, + "p1": [2.5, 2.5, 1.5], + "p2": [2.5, 2.5, 1.5] + } + ] + }, + "clearance": { + "pass": true, + "clearance": 0.5, + "clashes": [] + } + } +} +``` + +Requires the IfcOpenShell C++ geometry bindings. + +## Error handling + +Errors are written to stderr. Exit code is 0 on success, 1 on error. + +## License + +LGPLv3+ -- see the IfcOpenShell project license. diff --git a/src/ifcquery/ifcquery/clash.py b/src/ifcquery/ifcquery/clash.py index bb1132e21e..a9601379d3 100644 --- a/src/ifcquery/ifcquery/clash.py +++ b/src/ifcquery/ifcquery/clash.py @@ -62,9 +62,7 @@ def _get_scope_elements( return elements, "all" -def _build_tree( - model: ifcopenshell.file, elements: set[ifcopenshell.entity_instance] -) -> ifcopenshell.geom.tree | None: +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). @@ -72,9 +70,7 @@ def _build_tree( 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) - ) + iterator = ifcopenshell.geom.iterator(geom_settings, model, multiprocessing.cpu_count(), include=list(elements)) if not iterator.initialize(): return None while True: @@ -84,9 +80,7 @@ def _build_tree( return geom_tree -def _format_clash( - clash_result, geom_tree: ifcopenshell.geom.tree, model: ifcopenshell.file -) -> dict[str, Any]: +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 @@ -124,9 +118,7 @@ def clash( if not scope_elements: result["pass"] = True - result["checks"] = { - "intersection": {"pass": True, "tolerance": tolerance, "clashes": []} - } + result["checks"] = {"intersection": {"pass": True, "tolerance": tolerance, "clashes": []}} if clearance is not None: result["checks"]["clearance"] = {"pass": True, "clearance": clearance, "clashes": []} return result diff --git a/src/ifcquery/tests/test_clash.py b/src/ifcquery/tests/test_clash.py index 507a4ad122..a03214c86d 100644 --- a/src/ifcquery/tests/test_clash.py +++ b/src/ifcquery/tests/test_clash.py @@ -142,9 +142,7 @@ class TestNoClashes: # Create a model with a single wall f = ifcopenshell.api.project.create_file() ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0] - ifcopenshell.api.owner.settings.get_application = lambda ifc: ( - ifc.by_type("IfcApplication") or [None] - )[0] + ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0] project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="P") ifcopenshell.api.unit.assign_unit(f) site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="S")