Add ifc_contexts and ifc_materials tools

New ifcquery modules list geometric representation contexts (with step
IDs, context type, identifier, target view, parent) and material sets
(IfcMaterial, layer sets, constituent sets, profile sets). Exposed as
CLI subcommands and MCP tools.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Bruno Postle
2026-03-01 19:49:53 +00:00
parent 1cb298fa9c
commit bf5b48d037
7 changed files with 271 additions and 2 deletions
+22 -1
View File
@@ -11,8 +11,9 @@ from ifcedit.discover import function_docs, list_functions, list_modules
from ifcedit.quantify import run_quantify
from ifcedit.run import run_api
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, render as render_mod, schedule, schema, select, summary, tree
from ifcquery import info, materials as materials_mod, relations, render as render_mod, schedule, schema, select, summary, tree
from ifcquery import validate as validate_mod
@@ -150,6 +151,14 @@ class IfcSession:
scope=scope,
)
def ifc_contexts(self) -> list[dict[str, Any]]:
"""List all geometric representation contexts and subcontexts with their step IDs."""
return contexts_mod.contexts(self._require_model())
def ifc_materials(self) -> list[dict[str, Any]]:
"""List all materials and material sets (layers, constituents, profiles)."""
return materials_mod.materials(self._require_model())
# ------------------------
# Edit discovery + execute
# ------------------------
@@ -329,6 +338,18 @@ class IfcSession:
"additionalProperties": False,
},
},
{
"type": "function",
"name": "ifc_contexts",
"description": "List all geometric representation contexts and subcontexts with their step IDs, context type, identifier, and target view. Use this to find the context ID required for geometry-creation API calls.",
"parameters": {"type": "object", "properties": {}, "required": [], "additionalProperties": False},
},
{
"type": "function",
"name": "ifc_materials",
"description": "List all materials and material sets (IfcMaterial, IfcMaterialLayerSet, IfcMaterialConstituentSet, IfcMaterialProfileSet) with their layers, constituents, or profiles.",
"parameters": {"type": "object", "properties": {}, "required": [], "additionalProperties": False},
},
{
"type": "function",
"name": "ifc_list",
+8
View File
@@ -85,6 +85,14 @@ def build_server() -> Any:
scope=scope,
)
@server.tool()
def ifc_contexts() -> list[dict[str, Any]]:
return session.ifc_contexts()
@server.tool()
def ifc_materials() -> list[dict[str, Any]]:
return session.ifc_materials()
# ---- Edit ----
@server.tool()
def ifc_list(module: str = "") -> list[dict]:
+10 -1
View File
@@ -27,8 +27,9 @@ 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, render as render_mod, schedule, schema, select, summary, tree
from ifcquery import info, materials as materials_mod, relations, render as render_mod, schedule, schema, select, summary, tree
from ifcquery import validate as validate_mod
@@ -121,6 +122,10 @@ def main():
"--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)")
@@ -204,6 +209,10 @@ def main():
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":
+44
View File
@@ -0,0 +1,44 @@
# 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
+86
View File
@@ -0,0 +1,86 @@
# 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
+49
View File
@@ -0,0 +1,49 @@
import ifcopenshell.api.context
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.unit
from ifcquery.contexts import contexts
class TestContexts:
def test_empty_model(self):
f = ifcopenshell.api.project.create_file()
result = contexts(f)
assert isinstance(result, list)
assert len(result) == 0
def test_model_context(self, model):
import ifcopenshell.api.context
ifcopenshell.api.context.add_context(model, context_type="Model")
result = contexts(model)
assert len(result) == 1
entry = result[0]
assert entry["type"] == "IfcGeometricRepresentationContext"
assert entry["context_type"] == "Model"
assert "id" in entry
assert "context_identifier" in entry
def test_subcontext(self, model):
import ifcopenshell.api.context
model_ctx = ifcopenshell.api.context.add_context(model, context_type="Model")
ifcopenshell.api.context.add_context(
model,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model_ctx,
)
result = contexts(model)
assert len(result) == 2
subctx = next(e for e in result if e["type"] == "IfcGeometricRepresentationSubContext")
assert subctx["context_identifier"] == "Body"
assert subctx["target_view"] == "MODEL_VIEW"
assert subctx["parent_context_id"] == model_ctx.id()
def test_ids_are_integers(self, model):
import ifcopenshell.api.context
ifcopenshell.api.context.add_context(model, context_type="Model")
result = contexts(model)
for entry in result:
assert isinstance(entry["id"], int)
+52
View File
@@ -0,0 +1,52 @@
import ifcopenshell.api.material
import ifcopenshell.api.project
from ifcquery.materials import materials
class TestMaterials:
def test_empty_model(self, model):
result = materials(model)
assert isinstance(result, list)
assert len(result) == 0
def test_single_material(self, model):
ifcopenshell.api.material.add_material(model, name="Concrete", category="concrete")
result = materials(model)
assert len(result) == 1
m = result[0]
assert m["type"] == "IfcMaterial"
assert m["name"] == "Concrete"
assert m["category"] == "concrete"
assert isinstance(m["id"], int)
def test_material_layer_set(self, model):
mat = ifcopenshell.api.material.add_material(model, name="Brick")
layer_set = ifcopenshell.api.material.add_material_set(model, name="BrickSet", set_type="IfcMaterialLayerSet")
ifcopenshell.api.material.add_layer(model, layer_set=layer_set, material=mat)
result = materials(model)
layer_sets = [e for e in result if e["type"] == "IfcMaterialLayerSet"]
assert len(layer_sets) == 1
ls = layer_sets[0]
assert ls["name"] == "BrickSet"
assert isinstance(ls["layers"], list)
assert len(ls["layers"]) == 1
layer = ls["layers"][0]
assert layer["material"] == "Brick"
def test_material_constituent_set(self, model):
mat = ifcopenshell.api.material.add_material(model, name="Steel")
cs = ifcopenshell.api.material.add_material_set(model, name="CompSet", set_type="IfcMaterialConstituentSet")
ifcopenshell.api.material.add_constituent(model, constituent_set=cs, material=mat)
result = materials(model)
constituent_sets = [e for e in result if e["type"] == "IfcMaterialConstituentSet"]
assert len(constituent_sets) == 1
entry = constituent_sets[0]
assert entry["name"] == "CompSet"
assert isinstance(entry["constituents"], list)
def test_ids_are_integers(self, model):
ifcopenshell.api.material.add_material(model, name="Wood")
result = materials(model)
for entry in result:
assert isinstance(entry["id"], int)