mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user