From bfba12a612dfc3d8da9b41d196467ff62c2f1732 Mon Sep 17 00:00:00 2001 From: andrinr Date: Mon, 19 Oct 2026 15:59:30 +0200 Subject: [PATCH] Viewer: say which tier each feature runs on, instead of degrading silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without diff-brep, cadjoint runs its exact features on dual contouring. The viewer now says so at the point of use, with one treatment: a tier chip (a K4 value chip: what you get, then where it comes from, with a dashed edge for the public form). Where the exact path is missing, one line under the control gives the reason. A control that cannot run is disabled, not hidden. - Render popover: Feature edges is tagged "Approximate · lattice" or "Exact curves · private tier". The B-rep inspector is tagged, and disabled up front with the reason in the public tier; its fetch cycle never asks. - File › Export…: the STEP entry reads "STEP · faceted", the format carries "Faceted · public tier", Analytic surfaces is off and disabled, and the note is the reason. - Meshes window: a Gmsh mesh is tagged "Frozen geometry · public tier" with the reason, or "Follows design · private tier". - B-rep handle drag is not exposed in the UI, so it has no tag. The frontend reads GET /api/capabilities once, after the session. Each kind in the response now carries `label` and, when missing, `message`, both from cadjoint.tier: the library's refusals and the viewer share one wording, reworded plainly. CADJOINT_PUBLIC_TIER=1 hides the private kinds from the process-wide registry (workers inherit it), so one machine can show both tiers. The rulebook gains K4b for the chip, and docs/viewer.qmd a section. Also fixes brepInspector.spec's legend sum, which #98's triangle-outline row broke, and makes export.spec tier-aware. Co-Authored-By: Claude Opus 5.5 --- cadjoint/plugins/registry.py | 17 ++ cadjoint/tier.py | 53 +++++-- cadjoint/viewer/schema/api.py | 4 + cadjoint/viewer/schema/payloads.d.ts | 2 + .../viewer/static/assets/design-B0kjMxCa.js | 1 + .../viewer/static/assets/design-Dp3R7Pgn.js | 1 - .../{main-Bt9SgKsp.js => main-CWRSwsGN.js} | 104 ++++++------- ...{panels-jwafX2lo.js => panels-B0c2leWX.js} | 0 .../viewer/static/assets/panels-B9sqm32f.css | 1 + .../viewer/static/assets/panels-DiKif7Tu.css | 1 - cadjoint/viewer/static/design.html | 6 +- cadjoint/viewer/static/index.html | 6 +- docs/viewer.qmd | 17 ++ frontend/e2e/brepInspector.spec.ts | 19 ++- frontend/e2e/export.spec.ts | 13 +- frontend/e2e/tierTags.spec.ts | 146 ++++++++++++++++++ frontend/src/App.tsx | 2 + frontend/src/api.ts | 10 ++ frontend/src/components/ExportDialog.tsx | 36 +++-- frontend/src/components/RenderPanel.tsx | 53 +++++-- .../src/components/simulate/MeshesTab.tsx | 13 ++ frontend/src/components/ui/TierTag.tsx | 48 ++++++ frontend/src/components/ui/index.ts | 1 + .../src/components/viewer/BrepInspector.tsx | 2 +- frontend/src/design/sections/Components.tsx | 21 +++ frontend/src/shell/brepCycle.ts | 10 ++ frontend/src/styles.css | 40 +++++ frontend/src/tier.ts | 51 ++++++ frontend/test/tier.test.ts | 47 ++++++ tests/plugins/test_public_tier_switch.py | 76 +++++++++ 30 files changed, 696 insertions(+), 105 deletions(-) create mode 100644 cadjoint/viewer/static/assets/design-B0kjMxCa.js delete mode 100644 cadjoint/viewer/static/assets/design-Dp3R7Pgn.js rename cadjoint/viewer/static/assets/{main-Bt9SgKsp.js => main-CWRSwsGN.js} (66%) rename cadjoint/viewer/static/assets/{panels-jwafX2lo.js => panels-B0c2leWX.js} (100%) create mode 100644 cadjoint/viewer/static/assets/panels-B9sqm32f.css delete mode 100644 cadjoint/viewer/static/assets/panels-DiKif7Tu.css create mode 100644 frontend/e2e/tierTags.spec.ts create mode 100644 frontend/src/components/ui/TierTag.tsx create mode 100644 frontend/src/tier.ts create mode 100644 frontend/test/tier.test.ts create mode 100644 tests/plugins/test_public_tier_switch.py diff --git a/cadjoint/plugins/registry.py b/cadjoint/plugins/registry.py index c1f1345d..d872ec2a 100644 --- a/cadjoint/plugins/registry.py +++ b/cadjoint/plugins/registry.py @@ -32,6 +32,12 @@ url = "http://thermal.cadjoint.svc.cluster.local:8000" schema_hash = "sha256:2a1f..." +**Seeing the public tier.** ``CADJOINT_PUBLIC_TIER=1`` leaves the private +tier's kinds (:data:`cadjoint.tier.KINDS`) out of the process-wide registry +wherever they come from — what a public install sees, on a machine that has +``diff-brep`` installed. Child processes inherit it, so a playground started +with it answers every request as the public tier does. + Callers ask by name (:func:`get_plugin`) for one specific component, or by kind (:func:`plugin_for_kind`) for whatever currently fills a slot — which is what :mod:`cadjoint.fem.tesseracts.chain` and @@ -57,6 +63,8 @@ ENTRY_POINT_GROUP = "cadjoint.plugins" #: Environment variable naming a config file (or a directory holding one). CONFIG_ENV = "CADJOINT_PLUGINS" +#: Environment variable that hides the private tier's kinds (``1`` / ``true``). +PUBLIC_TIER_ENV = "CADJOINT_PUBLIC_TIER" #: The file name looked for in a config directory. CONFIG_NAME = "plugins.toml" @@ -464,9 +472,18 @@ def registry() -> PluginRegistry: with _REGISTRY_LOCK: if _REGISTRY is None: _REGISTRY = build_registry() + if public_tier_forced(): + from cadjoint.plugins.contracts import KIND_CONTRACTS + + _REGISTRY = _REGISTRY.without(KIND_CONTRACTS) return _REGISTRY +def public_tier_forced() -> bool: + """Whether ``$CADJOINT_PUBLIC_TIER`` asks for the public tier only.""" + return os.environ.get(PUBLIC_TIER_ENV, "").strip().lower() in ("1", "true", "yes", "on") + + def set_registry(new: PluginRegistry | None) -> PluginRegistry | None: """Install ``new`` as the process-wide registry, returning the old one. diff --git a/cadjoint/tier.py b/cadjoint/tier.py index 99dccdca..fd87ae9e 100644 --- a/cadjoint/tier.py +++ b/cadjoint/tier.py @@ -68,31 +68,51 @@ #: What the Meshes window shows for a Gmsh mesh without the tier. GEOMETRY_FROZEN_NOTE = "GEOMETRY FROZEN · DIFF-BREP NOT INSTALLED" -#: The one sentence per kind, written for a public user. +#: The one sentence per kind, written for a public user: what the public +#: tier does instead, and that the exact path is the private tier's. The +#: viewer prints these verbatim beside the feature they limit. _MESSAGES: dict[str, str] = { PluginKind.NODE_MAP: ( - "node positions of a Gmsh mesh do not follow the design without diff-brep: " - "the mesh is frozen geometry. Use mesher='tetgen' for a differentiable mesh, " - "or install diff-brep." + "A Gmsh mesh is frozen geometry in the public tier: its nodes do not follow " + "the design, because the node map is part of the private tier (diff-brep). " + "Use mesher='tetgen' for a mesh that follows the design, or install diff-brep." ), PluginKind.FEATURE_EDGES: ( - "feature edges are read off the lattice without diff-brep; the exact edge " - "curves of the derived B-rep are the private tier's." + "Feature edges are read off the dual-contouring lattice in the public tier, " + "so they are approximate; exact edge curves are part of the private tier " + "(diff-brep)." ), PluginKind.BREP: ( - "the derived B-rep is the private tier's; without diff-brep there is no B-rep " - "object, only the dual-contour mesh." + "Exact B-rep extraction is part of the private tier (diff-brep); the public " + "tier has the dual-contour mesh only." ), PluginKind.STEP_EXPORT: ( - "STEP export is faceted without diff-brep; analytic planes and cylinders from " - "the derived B-rep are the private tier's." + "STEP export is faceted in the public tier: triangles from dual contouring. " + "Exact planes, cylinders and edge curves are part of the private tier " + "(diff-brep)." ), PluginKind.DRAG: ( - "dragging a B-rep handle solves the design through the derived B-rep, which " - "is the private tier's; install diff-brep." + "Dragging a B-rep handle solves the design through the exact B-rep, which is " + "part of the private tier (diff-brep)." ), } +#: Per kind, the tag the viewer shows beside the feature: ``(with the private +#: tier, without it)``. What you get, then where it comes from. +_LABELS: dict[str, tuple[str, str]] = { + PluginKind.NODE_MAP: ("Follows design · private tier", "Frozen geometry · public tier"), + PluginKind.FEATURE_EDGES: ("Exact curves · private tier", "Approximate · lattice"), + PluginKind.BREP: ("Exact · private tier", "Dual contouring · public tier"), + PluginKind.STEP_EXPORT: ("Exact surfaces · private tier", "Faceted · public tier"), + PluginKind.DRAG: ("Exact · private tier", "Unavailable · public tier"), +} + + +def label(kind: str, available: bool) -> str: + """The short tag for ``kind``: what the feature is in this tier.""" + exact, public = _LABELS.get(str(kind), ("Private tier", "Unavailable · public tier")) + return exact if available else public + class TierUnavailable(RuntimeError): """A private-tier kind was needed and is not filled (or not compatible). @@ -155,7 +175,14 @@ def as_dict(self) -> dict[str, Any]: "installed": self.installed, "provider": PROVIDER, "contract_version": CONTRACT_VERSION, - "kinds": {kind: asdict(entry) for kind, entry in self.kinds.items()}, + "kinds": { + kind: { + **asdict(entry), + "label": label(kind, entry.available), + "message": None if entry.available else message(kind, entry.reason), + } + for kind, entry in self.kinds.items() + }, } def flags(self) -> dict[str, bool]: diff --git a/cadjoint/viewer/schema/api.py b/cadjoint/viewer/schema/api.py index ba569e14..d700cddc 100644 --- a/cadjoint/viewer/schema/api.py +++ b/cadjoint/viewer/schema/api.py @@ -129,6 +129,10 @@ class KindStatus(Strict): contract_version: int | None compatible: bool reason: str | None + label: str + """The tag the viewer shows beside the feature, e.g. ``"Faceted · public tier"``.""" + message: str | None = None + """Without the kind: what the public tier does instead (:func:`cadjoint.tier.message`).""" class CapabilitiesResponse(Strict): diff --git a/cadjoint/viewer/schema/payloads.d.ts b/cadjoint/viewer/schema/payloads.d.ts index f8ccce37..85f6190c 100644 --- a/cadjoint/viewer/schema/payloads.d.ts +++ b/cadjoint/viewer/schema/payloads.d.ts @@ -640,6 +640,8 @@ export interface KindStatus { contract_version: number | null; compatible: boolean; reason: string | null; + label: string; + message?: string | null; } /** ``GET /api/capabilities``: which private plugin kinds this server can reach. */ diff --git a/cadjoint/viewer/static/assets/design-B0kjMxCa.js b/cadjoint/viewer/static/assets/design-B0kjMxCa.js new file mode 100644 index 00000000..d15ee267 --- /dev/null +++ b/cadjoint/viewer/static/assets/design-B0kjMxCa.js @@ -0,0 +1 @@ +import{C as G,d as me,i as n,c as t,S as H,a as y,s as h,b as j,e as mn,f as Vt,F as C,g as N,t as l,h as Se,j as J,k as F,l as Gt,o as ze,r as vn,L as gn,m as pn,n as ve,p as Q,u as qt,q as bn,v as $n,w as ie,x as Ut,y as Yt,z as Ce,B as fn,P as yn,A as Be,D as he,E as Xt,M as He,G as Ke,H as Ve,I as _n,J as Jt,K as wn,N as xn,O as kn,R as Sn,Q as Cn,T as An,U as En,V as Tn,W as On,X as Mn,Y as In,Z as Ln,_ as Nn,$ as Dn,a0 as Zt,a1 as Rn,a2 as jn,a3 as Pn,a4 as Fe,a5 as Te,a6 as Fn,a7 as Wn,a8 as zn,a9 as Qt,aa as Bn,ab as Hn,ac as nt,ad as lt,ae as Kn}from"./panels-B0c2leWX.js";var Vn=l("

"),Gn=l("

"),qn=l("

"),Un=l("

"),Yn=l("

"),Xn=l("

·
"),Jn=l(":1"),Zn=l(""),Qn=l("
ground sheet "),el=l(''),tl=l("

Each choice restyles the whole page. Download keeps the selection and every resolved value; tokens.ts can be set from the file.