Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cadjoint/plugins/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"

Expand Down Expand Up @@ -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.

Expand Down
53 changes: 40 additions & 13 deletions cadjoint/tier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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]:
Expand Down
4 changes: 4 additions & 0 deletions cadjoint/viewer/schema/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions cadjoint/viewer/schema/payloads.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
1 change: 1 addition & 0 deletions cadjoint/viewer/static/assets/design-B0kjMxCa.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion cadjoint/viewer/static/assets/design-Dp3R7Pgn.js

This file was deleted.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions cadjoint/viewer/static/assets/panels-B9sqm32f.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion cadjoint/viewer/static/assets/panels-DiKif7Tu.css

This file was deleted.

6 changes: 3 additions & 3 deletions cadjoint/viewer/static/design.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:," />
<title>CADJOINT Design language</title>
<script type="module" crossorigin src="/assets/design-Dp3R7Pgn.js"></script>
<link rel="modulepreload" crossorigin href="/assets/panels-jwafX2lo.js">
<link rel="stylesheet" crossorigin href="/assets/panels-DiKif7Tu.css">
<script type="module" crossorigin src="/assets/design-B0kjMxCa.js"></script>
<link rel="modulepreload" crossorigin href="/assets/panels-B0c2leWX.js">
<link rel="stylesheet" crossorigin href="/assets/panels-B9sqm32f.css">
<link rel="stylesheet" crossorigin href="/assets/design-DCZd5rah.css">
</head>
<body class="dl-body">
Expand Down
6 changes: 3 additions & 3 deletions cadjoint/viewer/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:," />
<title>CADJOINT Playground</title>
<script type="module" crossorigin src="/assets/main-Bt9SgKsp.js"></script>
<link rel="modulepreload" crossorigin href="/assets/panels-jwafX2lo.js">
<link rel="stylesheet" crossorigin href="/assets/panels-DiKif7Tu.css">
<script type="module" crossorigin src="/assets/main-CWRSwsGN.js"></script>
<link rel="modulepreload" crossorigin href="/assets/panels-B0c2leWX.js">
<link rel="stylesheet" crossorigin href="/assets/panels-B9sqm32f.css">
<link rel="stylesheet" crossorigin href="/assets/main-ByvcS6tS.css">
</head>
<body>
Expand Down
17 changes: 17 additions & 0 deletions docs/viewer.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ inspector is on. Without diff-brep the switch is greyed out with the reason;
when the extraction raises, the card prints the exception and the frame it
came from, and the solid stays on screen.

### Public and private tier

The exact paths are part of the private tier (diff-brep): the derived
B-rep, its edge curves, analytic STEP, and the node map that lets a Gmsh mesh
follow the design. Without diff-brep each of these features still runs, on
dual contouring, and the viewer labels it where you choose it. A small chip
says what you get and where it comes from: **Feature edges** reads
`Approximate · lattice` and the STEP entry in **File › Export…** reads
`STEP · faceted`. **B-rep inspector** is disabled, and a Gmsh mesh in the
Meshes window reads `Frozen geometry · public tier`. A dashed chip edge marks
the public form, and one line under the control gives the reason. With
diff-brep the same chips read `Exact … · private tier`. The viewer reads all
of this once from `GET /api/capabilities`, and the wording comes from
`cadjoint.tier`, which the library's own refusals also use. To see the public
tier on a machine with diff-brep installed, start the playground with
`CADJOINT_PUBLIC_TIER=1`.

### Quality and path tracing

Ultra is the default tier; the performance readout shows what it costs.
Expand Down
19 changes: 11 additions & 8 deletions frontend/e2e/brepInspector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,21 @@ test("the inspector draws the derived B-rep and reads it out", async () => {
if (!opened) return;
const { browser, page } = opened;
try {
await flip(page, "toggle-showBrep");
await expect(page.getByTestId("brep-inspector")).toBeVisible();
const status = await settled(page);
if (/Not installed/.test(status)) {
// Public tier only: the mode says so and stays off.
await expect(page.getByTestId("brep-message")).toContainText(/diff-brep/i);
const brep = (await page.evaluate(async () => (await fetch("/api/capabilities")).json())).kinds
.brep as { available: boolean; message: string | null };
if (!brep.available) {
// Public tier: the switch is disabled up front, with the server's
// sentence under it, and the inspector never asks for a B-rep.
await page.getByTestId("display-options").click();
await page.getByTestId("render-customize").click();
await expect(page.getByTestId("toggle-showBrep")).not.toBeChecked();
await expect(page.getByTestId("toggle-showBrep")).toBeDisabled();
await expect(page.getByTestId("toggle-showBrep")).not.toBeChecked();
await expect(page.getByTestId("tier-reason-brep")).toHaveText(brep.message!);
return;
}
await flip(page, "toggle-showBrep");
await expect(page.getByTestId("brep-inspector")).toBeVisible();
const status = await settled(page);
expect(status).toBe("Derived");

// The card's counts.
Expand All @@ -86,7 +89,7 @@ test("the inspector draws the derived B-rep and reads it out", async () => {
// Every legend row is a kind the server named, with a count; the counts
// add up to the edge total.
const counts = await page
.locator(".brep-legend-row:not(.brep-off-surface) small")
.locator(".brep-legend-row:has([data-testid^=brep-kind-]) small")
.allTextContents();
expect(counts.reduce((sum, text) => sum + Number(text), 0)).toBe(edges);
await expect(page.getByTestId("brep-kind-traced")).toBeChecked();
Expand Down
13 changes: 11 additions & 2 deletions frontend/e2e/export.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ test("File → Export… downloads the starter scene as a binary STL", async ({
await expect(page.getByTestId("export-name")).toHaveValue("scene");
await expect(page.getByTestId("export-resolution")).toHaveValue("64");
await pickOption(page, "export-format", "step");
await expect(page.getByTestId("export-note")).toContainText("B-rep");
await expect(page.getByTestId("export-option-analytic")).toBeChecked();
const exact = (await page.evaluate(async () => (await fetch("/api/capabilities")).json())).kinds
.step_export.available as boolean;
if (exact) {
await expect(page.getByTestId("export-note")).toContainText("B-rep");
await expect(page.getByTestId("export-option-analytic")).toBeChecked();
} else {
// The public tier says it at the choice: faceted, the analytic option off.
await expect(page.getByTestId("export-note")).toContainText("faceted");
await expect(page.getByTestId("export-option-analytic")).toBeDisabled();
await expect(page.getByTestId("export-option-analytic")).not.toBeChecked();
}
await pickOption(page, "export-format", "stl");
await expect(page.getByTestId("export-option-binary")).toBeChecked();

Expand Down
146 changes: 146 additions & 0 deletions frontend/e2e/tierTags.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { mkdirSync } from "node:fs";
import { chromium, expect, test, type Browser, type Page } from "@playwright/test";
import { pickOption } from "./helpers/select";

/**
* The tier, said at every feature it limits.
*
* Reads `/api/capabilities` from the page and checks each tagged feature
* against it, so the same spec holds on either tier:
*
* private: the tags say "Exact … · private tier" and every control runs;
* public (`CADJOINT_PUBLIC_TIER=1` on the server, or no diff-brep): the
* B-rep inspector is disabled with the server's sentence, feature edges
* are tagged approximate, STEP is "STEP · faceted" with the analytic
* option off and the reason in place of the note, and a Gmsh mesh is
* tagged frozen geometry.
*
* With `CADJOINT_TIER_SCREENS=<dir>` it photographs each place, file names
* prefixed with the tier:
*
* CADJOINT_E2E_PORT=6812 CADJOINT_TIER_SCREENS=/tmp/tier npx playwright test e2e/tierTags.spec.ts
*/

const PORT = process.env.CADJOINT_E2E_PORT ?? 8799;
const SHOTS = process.env.CADJOINT_TIER_SCREENS ?? "";

type Kinds = Record<string, { available: boolean; label: string; message?: string | null }>;

async function waitForCompile(page: Page) {
await expect(page.getByTestId("status")).not.toHaveText(/^(|Starting…)$/, { timeout: 120_000 });
await expect(page.getByTestId("toolbar-busy")).toHaveCount(0, { timeout: 120_000 });
}

async function open(): Promise<{ browser: Browser; page: Page }> {
const browser = await chromium.launch({
args: ["--enable-unsafe-webgpu", "--enable-features=Vulkan,WebGPU", "--use-angle=metal", "--enable-gpu"],
});
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
await page.goto(`http://127.0.0.1:${PORT}/`);
await waitForCompile(page);
const dismiss = page.getByRole("button", { name: "Dismiss" });
if (await dismiss.isVisible().catch(() => false)) await dismiss.click();
return { browser, page };
}

async function shot(page: Page, tier: string, name: string) {
if (!SHOTS) return;
mkdirSync(SHOTS, { recursive: true });
await page.waitForTimeout(600);
await page.screenshot({ path: `${SHOTS}/${tier}-${name}.png` });
}

async function recompile(page: Page, edit: (text: string) => string) {
await page.evaluate((body) => {
type EditorLike = {
view?: { state: { doc: { length: number; toString(): string } }; dispatch: (spec: unknown) => void };
};
const content = document.querySelector("[data-testid=editor] .cm-content") as
| (HTMLElement & { cmView?: EditorLike; cmTile?: EditorLike })
| null;
const view = content?.cmView?.view ?? content?.cmTile?.view;
if (!view) throw new Error("no CodeMirror view");
// eslint-disable-next-line no-new-func
const next = new Function("text", body)(view.state.doc.toString()) as string;
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: next } });
}, `return (${edit.toString()})(text);`);
await page.getByTestId("run").click();
await waitForCompile(page);
}

test("every tier-limited feature says which tier it runs on", async () => {
test.setTimeout(300_000);
const { browser, page } = await open();
try {
const kinds = (await page.evaluate(async () => (await fetch("/api/capabilities")).json()))
.kinds as Kinds;
const tier = kinds.brep.available ? "private" : "public";

// ── the render popover: B-rep inspector and feature edges ───────────
await page.getByTestId("display-options").click();
await page.getByTestId("render-customize").click();
const inspector = page.getByTestId("toggle-showBrep");
await expect(page.getByTestId("tier-brep")).toHaveText(kinds.brep.label);
await expect(page.getByTestId("tier-feature_edges")).toHaveText(kinds.feature_edges.label);
if (kinds.brep.available) {
await expect(inspector).toBeEnabled();
await expect(page.getByTestId("tier-reason-brep")).toHaveCount(0);
} else {
await expect(inspector).toBeDisabled();
await expect(page.getByTestId("tier-reason-brep")).toHaveText(kinds.brep.message!);
await expect(page.getByTestId("tier-brep")).toHaveAttribute("data-available", "false");
}
await inspector.scrollIntoViewIfNeeded();
await shot(page, tier, "render-popover");
await page.getByTestId("display-options").click();

// ── File › Export…: STEP at the point of choice ────────────────────
await page.getByTestId("menu-file").click();
await page.getByTestId("menu-file-export").click();
await expect(page.getByTestId("export-dialog")).toBeVisible();
await pickOption(page, "export-format", "step");
await expect(page.getByTestId("export-tier")).toHaveText(kinds.step_export.label);
const analytic = page.getByTestId("export-option-analytic");
if (kinds.step_export.available) {
await expect(analytic).toBeEnabled();
await expect(page.getByTestId("export-tier-reason")).toHaveCount(0);
} else {
await expect(analytic).toBeDisabled();
await expect(analytic).not.toBeChecked();
await expect(page.getByTestId("export-tier-reason")).toHaveText(kinds.step_export.message!);
}
await shot(page, tier, "export-step");
// The choice itself is labelled: the picker's STEP entry.
await page.getByTestId("export-format").click();
await expect(page.getByTestId("export-format-option-step")).toHaveText(
kinds.step_export.available ? /STEP$/ : /STEP · faceted$/,
);
await shot(page, tier, "export-picker");
await page.getByTestId("export-format-option-step").click();
await page.keyboard.press("Escape");
await expect(page.getByTestId("export-dialog")).toHaveCount(0);

// ── a Gmsh mesh in the Meshes window ────────────────────────────────
// A second mesh of the sink, filled by Gmsh. Its own declaration: the
// starter's optimization runs over `sink-mesh`, and over a frozen Gmsh
// mesh it would (rightly) refuse at compile.
await recompile(page, (text: string) =>
text +
'\ngmsh_mesh = SimMesh(name="gmsh-mesh", resolution=(18, 13, 11), bounds=(-1.05, -0.8, -0.3),' +
' size=(2.1, 1.6, 1.4), method="tet10", mesher="gmsh", domain=thermal_body)\n',
);
await page.getByTestId("editmode-simulate").click();
await page.getByTestId("window-tab-meshes").click();
const tag = page.getByTestId("mesh-tier-tag-gmsh-mesh");
await expect(tag).toHaveText(kinds.node_map.label, { timeout: 30_000 });
if (!kinds.node_map.available) {
await expect(page.getByTestId("mesh-tier-gmsh-mesh")).toContainText(kinds.node_map.message!);
}
// A TetGen mesh carries no tag: it follows the design in either tier.
await expect(page.getByTestId("mesh-tier-sink-mesh")).toHaveCount(0);
await page.getByTestId("mesh-gmsh-mesh").scrollIntoViewIfNeeded();
await shot(page, tier, "meshes-gmsh");
} finally {
await browser.close();
}
});
Loading
Loading