ifcquery/ifcedit: enable shell scripting by composing query and edit commands

Add --format ids to ifcquery to output step IDs suitable for piping into
ifcedit parameters. Add ifcedit foreach to apply an operation to every
element in a query result. Extend clash and relations output so --format ids
extracts all involved element IDs, enabling one-liners like clash detection
piped directly into render.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Bruno Postle
2026-03-29 15:17:22 +01:00
parent 0ed96d32dd
commit 1c26ee86c9
11 changed files with 523 additions and 7 deletions
+37
View File
@@ -82,3 +82,40 @@ class TestCLI:
def test_no_command(self, ifc_path):
rc, stdout, stderr = run_ifcquery(ifc_path)
assert rc != 0
def test_select_ids_format(self, ifc_path):
rc, stdout, stderr = run_ifcquery(ifc_path, "--format", "ids", "select", "IfcWall")
assert rc == 0
# Should be a comma-separated string of integers with no surrounding whitespace
ids = stdout.strip()
assert ids != ""
for part in ids.split(","):
assert part.isdigit()
def test_select_ids_format_multiple(self, ifc_path, model):
rc, stdout, stderr = run_ifcquery(ifc_path, "--format", "ids", "select", "IfcElement")
assert rc == 0
ids = stdout.strip().split(",")
assert len(ids) >= 2
def test_ids_format_empty_result(self, ifc_path):
rc, stdout, stderr = run_ifcquery(ifc_path, "--format", "ids", "select", "IfcDoor")
assert rc == 0
assert stdout.strip() == ""
def test_relations_ids_format(self, ifc_path, model):
storey = model.by_type("IfcBuildingStorey")[0]
rc, stdout, stderr = run_ifcquery(ifc_path, "--format", "ids", "relations", str(storey.id()))
assert rc == 0
ids = stdout.strip().split(",")
assert all(i.isdigit() for i in ids)
# should include the storey itself and its contained elements
assert str(storey.id()) in ids
wall_id = str(model.by_type("IfcWall")[0].id())
assert wall_id in ids
def test_info_ids_format(self, ifc_path, model):
wall = model.by_type("IfcWall")[0]
rc, stdout, stderr = run_ifcquery(ifc_path, "--format", "ids", "info", str(wall.id()))
assert rc == 0
assert stdout.strip() == str(wall.id())
+41
View File
@@ -91,6 +91,47 @@ class TestTraverseUp:
assert len(chain) == 4 # storey -> building -> site -> project
class TestElementsSummary:
def test_wall_elements_includes_self(self, model):
wall = model.by_type("IfcWall")[0]
result = relations(model, wall)
ids = [e["id"] for e in result["elements"]]
assert wall.id() in ids
def test_wall_elements_includes_container(self, model):
wall = model.by_type("IfcWall")[0]
result = relations(model, wall)
ids = [e["id"] for e in result["elements"]]
storey = model.by_type("IfcBuildingStorey")[0]
assert storey.id() in ids
def test_storey_elements_includes_contained(self, model):
storey = model.by_type("IfcBuildingStorey")[0]
result = relations(model, storey)
ids = [e["id"] for e in result["elements"]]
wall = model.by_type("IfcWall")[0]
assert wall.id() in ids
def test_elements_no_duplicates(self, model):
storey = model.by_type("IfcBuildingStorey")[0]
result = relations(model, storey)
ids = [e["id"] for e in result["elements"]]
assert len(ids) == len(set(ids))
def test_elements_all_have_id_and_type(self, model):
wall = model.by_type("IfcWall")[0]
result = relations(model, wall)
for e in result["elements"]:
assert "id" in e
assert "type" in e
def test_traverse_up_has_no_elements_field(self, model):
wall = model.by_type("IfcWall")[0]
result = relations(model, wall, traverse="up")
assert isinstance(result, list)
assert not any("elements" in item for item in result)
class TestJsonSerializable:
def test_relations_serializable(self, model):
wall = model.by_type("IfcWall")[0]