From 6ffc4a2b2967f0e562c896eca317efb732b14b27 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:57:54 +0000 Subject: [PATCH] Simplify /clash: single selector, detect collisions within selected set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop selector_b — the endpoint now takes the URL's selector= and tests all matched elements against each other, consistent with the spec's model. Self-pairs and duplicates are filtered; requires at least 2 matched elements. https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- ifcurl/render_service.py | 35 +++++++++-------------------------- ifcurl/service.py | 29 ++++++++++------------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/ifcurl/render_service.py b/ifcurl/render_service.py index 9edda7a..ae8b08e 100644 --- a/ifcurl/render_service.py +++ b/ifcurl/render_service.py @@ -106,10 +106,8 @@ def _sandboxed_select(ifc_bytes: bytes, selector: str) -> list[str]: return [e.GlobalId for e in matched if hasattr(e, "GlobalId") and e.GlobalId] -def _sandboxed_clash( - ifc_bytes: bytes, selector_a: str, selector_b: str | None -) -> list[list[str]]: - """Detect geometric clashes between two sets of IFC elements. +def _sandboxed_clash(ifc_bytes: bytes, selector: str) -> list[list[str]]: + """Detect geometric clashes within the elements matched by selector. Uses ifcopenshell.geom.tree.clash_collision_many for mesh-level interpenetration detection (hard clashes only, touching excluded). @@ -124,32 +122,21 @@ def _sandboxed_clash( settings = ifcopenshell.geom.settings() settings.set("use-world-coords", True) - elems_a = list(ifcopenshell.util.selector.filter_elements(model, selector_a)) - ids_a = {e.id() for e in elems_a} - - if selector_b: - elems_b = list(ifcopenshell.util.selector.filter_elements(model, selector_b)) - else: - elems_b = [ - e - for e in model.by_type("IfcProduct") - if e.id() not in ids_a and getattr(e, "GlobalId", None) - ] - - if not elems_a or not elems_b: + elems = list(ifcopenshell.util.selector.filter_elements(model, selector)) + if len(elems) < 2: return [] tree = ifcopenshell.geom.tree() tree.add_file(model, settings) - clashes = tree.clash_collision_many(elems_a, elems_b, allow_touching=False) + clashes = tree.clash_collision_many(elems, elems, allow_touching=False) pairs: list[list[str]] = [] seen: set[frozenset[str]] = set() for c in clashes: guid_a = getattr(c.a, "GlobalId", None) guid_b = getattr(c.b, "GlobalId", None) - if not guid_a or not guid_b: + if not guid_a or not guid_b or guid_a == guid_b: continue key = frozenset((guid_a, guid_b)) if key not in seen: @@ -321,20 +308,16 @@ async def select_endpoint( @app.post("/clash") async def clash_endpoint( ifc: UploadFile = File(...), - selector_a: str = Form(...), - selector_b: str = Form(""), + selector: str = Form(...), ) -> Response: - """Detect clashing element pairs. + """Detect clashing element pairs within the selected set. Returns JSON ``{"pairs": [["guidA", "guidB"], ...]}``. - An empty ``selector_b`` means test set A against all other model elements. """ ifc_bytes = await ifc.read() try: - pairs = run_sandboxed( - _sandboxed_clash, ifc_bytes, selector_a, selector_b or None - ) + pairs = run_sandboxed(_sandboxed_clash, ifc_bytes, selector) except SandboxCrashError as exc: raise HTTPException(status_code=422, detail=f"IFC clash detection crashed: {exc}") from exc except SandboxTimeoutError as exc: diff --git a/ifcurl/service.py b/ifcurl/service.py index 960af24..e99a892 100644 --- a/ifcurl/service.py +++ b/ifcurl/service.py @@ -126,9 +126,7 @@ def _select_via_socket(ifc_bytes: bytes, selector: str) -> list[str]: return resp.json()["guids"] -def _clash_via_socket( - ifc_bytes: bytes, selector_a: str, selector_b: str | None -) -> list[list[str]]: +def _clash_via_socket(ifc_bytes: bytes, selector: str) -> list[list[str]]: """Delegate clash detection to the render service over the Unix socket.""" import httpx @@ -137,7 +135,7 @@ def _clash_via_socket( resp = client.post( "http://render/clash", files={"ifc": ifc_bytes}, - data={"selector_a": selector_a, "selector_b": selector_b or ""}, + data={"selector": selector}, timeout=_RENDER_TIMEOUT, ) except httpx.TransportError as exc: @@ -450,8 +448,6 @@ class QueryRequest(BaseModel): class ClashRequest(BaseModel): url: str - selector_b: str | None = None - """Second element set to test against. When omitted, set A is tested against all other model elements.""" token: str | None = None @@ -829,21 +825,18 @@ def query(request: QueryRequest) -> JSONResponse: @app.get("/clash") -def clash_get(url: str, selector_b: str | None = None, token: str | None = None) -> JSONResponse: +def clash_get(url: str, token: str | None = None) -> JSONResponse: """GET variant of POST /clash.""" - return clash(ClashRequest(url=url, selector_b=selector_b, token=token)) + return clash(ClashRequest(url=url, token=token)) @app.post("/clash") def clash(request: ClashRequest) -> JSONResponse: - """Detect geometric clashes between two sets of elements. + """Detect geometric clashes within the elements matched by the selector. - Set A is defined by the URL's ``selector=`` parameter. Set B is defined - by ``selector_b``; when omitted, set A is tested against all other - model elements. - - Uses ifcopenshell hard-clash (interpenetration) detection. Returns - ``{"pairs": [["", ""], …]}``. + The URL's ``selector=`` parameter defines the set of elements to test + against each other. Uses ifcopenshell hard-clash (interpenetration) + detection. Returns ``{"pairs": [["", ""], …]}``. """ try: ifc_url = IfcUrl.parse(request.url) @@ -877,11 +870,9 @@ def clash(request: ClashRequest) -> JSONResponse: try: if _RENDER_SOCKET: - pairs = _clash_via_socket(ifc_bytes, ifc_url.selector, request.selector_b) + pairs = _clash_via_socket(ifc_bytes, ifc_url.selector) else: - pairs = run_sandboxed( - _sandboxed_clash, ifc_bytes, ifc_url.selector, request.selector_b - ) + pairs = run_sandboxed(_sandboxed_clash, ifc_bytes, ifc_url.selector) except SandboxCrashError as exc: raise HTTPException( status_code=422, detail=f"IFC clash detection crashed: {exc}"