From 98c258d0ccac357a5b08628c48c1cee0d687c137 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 07:22:55 +0000 Subject: [PATCH 1/2] SPECIFICATION.md: add query parameter and visibility=clash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two new parameters to address GitHub issues #2 and #3: - `query`: dot-notation path (e.g. Pset_WallCommon.FireRating) that returns a {GlobalId: value} JSON object for the selected elements. Restricted to rooted entities; additive with view parameters so a viewer can show both a 3D view and a property table simultaneously. - `visibility=clash`: visual-cue-only rendering style that marks selected elements with clash-indicator styling. No semantic claim; exports to BCF as highlight since BCF has no structural equivalent. Updates §3, §7, and §8 accordingly. https://claude.ai/code/session_01CKpkhWhzkzR5K5PDSR8N5F --- SPECIFICATION.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 4f71e3d..db74374 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -96,6 +96,35 @@ selector=325Q7Fhnf67OZC$$r43uzK When omitted, no selection is applied. +### `query` + +A single attribute or property path to retrieve from the selected elements, +using dot notation to address property sets. + +``` +query=Name +query=Description +query=Pset_WallCommon.FireRating +query=Qto_WallBaseQuantities.NetVolume +``` + +The response is a JSON object whose keys are the GlobalId of each matching +element and whose values are the retrieved attribute or property value: + +```json +{"325Q7Fhnf67OZC$$r43uzK": "2HR", "0t2oN7…": "1HR"} +``` + +Only elements that have a GlobalId are included in the response. Elements +matched by the selector that lack a GlobalId are silently excluded. Attributes +that resolve to an IFC entity reference rather than a scalar value are +undefined and excluded. + +`query` may be combined with view parameters (`camera`, `fov`, `scale`, +`clip`, `visibility`). A viewer receiving both should display the 3D view and +the queried property values together. A render service producing a static image +ignores `query`. + ### `camera` Nine comma-separated decimal values defining the camera in IFC world @@ -188,9 +217,16 @@ Controls how selected and unselected elements are displayed. | `highlight` | Show all elements, highlight selection (default) | | `ghost` | Show selection normally, dim all other elements | | `isolate` | Show only selected elements, hide all others | +| `clash` | Show all elements, mark selection with clash-indicator styling (e.g. red) | When omitted, `highlight` is assumed. +`clash` is a visual cue only. It carries no semantic claim about the nature of +the issue; it signals to the viewer that the selected elements should be +rendered in a manner that visually distinguishes a reported clash. When +exported to BCF, `clash` is treated as `highlight` — BCF has no equivalent +structural distinction. + --- ## 4. Coordinate system @@ -272,15 +308,21 @@ A compliant viewer must: - Apply perspective camera (`camera` + `fov`) - Apply orthographic camera (`camera` + `scale`) - Apply clipping planes (`clip`) -- Apply visibility mode (`highlight`, `ghost`, `isolate`) +- Apply visibility mode (`highlight`, `ghost`, `isolate`, `clash`) - Resolve relative `IFCDOCUMENTREFERENCE` paths using the root file's repo and ref context - Resolve `ifc://` `IFCDOCUMENTREFERENCE` locations +When `query` is present, a compliant viewer should retrieve the specified +attribute or property value for each selected element and display the results +alongside the 3D view. If view parameters are also present, both the 3D view +and the property values are shown. A render service producing a static image +may ignore `query`. + Optional extensions: - Semantic colouring -- Advanced visual styles beyond the three visibility modes +- Advanced visual styles beyond the four visibility modes --- @@ -322,6 +364,24 @@ OpenCDE document, current version, no view parameters (opens at default view): ifc://cde.example.com/project-123?document_id=bf546064-6b97-4730-a094-c21ab929c91a ``` +Query the fire rating of all walls — returns `{"": "", …}`: + +``` +ifc://example.com/org/project@heads/main?path=models/building.ifc&selector=IfcWall&query=Pset_WallCommon.FireRating +``` + +Query with a camera view — viewer shows 3D view and property table together: + +``` +ifc://example.com/org/project@heads/main?path=models/building.ifc&selector=IfcWall,+Name="Core+Wall"&camera=10,20,5,0,-1,0,0,0,1&fov=60&query=Name +``` + +Clash indicator — selected elements rendered with clash styling: + +``` +ifc://example.com/org/project@heads/main?path=models/building.ifc&selector=IfcWall,+Name="Core+Wall"+IfcBeam,+Name="B1"&visibility=clash +``` + --- ## 9. OpenCDE transport From c02ccfe5ddd40671a454354efe72662463e3eeb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 07:27:35 +0000 Subject: [PATCH 2/2] url.py, test_url.py: add query parameter and visibility=clash - IfcUrl gains a query field (str | None) for dot-notation attribute/property paths (e.g. Pset_WallCommon.FireRating) - visibility now accepts 'clash' in addition to the existing three modes - to_string() serialises both new fields - 10 new tests covering query parsing, pset paths, combined query+view, clash visibility, roundtrip serialisation, and the new spec examples https://claude.ai/code/session_01CKpkhWhzkzR5K5PDSR8N5F --- ifcurl/url.py | 11 ++++++--- tests/test_url.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/ifcurl/url.py b/ifcurl/url.py index c98719e..0467282 100644 --- a/ifcurl/url.py +++ b/ifcurl/url.py @@ -41,13 +41,14 @@ class IfcUrl: ref: str # e.g. 'heads/main', 'tags/v1.2', 'abc123def', 'HEAD' path: str | None # IFC file path within the repository selector: str | None # IfcOpenShell selector expression (percent-decoded) + query: str | None # dot-notation attribute/property path, e.g. 'Pset_WallCommon.FireRating' camera: tuple[float, float, float, float, float, float, float, float, float] | None fov: float | None # perspective field of view in degrees scale: float | None # orthographic view-to-world scale (BCF ViewToWorldScale) clips: list[tuple[float, float, float, float, float, float]] = field( default_factory=list ) - visibility: str = "highlight" # 'highlight', 'ghost', or 'isolate' + visibility: str = "highlight" # 'highlight', 'ghost', 'isolate', or 'clash' @classmethod def parse(cls, url_string: str) -> IfcUrl: @@ -95,6 +96,7 @@ class IfcUrl: path = qs["path"][0] if "path" in qs else None selector = unquote(qs["selector"][0]) if "selector" in qs else None + query = qs["query"][0] if "query" in qs else None camera: tuple[float, ...] | None = None if "camera" in qs: @@ -150,9 +152,9 @@ class IfcUrl: clips.append(tuple(vals)) # type: ignore[arg-type] visibility_raw = qs.get("visibility", ["highlight"])[0] - if visibility_raw not in ("highlight", "ghost", "isolate"): + if visibility_raw not in ("highlight", "ghost", "isolate", "clash"): raise ValueError( - f"Unknown visibility mode {visibility_raw!r}; expected 'highlight', 'ghost', or 'isolate'" + f"Unknown visibility mode {visibility_raw!r}; expected 'highlight', 'ghost', 'isolate', or 'clash'" ) return cls( @@ -163,6 +165,7 @@ class IfcUrl: ref=ref, path=path, selector=selector, + query=query, camera=camera, # type: ignore[arg-type] fov=fov, scale=scale, @@ -227,6 +230,8 @@ class IfcUrl: parts.append(f"path={quote(self.path, safe='/')}") if self.selector: parts.append(f"selector={quote(self.selector, safe='$')}") + if self.query: + parts.append(f"query={self.query}") if self.camera: vals = ",".join(repr(v) for v in self.camera) parts.append(f"camera={vals}") diff --git a/tests/test_url.py b/tests/test_url.py index 6a56e39..8e39f47 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -158,6 +158,38 @@ def test_visibility_isolate(): assert url.visibility == "isolate" +def test_visibility_clash(): + url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc&visibility=clash") + assert url.visibility == "clash" + + +def test_query_attribute(): + url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc&selector=IfcWall&query=Name") + assert url.query == "Name" + + +def test_query_pset(): + url = IfcUrl.parse( + "ifc://example.com/org/repo@HEAD?path=m.ifc&selector=IfcWall&query=Pset_WallCommon.FireRating" + ) + assert url.query == "Pset_WallCommon.FireRating" + + +def test_query_absent(): + url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc") + assert url.query is None + + +def test_query_with_view_params(): + url = IfcUrl.parse( + "ifc://example.com/org/repo@HEAD?path=m.ifc" + "&selector=IfcWall&camera=10,20,5,0,-1,0,0,0,1&fov=60&query=Name" + ) + assert url.query == "Name" + assert url.camera is not None + assert url.fov == 60.0 + + # --------------------------------------------------------------------------- # git_remote_url() # --------------------------------------------------------------------------- @@ -302,6 +334,37 @@ def test_unknown_visibility(): IfcUrl.parse("ifc://example.com/o/r@HEAD?path=m.ifc&visibility=wireframe") +def test_to_string_roundtrip_query(): + raw = "ifc://example.com/org/repo@heads/main?path=m.ifc&selector=IfcWall&query=Pset_WallCommon.FireRating" + assert IfcUrl.parse(raw).to_string() == raw + + +def test_to_string_roundtrip_visibility_clash(): + raw = "ifc://example.com/org/repo@heads/main?path=m.ifc&selector=IfcWall&visibility=clash" + assert IfcUrl.parse(raw).to_string() == raw + + +def test_spec_example_query(): + url = IfcUrl.parse( + "ifc://example.com/org/project@heads/main" + "?path=models/building.ifc&selector=IfcWall&query=Pset_WallCommon.FireRating" + ) + assert url.query == "Pset_WallCommon.FireRating" + assert url.selector == "IfcWall" + assert url.camera is None + + +def test_spec_example_clash(): + url = IfcUrl.parse( + "ifc://example.com/org/project@heads/main" + "?path=models/building.ifc" + "&selector=IfcWall%2C%2BName%3D%22Core%2BWall%22%2BIfcBeam%2C%2BName%3D%22B1%22" + "&visibility=clash" + ) + assert url.visibility == "clash" + assert url.selector is not None + + # --------------------------------------------------------------------------- # to_string() # ---------------------------------------------------------------------------