mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 10:18:14 +00:00
Merge pull request #4 from brunopostle/claude/review-spec-coverage-cM3AC
SPECIFICATION.md: add query parameter and visibility=clash
This commit is contained in:
commit
d261648010
3 changed files with 133 additions and 5 deletions
|
|
@ -96,6 +96,35 @@ selector=325Q7Fhnf67OZC$$r43uzK
|
||||||
|
|
||||||
When omitted, no selection is applied.
|
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`
|
### `camera`
|
||||||
|
|
||||||
Nine comma-separated decimal values defining the camera in IFC world
|
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) |
|
| `highlight` | Show all elements, highlight selection (default) |
|
||||||
| `ghost` | Show selection normally, dim all other elements |
|
| `ghost` | Show selection normally, dim all other elements |
|
||||||
| `isolate` | Show only selected elements, hide all others |
|
| `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.
|
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
|
## 4. Coordinate system
|
||||||
|
|
@ -272,15 +308,21 @@ A compliant viewer must:
|
||||||
- Apply perspective camera (`camera` + `fov`)
|
- Apply perspective camera (`camera` + `fov`)
|
||||||
- Apply orthographic camera (`camera` + `scale`)
|
- Apply orthographic camera (`camera` + `scale`)
|
||||||
- Apply clipping planes (`clip`)
|
- 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
|
- Resolve relative `IFCDOCUMENTREFERENCE` paths using the root file's repo
|
||||||
and ref context
|
and ref context
|
||||||
- Resolve `ifc://` `IFCDOCUMENTREFERENCE` locations
|
- 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:
|
Optional extensions:
|
||||||
|
|
||||||
- Semantic colouring
|
- 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
|
ifc://cde.example.com/project-123?document_id=bf546064-6b97-4730-a094-c21ab929c91a
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Query the fire rating of all walls — returns `{"<guid>": "<value>", …}`:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
## 9. OpenCDE transport
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,14 @@ class IfcUrl:
|
||||||
ref: str # e.g. 'heads/main', 'tags/v1.2', 'abc123def', 'HEAD'
|
ref: str # e.g. 'heads/main', 'tags/v1.2', 'abc123def', 'HEAD'
|
||||||
path: str | None # IFC file path within the repository
|
path: str | None # IFC file path within the repository
|
||||||
selector: str | None # IfcOpenShell selector expression (percent-decoded)
|
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
|
camera: tuple[float, float, float, float, float, float, float, float, float] | None
|
||||||
fov: float | None # perspective field of view in degrees
|
fov: float | None # perspective field of view in degrees
|
||||||
scale: float | None # orthographic view-to-world scale (BCF ViewToWorldScale)
|
scale: float | None # orthographic view-to-world scale (BCF ViewToWorldScale)
|
||||||
clips: list[tuple[float, float, float, float, float, float]] = field(
|
clips: list[tuple[float, float, float, float, float, float]] = field(
|
||||||
default_factory=list
|
default_factory=list
|
||||||
)
|
)
|
||||||
visibility: str = "highlight" # 'highlight', 'ghost', or 'isolate'
|
visibility: str = "highlight" # 'highlight', 'ghost', 'isolate', or 'clash'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, url_string: str) -> IfcUrl:
|
def parse(cls, url_string: str) -> IfcUrl:
|
||||||
|
|
@ -95,6 +96,7 @@ class IfcUrl:
|
||||||
|
|
||||||
path = qs["path"][0] if "path" in qs else None
|
path = qs["path"][0] if "path" in qs else None
|
||||||
selector = unquote(qs["selector"][0]) if "selector" 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
|
camera: tuple[float, ...] | None = None
|
||||||
if "camera" in qs:
|
if "camera" in qs:
|
||||||
|
|
@ -150,9 +152,9 @@ class IfcUrl:
|
||||||
clips.append(tuple(vals)) # type: ignore[arg-type]
|
clips.append(tuple(vals)) # type: ignore[arg-type]
|
||||||
|
|
||||||
visibility_raw = qs.get("visibility", ["highlight"])[0]
|
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(
|
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(
|
return cls(
|
||||||
|
|
@ -163,6 +165,7 @@ class IfcUrl:
|
||||||
ref=ref,
|
ref=ref,
|
||||||
path=path,
|
path=path,
|
||||||
selector=selector,
|
selector=selector,
|
||||||
|
query=query,
|
||||||
camera=camera, # type: ignore[arg-type]
|
camera=camera, # type: ignore[arg-type]
|
||||||
fov=fov,
|
fov=fov,
|
||||||
scale=scale,
|
scale=scale,
|
||||||
|
|
@ -227,6 +230,8 @@ class IfcUrl:
|
||||||
parts.append(f"path={quote(self.path, safe='/')}")
|
parts.append(f"path={quote(self.path, safe='/')}")
|
||||||
if self.selector:
|
if self.selector:
|
||||||
parts.append(f"selector={quote(self.selector, safe='$')}")
|
parts.append(f"selector={quote(self.selector, safe='$')}")
|
||||||
|
if self.query:
|
||||||
|
parts.append(f"query={self.query}")
|
||||||
if self.camera:
|
if self.camera:
|
||||||
vals = ",".join(repr(v) for v in self.camera)
|
vals = ",".join(repr(v) for v in self.camera)
|
||||||
parts.append(f"camera={vals}")
|
parts.append(f"camera={vals}")
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,38 @@ def test_visibility_isolate():
|
||||||
assert url.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()
|
# git_remote_url()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -302,6 +334,37 @@ def test_unknown_visibility():
|
||||||
IfcUrl.parse("ifc://example.com/o/r@HEAD?path=m.ifc&visibility=wireframe")
|
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()
|
# to_string()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue