diff --git a/.agents/skills/marimo-pair/SKILL.md b/.agents/skills/marimo-pair/SKILL.md deleted file mode 100644 index 15d5b088..00000000 --- a/.agents/skills/marimo-pair/SKILL.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -name: marimo-pair -description: >- - Drive a live marimo notebook as a workspace: run Python in the same kernel - the user does, inspect live notebook state, and commit durable notebook - changes. Use when the user wants to start a marimo notebook or pair on an - active marimo session. -allowed-tools: Bash(bash **/scripts/discover-servers.sh *), Bash(bash **/scripts/execute-code.sh *), Read ---- - -marimo is a reactive Python runtime for building reproducible Python programs -(marimo notebooks). Cells are connected by the variables they define and -reference. Running a cell re-executes dependents in dataflow order. The active -runtime holds the kernel namespace, cell state, and dataflow graph. The -notebook (`.py` file) is the artifact the kernel writes from that state while a -session is running. - -A user interacts with the same runtime via a notebook UI with cells, outputs, -and widgets. - -**WARNING. The active runtime is the source of truth.** During a session, you -SHOULD NOT modify the associated `.py` file directly. File edits WILL NOT reach -the active kernel or user, and the kernel may overwrite them on save. Use -`marimo._code_mode` (`cm`) for notebook changes. Reading disk is fine, but -prefer `ctx.cells[...].code` for current cell code. - -The harness reports the absolute path to this `SKILL.md`. Resolve bundled -`scripts/...` and `reference/...` paths from its parent directory, even when -the current working directory is a notebook workspace. In command examples, -replace `/absolute/path/to/marimo-pair` with that directory. - -## Required first kernel command - -Start every code-mode session with this dedicated command: - -```bash -bash /absolute/path/to/marimo-pair/scripts/execute-code.sh \ - --url http://localhost:2718 \ - -c "import marimo._code_mode as cm; help(cm)" -``` - -Follow this order for each kernel, including read-only tasks: - -1. Run the inspection command once. -2. Wait for successful `help(cm)` output. -3. Then use `cm.get_context()` or another `cm` API in a later call. - -Do not run task-specific `cm` code before the inspection command succeeds. - -## Connect to a Notebook - -Use the bundled `execute-code.sh` from the reported skill directory or MCP -(`execute_code(...)`) to run Python in a live marimo kernel. - -`execute-code.sh` always takes `--url`. If the user provides a notebook URL, -run the required inspection against it directly: - -```bash -bash /absolute/path/to/marimo-pair/scripts/execute-code.sh \ - --url http://localhost:2718 \ - -c "import marimo._code_mode as cm; help(cm)" -``` - -After that command succeeds, pass task code with `-c CODE`, `-` for stdin, or -a file path: - -```bash -bash /absolute/path/to/marimo-pair/scripts/execute-code.sh \ - --url http://localhost:2718 - <<'PY' -import marimo._code_mode as cm - -async with cm.get_context() as ctx: - cid = ctx.create_cell("x = df.head()") - ctx.run_cell(cid) -PY -``` - -If the user gives no URL, find or start a notebook. Look for a running server -with `bash /absolute/path/to/marimo-pair/scripts/discover-servers.sh`, MCP -`list_sessions()`, or local process context, and pass the `url` it reports to -`--url`. With one notebook open, the script targets it automatically; with -several, pass `--file` with the notebook's file key. - -If no server is running and the user wants a notebook, start marimo with -`--no-token` (and without `--headless`) so it auto-registers for discovery. The -notebook UI must be open for `execute-code` to target it. The right invocation -depends on context (project tooling, global install, sandbox mode). If the -notebook file contains a PEP 723 `# -/// script` header, it MUST be opened with `--sandbox` — otherwise marimo -ignores the inline dependencies. See -[finding-marimo.md](reference/finding-marimo.md) for the full decision tree and -[execution-context.md](reference/execution-context.md) for selector resolution, -scripts, MCP, and shell quoting. - -## Scratchpad Scope - -`execute-code` evaluates Python in marimo's scratchpad: a temporary namespace -with a shallow copy of the kernel globals. Notebook variables are available by -name, but new top-level bindings and rebindings are discarded after each call. -In-place mutations to notebook-owned objects can persist because those names -still reference live objects. - -Each call reports stdout and stderr from the scratchpad, plus console output -from notebook cells it causes to run, including reactive descendants. - -### Ordinary Python - -Use ordinary Python in the scratchpad to inspect variables, sample data, test -transformations, probe APIs, check imports, and read widget state. - -```python -print(df.head()) - -x = 10 -print(x) -``` - -Here `df` comes from notebook globals, while `x` is a scratchpad-local binding. -`x` exists for this call only and WILL NOT be added to notebook globals. - -### Persist with `cm` - -Top-level scratchpad assignments and rebindings are temporary. To persist work, -including new variables, you MUST submit changes through `marimo._code_mode` -(`cm`). - -`marimo._code_mode` is a PRIVATE, UNSTABLE agent API (note the leading -underscore). It exists for tools like this skill to drive a live kernel from -the scratchpad. DO NOT import it from notebook cells, library code, or -anything a user would run — methods can change or disappear across marimo -versions and kernels. Treat every `import marimo._code_mode as cm` as -scratchpad-only. - -Open a code-mode context to queue notebook changes. - -```python -import marimo._code_mode as cm - -async with cm.get_context() as ctx: - cid = ctx.create_cell("x = df.head()") - ctx.run_cell(cid) -``` - -The scratchpad supports top-level async code. Use `async with` directly; -wrapping it in `asyncio.run(...)` is unnecessary and can conflict with the -kernel's event loop. - -After this block exits and the new cell runs, `x` is notebook state. Later -scratchpad calls can read `x` by name. Code later in the same scratchpad call -should read `ctx.globals["x"]`, because the scratchpad namespace was copied -before the cell ran. - -Inside the context, queued mutation methods are synchronous. Call them -directly; do not `await` them. Each call queues an operation for marimo to -apply when the context exits normally. If the block raises, the queue is -discarded. - -On clean exit, marimo applies packages, validates and applies structural cell -changes, runs queued cells, then may run dependents. Validation is only -structural since queued cell runs can still error. `create_cell` and -`edit_cell` change notebook structure only. Use `run_cell` to execute. - -`create_cell` currently defaults to `hide_code=True`, which collapses the code -editor in the UI. Pass `hide_code=False` if the user wants created cells to -be visible without manually expanding them. - - -## Marimo Rules - -marimo imposes a small contract on notebook code so it can keep the notebook as -a directed acyclic graph (DAG): - -- **No cycles** - cells cannot depend on each other in a cycle. -- **No public redefinitions across cells** - each name has one owning cell. -- **No wildcard imports** - `import *` prevents static analysis of definitions. - -These rules keep the kernel, UI, and saved artifact consistent. - -When `cm` submits a cell body, marimo parses its top-level definitions and -references. Public names enter the graph. Names that start with `_` are local -to their cell and unavailable to other cells. If a `cm` edit violates the -contract, marimo rejects the structural change and returns the validation -error. - -## The Notebook's Shape - -A notebook is an ordered collection of cells. `ctx.cells` is the document view -and `ctx.graph` is the dataflow view. - -```python -for cell in ctx.cells: - cell # .id, .code, .name, .config, .status, .errors - -ctx.cells["setup"] # by name -ctx.cells[0] # by position -list(ctx.cells.keys()) # all IDs, in notebook order -``` - -Cell IDs are opaque strings which can be queried from the notebook or captured -from `cm` return values: - -```python -cid = ctx.create_cell("df = pd.read_csv('data.csv')") -print(cid) # e.g. 'Hbol' -``` - -Alternatively, cells can be assigned and referenced by `name`. The graph can be -used to understand its role in the dataflow. - -```python -for cid, impl in ctx.graph.cells.items(): - impl # .defs, .refs (sets of public names) - -ctx.graph.descendants(cid) # cells that re-run when this one changes -ctx.graph.ancestors(cid) # cells this one depends on -``` - -In marimo, deletes are *destructive* so it can be useful to query the -descendants prior to deleting to understand it's impact. - -## Writing Notebook Changes - -The graph contract keeps marimo able to run and save the notebook. Passing -those checks alone does not guarantee a useful artifact. Committed cells should -still be readable, rerunnable, and editable. - -Make durable edits that reuse the notebook's existing names, imports, -dependencies, and UI model. Don't be lazy. Avoid one-off workarounds that pass -`cm` validation but leave a brittle notebook. - -### Cell Bodies - -Submit the code that belongs in the cell. - -- **Submit cell contents** - `create_cell` and `edit_cell` take cell contents, - not saved-file `@app.cell` wrappers. -- **Read before replacing** - for now, another editor may change a cell between - scratchpad calls. Before `edit_cell`, read the current body from - `ctx.cells[...]` and submit the full replacement. -- **Reuse notebook imports** - if `np` already exists, use it or edit the owning - import cell. DO NOT add `import numpy as _np` just to bypass the graph. -- **Define each public name once** - a public name has one owning cell. - Reassigning it in another cell fails with `Multiply-defined names`; edit the - owning cell or give the result a new name. See - [gotchas.md](reference/gotchas.md). -- **Run cells deliberately** - `create_cell` and `edit_cell` change structure - only. Queue `ctx.run_cell(...)` when the cell should execute. - -### Cell Boundaries - -A cell is also a rerun boundary. Put expensive or reusable computation -upstream of presentation so UI edits stay cheap. Keep cheap, -presentation-specific work with the view when that is easier to read. - -Use `mo.vstack` and `mo.hstack` only when the composition is part of the UI. -Narrative often reads better in an adjacent markdown cell. - -### Prefer `cm`-Managed Changes - -Use `cm` APIs when they exist. Avoid direct file edits, shell package commands, -and scratchpad-only state for changes that should persist. - -- **Do not edit the `.py` artifact** - DO NOT use `Edit`, `Write`, or - `NotebookEdit` on the notebook file during a live session. Use - `ctx.edit_cell(...)` even for small changes. -- **Manage packages through `cm`** - use `ctx.packages.add()` or - `ctx.packages.remove()` instead of direct `uv` or `pip`; confirm - non-obvious dependency changes. -- **Avoid transient paths** - persisted cells should not depend on `/tmp/...` - unless the work is intentionally transient. -- **Delete deliberately** - deleting a cell removes globals it defines. Reuse - empty cells when convenient and delete cells left empty after edits. - -### UI and Widgets - -Inspect the object before changing it. Different UI objects update through -different paths. - -- **Set `mo.ui.*` through `cm`** - use `ctx.set_ui_value(element, value)` inside - `cm.get_context()`. -- **Set anywidget traitlets directly** - synced traitlets are Python - attributes, for example `widget.value = 5`. - -For designing custom visual or interactive output, see -[rich-representations.md](reference/rich-representations.md). - -## References - -- [execution-context.md](reference/execution-context.md) — scripts, MCP, auth, startup, and shell quoting -- [finding-marimo.md](reference/finding-marimo.md) — choosing the right marimo invocation -- [gotchas.md](reference/gotchas.md) — name redefinition, cached module proxies, and notebook traps -- [rich-representations.md](reference/rich-representations.md) — custom widgets and visualizations -- [notebook-improvements.md](reference/notebook-improvements.md) — improving existing notebooks diff --git a/.agents/skills/marimo-pair/reference/execution-context.md b/.agents/skills/marimo-pair/reference/execution-context.md deleted file mode 100644 index 6db6f322..00000000 --- a/.agents/skills/marimo-pair/reference/execution-context.md +++ /dev/null @@ -1,70 +0,0 @@ -# Connection Troubleshooting - -Use this reference when `execute-code.sh` or MCP cannot reach the intended -marimo session, cannot select a session, or fails because code was passed -incorrectly. - -## Targeting - -- Pass the discovered `--url` unchanged; `null` means no address was reachable. -- With one open notebook, the script resolves its session. -- Use `--file` for an exact, stable file key or path. It resolves the current - session on every call. -- Use `--session` for an exact active session ID; it goes stale on reconnect. -- The selectors are mutually exclusive. Preserve supplied values; never guess. - -## Auth - -For token-authenticated servers, prefer `MARIMO_TOKEN`. - -```bash -MARIMO_TOKEN=... bash scripts/execute-code.sh --url http://localhost:2718 -c "1 + 1" -``` - -`--token` also works, but may expose the token in process listings. If both are -present, `--token` overrides `MARIMO_TOKEN`. The script sends the token as -`Authorization: ******` on session discovery and code execution requests. - -## Code Input - -Pass code with `-c CODE`, `-` for stdin, or a file path. - -```bash -bash scripts/execute-code.sh --url http://localhost:2718 - <<'PY' -print(df.head()) -PY -``` - -```bash -bash scripts/execute-code.sh --url http://localhost:2718 /tmp/code.py -``` - -## Common Script Errors - -- **`[]` from discover-servers.sh** - nothing is registered. Only servers - started with `--no-token` register; otherwise ask the user for the URL, or - start marimo with the project's normal tooling. -- **No active sessions on the server** - open the notebook in the browser. -- **No active session matches `--file`** - check the exact file key against the - available paths printed by the script. -- **Multiple sessions on server** - several notebooks are open; rerun with the - `--file` path or `--session` ID shown for the notebook you want. -- **Failed to connect** - check the URL, token, and whether the server is still - running. -- **Execution did not complete** - the server ended the stream without a - result. With `--file`, retry the same command so it resolves the current - session. With `--session`, the ID is probably stale after a page refresh. -- **... is running on the Windows host but answered at no address reachable - from WSL** - WSL's network cannot reach it. The message lists the fixes; the - usual one is restarting marimo on the host with `--host 0.0.0.0`. Running the - scripts from Git Bash or PowerShell on the Windows side also works. -- **needs jq / curl on PATH** - install them in whichever environment runs the - scripts. Inside WSL that means the distro, not Windows. -- **SyntaxError** - the submitted Python was malformed; use a heredoc or file. -- **ImportError** - diagnose in the notebook kernel. Install packages through - `cm` when needed. - -## Starting marimo - -Discover first. If no server is running and the user wants a notebook, use -[finding-marimo.md](finding-marimo.md). diff --git a/.agents/skills/marimo-pair/reference/finding-marimo.md b/.agents/skills/marimo-pair/reference/finding-marimo.md deleted file mode 100644 index 793afc68..00000000 --- a/.agents/skills/marimo-pair/reference/finding-marimo.md +++ /dev/null @@ -1,117 +0,0 @@ -# Finding and Invoking marimo - -Only servers started with `--no-token` register in the local server registry -and are auto-discoverable — starting without a token makes discovery easier. -If a server has a token, set the `MARIMO_TOKEN` environment variable before -calling the execute script (avoids leaking the token in process listings). - -```sh -marimo edit notebook.py --no-token [--sandbox] -``` - -Start marimo in edit mode without `--headless` unless the user asks for a -headless server. The notebook UI must be open in a browser before marimo has an -active session for `execute-code` to target. If running headless, give the user -the local URL and wait for them to open it before executing code. - -How you invoke `marimo` depends on context — find the right way to run it. - -## Where the registry lives - -A registering server writes one JSON file per instance and removes it on clean -shutdown: `$XDG_STATE_HOME/marimo/servers` (default `~/.local/state/marimo/servers`) -on Linux and macOS, `%USERPROFILE%\.marimo\servers` on Windows. - -Inside WSL, both are in play. A notebook started in the distro registers at the -XDG path; one started on the Windows host registers in the Windows profile, -which `discover-servers.sh` also reads. Windows-host servers show up with -`"origin": "windows-host"`, and reaching them depends on WSL networking: under -the default NAT the distro has its own loopback, so a host notebook bound to -`127.0.0.1` is unreachable and only `--host 0.0.0.0` (or mirrored networking) -lets WSL connect. The `url` in each entry already accounts for this, so use it. - -Cross-OS discovery needs WSL interop: `cmd.exe` to locate the Windows profile, -and `tasklist.exe` to tell a live Windows PID from a dead one. With interop -disabled, only servers inside the distro are found. - -## Notebooks with PEP 723 metadata require `--sandbox` - -Before picking a runner, check the notebook file for a PEP 723 header: - -```python -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# "marimo", -# "polars", -# ] -# /// -``` - -If the block is present, the notebook was authored as a self-contained -sandboxed script and **SHOULD be opened with `--sandbox`**. Without the flag -marimo runs in the ambient environment and silently ignores the inline -dependencies. Imports will fail or, worse, resolve to a different version than -the author pinned. - -`--sandbox` works regardless of project context: inside a uv project, `uv run -marimo edit notebook.py --no-token --sandbox` still creates the isolated env -from the PEP 723 block rather than the project's `.venv`. - -## Inside a Python project - -If there's a `pyproject.toml` in cwd or a parent directory, check that marimo -is actually in the dependencies before using the project's runner. Look for -`marimo` in: - -- `[project.dependencies]` -- `[project.optional-dependencies]` or `[dependency-groups]` (dev deps) -- `[tool.pixi.dependencies]` -- The project's `.venv` (`uv pip show marimo` or check `.venv/bin/marimo`) - -If marimo is in a named dependency group (not the default), you need to -specify it: - -```sh -# marimo is in [dependency-groups] → "notebooks" group -uv run --group notebooks marimo edit notebook.py --no-token -``` - -Once you know marimo is available, use whatever CLI runner the project uses: - -```sh -# uv-managed project -uv run marimo edit notebook.py --no-token -# pixi-managed project -pixi run marimo edit notebook.py --no-token -``` - -Skip `--sandbox` here — the project already manages dependencies. - -If `pyproject.toml` exists but marimo is **not** in the deps, treat this as -"outside a project" (see below). - -## Outside a Python project - -Prefer `--sandbox`. Sandbox mode creates an isolated environment for the -notebook and writes dependencies into the script itself as inline PEP 723 -metadata — so the notebook stays self-contained and reproducible. - -```sh -# With uv available (preferred) -uvx marimo@latest edit notebook.py --no-token --sandbox - -# With marimo installed globally -marimo edit notebook.py --no-token --sandbox -``` - -## Global marimo install - -If marimo is installed globally, check the version — code mode shipped in -v0.21.1. If the installed version is older, prompt the user to upgrade before -proceeding. - -## Nothing found - -If no project marimo, no `uv`/`uvx`, and no global `marimo` on PATH, tell the -user to install `uv` (). diff --git a/.agents/skills/marimo-pair/reference/gotchas.md b/.agents/skills/marimo-pair/reference/gotchas.md deleted file mode 100644 index 28440b2b..00000000 --- a/.agents/skills/marimo-pair/reference/gotchas.md +++ /dev/null @@ -1,90 +0,0 @@ -# Gotchas - -## Private variables are cell-scoped - -Variables with a `_` prefix are **private to the cell that defines them** in -marimo. They cannot be referenced from other cells — you'll get a `NameError`. - -This matters when building notebooks programmatically. A common mistake: - -```python -# Cell A -_df = pd.DataFrame(results) # _df is private to this cell - -# Cell B — FAILS -mo.ui.table(_df) # NameError: name '_df' is not defined -``` - -**Fix:** Either merge both into one cell, or use a non-private name (`df`). - -## Redefining a public name across cells - -Each public name has one owning cell. Defining it again in another cell fails -with `Multiply-defined names`. This is easy to hit when building a notebook -incrementally — a second cell reassigns `df`, `results`, `data`, etc. - -```python -# Cell A -df = pd.read_csv("data.csv") - -# Cell B — FAILS: df already defined in Cell A -df = df.dropna() # Multiply-defined names: df -``` - -**Fix — pick one:** - -- **Edit the owning cell** if the step belongs there (`ctx.edit_cell`). -- **Use a new name** when later cells need the result (`clean = df.dropna()`). - -`ctx.graph.cells[cid].defs` shows what a cell already owns. - -## Duplicate public imports across cells - -The same single-definition rule applies to imports: a public name (like `pd`) -can only be defined in one cell. If two cells both `import pandas as pd`, you -get a `Multiply-defined names` error at validation. - -**Fix:** Reuse the existing import — reference `pd` directly, or edit the -owning cell (`ctx.edit_cell`) if the import belongs there instead. If several -cells each need their own copy, consolidate them into the setup cell or a -shared import-only cell (see -[notebook-improvements.md](notebook-improvements.md#setup-cell)). - -## `inspect.getsource()` on methods is indented - -`inspect.getsource()` on a class method preserves the original indentation. -Passing this to `ast.parse()` fails with `IndentationError`. - -```python -# FAILS -src = inspect.getsource(SomeClass.some_method) -tree = ast.parse(src) # IndentationError: unexpected indent - -# FIX -import textwrap -src = textwrap.dedent(inspect.getsource(SomeClass.some_method)) -tree = ast.parse(src) -``` - -## Cached module availability - -Some libraries cache optional-dependency availability at import time. Installing -a package mid-session via `ctx.packages.add()` won't update those caches. -The user may need to restart the kernel — but try known workarounds first. - -### Polars + pyarrow - -`df.to_pandas()` fails with `ModuleNotFoundError: pa.Table requires 'pyarrow'`. - -**Workaround** — if this error occurs after installing pyarrow mid-session, -run the following via `execute-code` (scratchpad), NOT in a cell. The patch -mutates the cached module object in the running kernel, so it doesn't need to -persist in the notebook. - -```python -import pyarrow as _pa -import polars.dataframe.frame as _frame_mod -_frame_mod.pa = _pa -``` - -Then re-run the failing cell. diff --git a/.agents/skills/marimo-pair/reference/notebook-improvements.md b/.agents/skills/marimo-pair/reference/notebook-improvements.md deleted file mode 100644 index e0a273ca..00000000 --- a/.agents/skills/marimo-pair/reference/notebook-improvements.md +++ /dev/null @@ -1,118 +0,0 @@ -# Notebook Improvements - -When the user asks to improve, optimize, or clean up their notebook, scan the -current cells for these opportunities. Use your judgment — don't over-apply, -and if you're unsure whether a change is worthwhile, ask the user. - -## Cell names - -Low priority unless the user asks. `setup` and cells defining -functions/classes are auto-named by marimo. Beyond that, naming is optional. -Note that naming markdown cells clutters the UI by showing the cell header -that's normally hidden. - -## Review dataflow names - -Review names after the cells work and their dependencies are visible. - -- Keep values consumed by later cells public. -- Inline small one-use bindings. -- Move dense procedural work into a function. -- Use a private top-level binding when it makes one cell easier to read. - -Many private names can mean that a useful stage is hidden or that a cell is -doing too much. Treat this as a prompt to inspect, not a rule. - -## Setup cell - -A setup cell is named `"setup"` and is guaranteed to run before all other -cells. It's the place for module imports. Consolidating imports here keeps -the notebook clean and ensures every cell can rely on those modules being -available. - -**The setup cell cannot reference other cells' variables.** It runs first, so -it must be self-contained: imports, constants, and definitions that depend only -on each other. Reading a name defined elsewhere (e.g. `df`, a UI element) fails -with `The setup cell cannot have references`. - -**Keep the setup cell import-only if you can.** marimo skips re-running -descendants when an edited cell contains *only* import statements, since -imports resolve independently of the notebook's reactive dataflow — this -applies to any import-only cell, not just `setup`. But it matters most for -`setup`, since every other cell depends on it: if the setup cell also holds -constants or other non-import definitions, editing it re-runs the *entire* -notebook, not just the imports. If you need setup-time constants too, put -them in their own cell downstream of `setup` rather than mixing them in. - -First check if the notebook already has a cell named `"setup"`. If not, create -one and hoist scattered imports into it. `name="setup"` auto-positions the cell -first — no `before`/`after` needed: - -```python -cid = ctx.create_cell('''import polars as pl -import marimo as mo -import anywidget -import traitlets''', name="setup") -ctx.run_cell(cid) -``` - -If a setup cell already exists, `create_cell(name="setup")` raises `ValueError`; -use `ctx.edit_cell("setup", code=...)` and `ctx.run_cell("setup")` instead. - -## Lift reusable functions into their own cells - -When a cell contains a single function or class that doesn't reference -variables from other cells, marimo treats it specially — it can be written as -a standalone definition and reused outside the notebook. These functions can -use modules from the setup cell. - -Look for functions that **could belong in a library**: data loading, transforms, -parsers, domain logic, custom widgets. If someone might reasonably `import` it -from another module, it's a good candidate to lift into its own cell. - -Don't lift everything — notebook-specific wiring (UI layout, display logic, -cell-level orchestration) should stay where it is. Use `_prefix` for -cell-internal helpers that aren't meant to be reused. - -```python -# before: useful logic buried in a larger cell -objects = pl.read_csv("https://example.com/objects.csv") -artists = pl.read_csv("https://example.com/artists.csv") - -def top_counts(df, col, n=5): - return df.group_by(col).len().sort("len", descending=True).head(n) - -result = top_counts(objects.join(artists, on="id"), "category") -``` - -```python -# after: top_counts is general-purpose — give it its own cell - -# cell 1 -def top_counts(df, col, n=5): - return df.group_by(col).len().sort("len", descending=True).head(n) -``` - -```python -# cell 2 -result = top_counts(df, "category") -``` - -## `mo.persistent_cache` - -`@mo.persistent_cache` caches a function's result to disk so it isn't -recomputed on subsequent runs. The cache persists across kernel restarts. - -```python -@mo.persistent_cache -def load_data(): - objects = pl.read_csv("https://example.com/objects.csv") - artists = pl.read_csv("https://example.com/artists.csv") - return objects.join(artists, on="id", how="left") - -df = load_data() -``` - -Good candidates: data loading, ETL, expensive computation that rarely changes. -Don't over-optimize — if you're unsure, suggest it to the user rather than -applying it. diff --git a/.agents/skills/marimo-pair/reference/rich-representations.md b/.agents/skills/marimo-pair/reference/rich-representations.md deleted file mode 100644 index 64cb43eb..00000000 --- a/.agents/skills/marimo-pair/reference/rich-representations.md +++ /dev/null @@ -1,303 +0,0 @@ -# Rich Representations - -Custom visual encodings for data that go beyond standard charts and tables. - -## Guiding principles - -**Visualization matters.** Helping users build custom visual representations -is one of the highest-impact things the agent can do. A bespoke encoding -tailored to the task — labeling, batch review, comparing variants — lets -users *see* their data in ways that tables and numbers never will. marimo -is an environment where users create their own views, not just consume -library charts. Help them imagine what's possible, then build it. - -**Use modern web APIs.** Models may default to older browser patterns; prefer -modern HTML, CSS, and JavaScript that are supported in current browsers. Avoid -build steps unless the task clearly needs them. - -**Prefer compact output.** marimo clips cell output at ~610px and scrolls. -Avoid hitting that limit; if you need more space, manage your own scrolling -inside a fixed-height container. - -**Keep it thin, make it compose.** A widget is a thin layer over data, not -an application. One clear purpose, few traitlets, small `_esm`. Build small -pieces that compose in the notebook — combine with other cells, UI elements, -and views. Don't over-engineer. - -## Decision tree - -| Need | Approach | -|------|----------| -| Custom output or interaction | **anywidget** — flexible enough to grow from display-only to interactive | -| Tiny static HTML representation | `_display_()` or `mo.Html` | -| Built-in control used as-is (slider, dropdown) | `mo.ui.*` | - -For custom representations, prefer anywidget unless the output is clearly a -small static one-off. - -## anywidget - -[anywidget](https://anywidget.dev) bridges Python and JavaScript via -traitlets. `.tag(sync=True)` makes a traitlet bidirectional — Python sets a -value → JS sees it; JS calls `model.set()` + `model.save_changes()` → -Python sees it. `_css` is optional global CSS. - -**marimo does not render traditional Jupyter widgets.** Libraries like jscatter, -ipyvolume, etc. often have a top-level object whose default representation is a -Jupyter widget (`application/vnd.jupyter.widget-view+json`). marimo cannot -display these — you need to find the underlying **anywidget** instance, which -marimo *does* support. - -Common pattern: look for a `.widget` attribute on the library object: - -```python -# jscatter example — Scatter is not renderable, but .widget is an anywidget -scatter = jscatter.Scatter(data=df, x="x", y="y") -scatter.widget # <-- use this in the cell output -``` - -When unsure, check in the scratchpad: - -```python -import anywidget -obj = scatter.widget # or whatever accessor the library provides -print(isinstance(obj, anywidget.AnyWidget)) # True = marimo can render it -``` - -### `_esm` lifecycle - -**Render only** (most widgets): - -```js -function render({ model, el }) { /* ... */ } -export default { render }; -``` - -**Initialize + render** (shared state across views, one-time setup): - -```js -export default () => { - return { - initialize({ model }) { - // Once per widget instance — timers, connections, shared handlers - return () => { /* cleanup */ }; - }, - render({ model, el }) { - // Once per view — display in 3 cells = 3 renders - return () => { /* cleanup DOM listeners */ }; - } - }; -}; -``` - -- `model.on()` is auto-cleaned when a view is removed -- DOM `addEventListener` is **not** — clean up with `AbortController` - -### Timer example (initialize + render) - -`initialize` owns one interval; each `render` view displays it. - -```python -import anywidget -import traitlets - -_TIMER_ESM = """ -export default () => { - return { - initialize({ model }) { - const id = setInterval(() => { - if (model.get("running")) { - model.set("seconds", model.get("seconds") + 1); - model.save_changes(); - } - }, 1000); - return () => clearInterval(id); - }, - render({ model, el }) { - const controller = new AbortController(); - const { signal } = controller; - - const span = document.createElement("span"); - span.style.cssText = "font: 24px monospace;"; - - const btn = document.createElement("button"); - btn.style.cssText = "margin-left: 8px; cursor: pointer;"; - - function update() { - const s = model.get("seconds"); - const mm = String(Math.floor(s / 60)).padStart(2, "0"); - const ss = String(s % 60).padStart(2, "0"); - span.textContent = `${mm}:${ss}`; - btn.textContent = model.get("running") ? "⏸" : "▶"; - } - - model.on("change:seconds", update); - model.on("change:running", update); - - btn.addEventListener("click", () => { - model.set("running", !model.get("running")); - model.save_changes(); - }, { signal }); - - update(); - el.append(span, btn); - return () => controller.abort(); - } - }; -}; -""" - -class Timer(anywidget.AnyWidget): - seconds = traitlets.Int(0).tag(sync=True) - running = traitlets.Bool(True).tag(sync=True) - _esm = _TIMER_ESM -``` - -### Composing with the notebook - -Widgets become reactive notebook citizens when you bridge a traitlet to -`mo.state`. This is a two-cell pattern — create the widget and wire up the -observer in one cell, read the value in another: - -```python -# Cell 1 — widget + observer -timer = Timer() - -get_seconds, set_seconds = mo.state(timer.seconds) -timer.observe(lambda _: set_seconds(timer.seconds), names=["seconds"]) - -timer # display the widget -``` - -```python -# Cell 2 — reacts to changes -seconds = get_seconds() -mo.md(f"Timer is at **{seconds}s** — {'running' if seconds > 0 else 'stopped'}") -``` - -The common pattern is `mo.state(widget.trait)` for the initial value, -`.observe()` on the specific trait name, and reading with the getter in a -downstream cell. See [Reactive anywidgets](#reactive-anywidgets-in-marimo) -for the details. - -### CDN dependencies - -Import JS libraries from [esm.sh](https://esm.sh) — no build step: - -```js -import * as d3 from "https://esm.sh/d3@7"; -import { tableFromIPC } from "https://esm.sh/@uwdata/flechette@2"; -``` - -### DataFrames and binary data - -**Prefer reducing data on the Python side.** Aggregate, filter, sample — -send the widget only what it needs. Most widgets should receive a small, -pre-processed payload via simple traitlets (lists, dicts). This keeps the -widget simple and avoids extra dependencies. - -**For large tabular data (>2k rows)** where the widget genuinely needs -row-level access, send Arrow IPC bytes instead of JSON. This adds -complexity and dependencies, so only reach for it when the data volume -justifies it. - -**Python — serialize:** - -```python -# Polars (native, no pyarrow needed) -_ipc=df.write_ipc(None).getvalue() - -# Any __arrow_c_stream__ source (pandas, narwhals, pyarrow, etc.) -import io, pyarrow as pa, pyarrow.feather as feather - -def to_arrow_ipc(data) -> bytes: - table = pa.RecordBatchReader.from_stream(data).read_all() - sink = io.BytesIO() - feather.write_feather(table, sink, compression="uncompressed") - return sink.getvalue() -``` - -**JS — deserialize with `@uwdata/flechette`:** - -```js -import { tableFromIPC } from "https://esm.sh/@uwdata/flechette@2"; -const table = tableFromIPC(new Uint8Array(model.get("_ipc").buffer)); -// table.numRows, table.numCols, table.get(i), table.getChild("col_name") -``` - -Use `traitlets.Any().tag(sync=True)` for the IPC bytes traitlet. - -## Reactive anywidgets in marimo - -When an anywidget trait (selection, value, zoom, etc.) should drive a -downstream marimo cell, use `mo.state()` + `.observe()` on the **specific -trait**. This is the preferred pattern: - -```python -# In the cell that creates the widget: -get_selection, set_selection = mo.state(widget.selection) -widget.observe( - lambda _: set_selection(widget.selection), - names=["selection"], -) - -# In a downstream cell — re-executes when selection changes: -selection = get_selection() -``` - -Initialize `mo.state()` with the widget's current trait value — not a -hardcoded default. Read the trait directly off the widget in the lambda. -Do **not** use `change["new"]` or `allow_self_loops=True`. - -### `mo.state` + `.observe()` vs `mo.ui.anywidget()` - -Two strategies for reactive anywidgets. Choose one per widget — don't mix them. - -| Strategy | Reactivity | Best for | -|----------|-----------|----------| -| `mo.state` + `.observe()` | Specific traits you pick | Precision — only named traits trigger downstream cells | -| `mo.ui.anywidget(widget)` | All synced traits as one `.value` dict | Convenience — observe everything at once | - -### Programmatic widget control (scratchpad) - -Read widget state or set UI controls from the scratchpad — no clicking: - -```python -print(timer.seconds) # read -timer.seconds = 0 # set — frontend updates automatically -``` - -`mo.ui.*` elements need `ctx.set_ui_value(...)` from code mode; anywidgets use -direct assignment. - -## `_display_()` protocol - -Any object with a `_display_()` method renders richly in marimo. Return -anything marimo can render — `mo.Html`, `mo.md()`, a chart, a string. - -Precedence: `_display_()` > built-in formatters > `_mime_()` > IPython -`_repr_*_()` methods. - -```python -from dataclasses import dataclass -import marimo as mo - -@dataclass -class ColorSwatch: - colors: list[str] - - def _display_(self): - divs = "".join( - f'
' - for c in self.colors - ) - return mo.Html(f'
{divs}
') -``` - -For inline ` + + +
+ + + diff --git a/editor-web/package-lock.json b/editor-web/package-lock.json new file mode 100644 index 00000000..378d3815 --- /dev/null +++ b/editor-web/package-lock.json @@ -0,0 +1,3018 @@ +{ + "name": "simplecad-editor-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simplecad-editor-web", + "version": "0.0.0", + "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@codemirror/language": "^6.13.1", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.43.14", + "@lezer/highlight": "^1.2.5", + "@tailwindcss/vite": "^4.3.3", + "@types/three": "^0.186.0", + "@uiw/react-codemirror": "^4.25.12", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "fflate": "^0.8.3", + "lucide-react": "^1.48.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-resizable-panels": "^4.13.3", + "tailwind-merge": "^3.7.0", + "tailwindcss": "^4.3.3", + "three": "^0.186.1", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slider": "^1.4.7", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz", + "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/language": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.13.1.tgz", + "integrity": "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/streamparser": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz", + "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz", + "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/streamparser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/streamparser/-/streamparser-6.0.0.tgz", + "integrity": "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==", + "license": "MIT" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.14", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.14.tgz", + "integrity": "sha512-hC45YnmJNR1rz42OF57BLoQNFMV12/13drnvibDsR2quNDmldcb4WvYVV81hFQg56i9lcqQMEDqilhX2QDcGQQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz", + "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz", + "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.19.tgz", + "integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz", + "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz", + "integrity": "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz", + "integrity": "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz", + "integrity": "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz", + "integrity": "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz", + "integrity": "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz", + "integrity": "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz", + "integrity": "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz", + "integrity": "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz", + "integrity": "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz", + "integrity": "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz", + "integrity": "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz", + "integrity": "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz", + "integrity": "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz", + "integrity": "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz", + "integrity": "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz", + "integrity": "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz", + "integrity": "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz", + "integrity": "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz", + "integrity": "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.7.tgz", + "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", + "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.186.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.186.0.tgz", + "integrity": "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.3", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.12", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.12.tgz", + "integrity": "sha512-wR5lz7jbVcBGyiiLEObAmDikcc39KNqrrd4bdUhav85WZu48ttgnSf1YV+grHuMsj5l/WCw6u+r9ZuSfURJgiA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.12", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.12.tgz", + "integrity": "sha512-RE0Gj/QesD7/JWPnJREG8Kq8brGv2NAH17iUECcBrLkqXzzEAgsj/4Qqvz/OlK8XWKmzlsenLRkObT9H+gYQ0w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.12", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.48.0.tgz", + "integrity": "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.85.0.tgz", + "integrity": "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.85.0", + "@oxlint/binding-android-arm64": "1.85.0", + "@oxlint/binding-darwin-arm64": "1.85.0", + "@oxlint/binding-darwin-x64": "1.85.0", + "@oxlint/binding-freebsd-x64": "1.85.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", + "@oxlint/binding-linux-arm-musleabihf": "1.85.0", + "@oxlint/binding-linux-arm64-gnu": "1.85.0", + "@oxlint/binding-linux-arm64-musl": "1.85.0", + "@oxlint/binding-linux-ppc64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-musl": "1.85.0", + "@oxlint/binding-linux-s390x-gnu": "1.85.0", + "@oxlint/binding-linux-x64-gnu": "1.85.0", + "@oxlint/binding-linux-x64-musl": "1.85.0", + "@oxlint/binding-openharmony-arm64": "1.85.0", + "@oxlint/binding-win32-arm64-msvc": "1.85.0", + "@oxlint/binding-win32-ia32-msvc": "1.85.0", + "@oxlint/binding-win32-x64-msvc": "1.85.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-resizable-panels": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.13.3.tgz", + "integrity": "sha512-k41s4nP8IXiGjnso8TsZLzoRbuxBb6buTJUOrFWrYpmbV4PGH5GHYM2RvNWWBRIJ6e+Zxlf3m9Tubk8VoPU9Ww==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", + "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==", + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + } + } +} diff --git a/editor-web/package.json b/editor-web/package.json new file mode 100644 index 00000000..ab30b075 --- /dev/null +++ b/editor-web/package.json @@ -0,0 +1,49 @@ +{ + "name": "simplecad-editor-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit -p tsconfig.json && vite build", + "lint": "oxlint", + "preview": "vite preview", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@codemirror/language": "^6.13.1", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.43.14", + "@lezer/highlight": "^1.2.5", + "@tailwindcss/vite": "^4.3.3", + "@types/three": "^0.186.0", + "@uiw/react-codemirror": "^4.25.12", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "fflate": "^0.8.3", + "lucide-react": "^1.48.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-resizable-panels": "^4.13.3", + "tailwind-merge": "^3.7.0", + "tailwindcss": "^4.3.3", + "three": "^0.186.1", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slider": "^1.4.7", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } +} diff --git a/editor-web/public/favicon.svg b/editor-web/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/editor-web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor-web/public/icons.svg b/editor-web/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/editor-web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/editor-web/src/App.tsx b/editor-web/src/App.tsx new file mode 100644 index 00000000..2d481e48 --- /dev/null +++ b/editor-web/src/App.tsx @@ -0,0 +1,433 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { CellEditor, type NotebookCell, type NotebookFeature, type ParamMeta } from "@/components/CellEditor"; +import { Dock } from "@/components/Dock"; +import { Header } from "@/components/Header"; +import { Inspector } from "@/components/Inspector"; +import { LoadingMask } from "@/components/LoadingMask"; +import { ModuleView, type ModuleState } from "@/components/ModuleView"; +import { NotebookList } from "@/components/NotebookList"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { fetchPreview, getWorkspace, openNotebook, postViewport, type ModuleRow, type NotebookRow } from "@/lib/api"; +import { parsePreview } from "@/lib/preview"; +import { EnvelopeSocket, type Envelope } from "@/lib/envelope"; +import { useRunLog } from "@/lib/runlog"; +import { useTheme } from "@/lib/theme"; +import { selectionPayload, variableNames, type SelectionItem } from "@/lib/selection"; +import { cn } from "@/lib/utils"; +import { Viewport, type PickResult, type SelectionMode } from "@/scene/Viewport"; +import type { SceneView } from "@/scene/scene-view"; + +const MODES: { mode: SelectionMode; label: string }[] = [ + { mode: "component", label: "comp" }, + { mode: "solid", label: "solid" }, + { mode: "face", label: "face" }, + { mode: "edge", label: "edge" }, + { mode: "vertex", label: "vert" }, +]; + +/** Editor application: left = notebook (cells, workspace), middle = the live + * model, right = the selection. The model is the in-memory scadpkg the kernel + * publishes whenever the cells settle; picking in the viewport writes the + * selection into the notebook (see `selection.set`). */ +export function App() { + const viewRef = useRef(null); + // Whether a model of the open notebook is on screen (it may be the last run's, + // restored while the kernel boots): then nothing is being waited for. + const hasModel = useRef(false); + const socketRef = useRef(null); + + const [notebooks, setNotebooks] = useState([]); + const [modules, setModules] = useState([]); + const [moduleView, setModuleView] = useState(null); + const [leftTab, setLeftTab] = useState("notebook"); + const [notebook, setNotebook] = useState(null); + // The notebook the server says is open (workspace-relative): what the list + // highlights. It changes the moment another is asked for, not when its + // kernel is up, and a late answer of the workspace list cannot undo it. + const [currentPath, setCurrentPath] = useState(null); + const [kernelAlive, setKernelAlive] = useState(false); + const [marimoUrl, setMarimoUrl] = useState(null); + const [status, setStatus] = useState("Connecting…"); + + const [params, setParams] = useState([]); + const [cells, setCells] = useState([]); + const [features, setFeatures] = useState([]); + const [picked, setPicked] = useState[]>([]); + const [selectionMode, setSelectionMode] = useState("solid"); + const [sceneVersion, setSceneVersion] = useState(0); + // The workspace is veiled while a model is being made: the kernel starting, a + // notebook opening, a module reloading, every cell running. It ends when the + // model arrives (`package`) or the work fails. Interactive rebuilds (a + // slider, one cell) are not veiled. + const [loading, setLoading] = useState("connecting"); + + const kind = notebooks.find((row) => row.path === currentPath)?.kind ?? null; + const theme = useTheme(); + const log = useRunLog(); + const running = cells.some((cell) => cell.status === "running" || cell.status === "queued"); + const handleStop = useCallback(() => { + socketRef.current?.send({ type: "interrupt.request" }); + setLoading(null); + log.push("info", "stop requested"); + log.resetBatch(); + }, [log.push, log.resetBatch]); + + const selection: SelectionItem[] = useMemo(() => { + const names = variableNames(picked); + return picked.map((item, index) => ({ ...item, variable: names[index] })); + }, [picked]); + + // -- package → viewport ------------------------------------------------------- + + // Camera framing is disruptive during slider drags; frame once per notebook. + const framedNotebook = useRef(null); + + const loadPreviewBytes = useCallback(async (buffer: ArrayBuffer, name: string | null) => { + const view = viewRef.current; + if (!view) return; + try { + const preview = parsePreview(buffer); + view.loadPreview(preview); + setStatus(`${preview.nodes.length} ${preview.nodes.length === 1 ? "part" : "parts"} · ${preview.parts.size} distinct`); + if (framedNotebook.current !== name) { + view.frame(); + framedNotebook.current = name; + } + setSceneVersion((version) => version + 1); + log.push("ok", `model shown: ${preview.nodes.length} ${preview.nodes.length === 1 ? "part" : "parts"}`); + } catch (error) { + log.push("error", error instanceof Error ? error.message : "unable to open the preview"); + } + }, [log.push]); + + // -- socket + lifecycle -------------------------------------------------------- + + useEffect(() => { + const socket = new EnvelopeSocket(`ws://${window.location.host}/ws`); + socketRef.current = socket; + + const refreshWorkspace = () => + void getWorkspace().then((data) => { + setNotebooks(data.notebooks); + setModules(data.modules ?? []); + setCurrentPath((path) => path ?? data.notebooks.find((row) => row.current)?.path ?? null); + }); + + socket.on("hello", (envelope) => { + setNotebook((envelope["notebook"] as string) ?? null); + setCurrentPath((envelope["path"] as string) ?? null); + setKernelAlive(Boolean(envelope["kernel"])); + // No notebook: nothing to wait for. Else the model is on its way. + if (hasModel.current) return; + const stage = envelope["notebook"] ? (envelope["kernel"] ? "building the model" : "starting the kernel") : null; + setLoading(stage); + if (stage) setStatus(`${stage[0].toUpperCase()}${stage.slice(1)}…`); + setMarimoUrl((envelope["marimo_url"] as string) ?? null); + refreshWorkspace(); + }); + + // Another notebook is being opened: drop the old model and its state now, + // the kernel takes a few seconds to boot and publish the new one. + socket.on("opening", (envelope) => { + hasModel.current = false; + viewRef.current?.clearScene(); + framedNotebook.current = null; + setCells([]); + setParams([]); + setFeatures([]); + setPicked([]); + setNotebook(String(envelope["notebook"] ?? "")); + setCurrentPath((envelope["path"] as string) ?? null); + setStatus(`opening ${envelope["notebook"]}…`); + setLoading(`opening ${envelope["notebook"]}`); + log.resetBatch(); + log.push("info", `opening ${envelope["notebook"]}`); + setSceneVersion((version) => version + 1); + refreshWorkspace(); + }); + + socket.on("params.schema", (envelope) => { + const vars = (envelope["vars"] as ParamMeta[] | undefined) ?? []; + setParams(vars.map((param) => ({ ...param }))); + }); + + socket.on("params.values", (envelope) => { + const values = envelope["values"] as Record; + setParams((previous) => + previous.map((param) => (param.id in values ? { ...param, value: values[param.id] } : param)), + ); + }); + + socket.on("module", (envelope) => { + if (!envelope["path"]) { + setModuleView(null); + setLeftTab((tab) => (tab === "module" ? "notebook" : tab)); + return; + } + const state = envelope as unknown as ModuleState; + setModuleView(state); + if (state.origin === "open") setLeftTab("module"); + }); + + // The live state of a cell's run: its button is run or stop by it. + socket.on("cell.status", (envelope) => { + const id = String(envelope["id"]); + const status = String(envelope["status"]); + setCells((previous) => previous.map((cell) => (cell.id === id ? { ...cell, status } : cell))); + log.cellStatus(id, status, (envelope["title"] as string | null) ?? null); + }); + + socket.on("notebook.cells", (envelope) => { + const next = (envelope["cells"] as NotebookCell[]) ?? []; + setCells(next); + log.knowCells(next); + }); + socket.on("notebook.features", (envelope) => setFeatures((envelope["features"] as NotebookFeature[]) ?? [])); + + socket.on("preview", (envelope) => { + setLoading(null); + const shown = hasModel.current; + hasModel.current = true; + // The kernel published the model that is already on screen: nothing to + // reload. (A page that has none yet, a refresh replaying this envelope, needs it.) + if (envelope["unchanged"] && shown) { + log.modelShown(); + return; + } + void fetchPreview().then((buffer) => loadPreviewBytes(buffer, (envelope["name"] as string) ?? null)); + log.modelShown(); + }); + + // The details of a picked entity (a preview carries none), asked for when it was picked. + socket.on("entity.info", (envelope) => { + const entityId = String(envelope["entity_id"]); + const entity = envelope["entity"] as { properties?: Record; geometry?: Record; tags?: string[] } | null; + setPicked((previous) => + previous.map((item) => + item.entityId === entityId + ? { ...item, pending: false, properties: entity?.properties ?? item.properties ?? {}, geometry: entity?.geometry ?? item.geometry ?? {}, tags: entity?.tags ?? item.tags } + : item, + ), + ); + }); + + // Messages of the server. Errors go to the log (the dock opens on one), not the header. + socket.on("status", (envelope) => { + if (typeof envelope["error"] === "string") { + if (envelope["error"]) { + log.push("error", envelope["error"]); + setLoading(null); + } + } else if (typeof envelope["info"] === "string") { + setStatus(envelope["info"]); + log.push("info", envelope["info"]); + } else if (typeof envelope["working"] === "string") { + setStatus(envelope["working"]); + log.push("run", envelope["working"]); + setLoading(envelope["working"].replace(/…$/, "")); + } else if (typeof envelope["exported"] === "string") { + setStatus(`exported ${envelope["exported"]}`); + log.push("ok", `exported ${envelope["exported"]}`); + } else if (typeof envelope["created"] === "string") { + setStatus(`created ${envelope["created"]}`); + log.push("ok", `created ${envelope["created"]}`); + } + }); + + socket.connect(); + return () => socket.close(); + }, [loadPreviewBytes, log.push, log.cellStatus, log.knowCells, log.resetBatch, log.modelShown]); + + // -- selection ------------------------------------------------------------------- + + const handlePick = useCallback((result: PickResult) => { + const view = viewRef.current; + const node = view?.nodes.find((item) => item.node_id === result.nodeId); + const entity = result.entity; + const item: Omit = { + key: `${result.nodeId}::${result.entityId ?? "component"}`, + kind: result.mode === "component" ? "component" : ((entity?.kind ?? result.mode) as SelectionItem["kind"]), + nodeId: result.nodeId, + entityId: result.entityId, + definitionId: node?.definition_id ?? null, + properties: entity?.properties ?? null, + geometry: entity?.geometry ?? null, + tags: entity?.tags ?? null, + }; + // A preview carries no entity details: ask the kernel for them. + const needsDetails = Boolean(view?.isPreview) && Boolean(item.entityId) && Object.keys(item.properties ?? {}).length === 0; + if (needsDetails && item.entityId) { + item.pending = true; + socketRef.current?.send({ type: "entity.info", entity_id: item.entityId, node_id: item.nodeId }); + } + setPicked((previous) => { + if (!result.additive) return [item]; + return previous.some((other) => other.key === item.key) + ? previous.filter((other) => other.key !== item.key) + : [...previous, item]; + }); + }, []); + + const handleClear = useCallback(() => setPicked((previous) => (previous.length ? [] : previous)), []); + + // The notebook follows the selection: the server writes it as ql code into + // the selection cell, and the viewport image is saved for coding agents. + const firstSelection = useRef(true); + useEffect(() => { + if (firstSelection.current) { + firstSelection.current = false; + return; + } + // The notebook gets the selection when every picked entity is described. + const waiting = selection.some((item) => item.pending); + const send = window.setTimeout(() => { + if (!waiting) socketRef.current?.send({ type: "selection.set", items: selectionPayload(selection) }); + }, 40); + const snapshot = window.setTimeout(() => { + const png = viewRef.current?.snapshotPng(); + if (png) void postViewport(png).catch(() => undefined); + }, 600); + return () => { + window.clearTimeout(send); + window.clearTimeout(snapshot); + }; + }, [selection]); + + // Never veil for ever: whatever failed to say so ends it after three minutes. + useEffect(() => { + if (loading === null) return; + const timer = window.setTimeout(() => setLoading(null), 180_000); + return () => window.clearTimeout(timer); + }, [loading]); + + // -- actions ----------------------------------------------------------------------- + + const handleOpen = (path: string) => { + setStatus(`opening ${path}…`); + openNotebook(path).catch((error) => { + log.push("error", `open failed: ${error.message}`); + setLoading(null); + }); + }; + + return ( +
+
{ + setLoading("running all cells"); + socketRef.current?.send({ type: "rerun.request" }); + }} + /> +
+ + + + + notebook + workspace + {moduleView && {moduleView.name}} + + + + + + socketRef.current?.send({ type: "module.open", path })} + /> + + {moduleView && ( + + socketRef.current?.send({ type: "module.close" })} + /> + + )} + + + + +
+
+ {MODES.map(({ mode, label }) => ( + + ))} + + ctrl+click: add + +
+
+ +
+
+
+ + + + +
+ +
+ +
+ ); +} + +export type { Envelope }; diff --git a/editor-web/src/assets/hero.png b/editor-web/src/assets/hero.png new file mode 100644 index 00000000..02251f4b Binary files /dev/null and b/editor-web/src/assets/hero.png differ diff --git a/editor-web/src/assets/vite.svg b/editor-web/src/assets/vite.svg new file mode 100644 index 00000000..5101b674 --- /dev/null +++ b/editor-web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/editor-web/src/components/CellEditor.tsx b/editor-web/src/components/CellEditor.tsx new file mode 100644 index 00000000..f1581cbd --- /dev/null +++ b/editor-web/src/components/CellEditor.tsx @@ -0,0 +1,491 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import CodeMirror from "@uiw/react-codemirror"; +import { python } from "@codemirror/lang-python"; +import { EditorView } from "@codemirror/view"; +import { + ChevronRight, + ChevronsDownUp, + ChevronsUp, + ChevronsUpDown, + Check, + Copy, + Loader2, + Play, + Plus, + Square, + Trash2, +} from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ParamControl, type ParamMeta, type ParamValue } from "@/components/ParamControl"; +import { cadTheme } from "@/lib/cm-theme"; +import { cn } from "@/lib/utils"; + +export type NotebookCell = { + id: string; + name: string; + code: string; + status?: string | null; + errors?: string[] | null; + /** Not run: a cell it depends on (or the setup) failed. */ + blocked?: boolean; + output?: string | null; + console?: string[]; + /** Slug of the cell's `# ---- feature: ... ----` header, if it has one. */ + feature?: string | null; + /** The step's name: feature slug, params header, or the first variable it defines. */ + title?: string; + /** An @app.function cell: edited in the file, not here. */ + readonly?: boolean; + /** Where the cell is in the notebook file, for pasting to a coding agent: + * `:L-L (cell 03 \`name\`)`. */ + ref?: string; +}; +/** One feature header; `cell` is the id of the cell it opens. */ +export type NotebookFeature = { cell: string; label: string; kind?: string }; +export type { ParamMeta, ParamValue }; + +type SocketLike = { send: (message: Record) => void } | null; + +const AUTOSAVE_MS = 700; +const EXTENSIONS = [python(), EditorView.lineWrapping]; + +function loadCollapsed(notebook: string | null): Set { + try { + const raw = window.localStorage.getItem(`sca-collapsed:${notebook}`); + return new Set(raw ? (JSON.parse(raw) as string[]) : []); + } catch { + return new Set(); + } +} + +/** The notebook: one block per cell. A block's header is its handle — the + * step's name — and folds the cell away; the buttons run it, run everything + * above it, or delete it. Every edit is autosaved to the notebook file, so a + * coding agent reading the file sees what is on screen. */ +export function CellEditor({ + cells, + features, + params, + socket, + notebook, + onInterrupt, +}: { + cells: NotebookCell[]; + features: NotebookFeature[]; + params: ParamMeta[]; + socket: SocketLike; + notebook: string | null; + onInterrupt: () => void; +}) { + const [drafts, setDrafts] = useState>(new Map()); + const [running, setRunning] = useState(null); + const [collapsed, setCollapsed] = useState>(() => loadCollapsed(notebook)); + // Two-stage delete: arm on the first click (button turns red), confirm on + // the second; any click elsewhere disarms. + const [armed, setArmed] = useState(null); + const saveTimers = useRef(new Map()); + + const featureByCell = useMemo(() => new Map(features.map((feature) => [feature.cell, feature])), [features]); + + useEffect(() => { + setCollapsed(loadCollapsed(notebook)); + setDrafts(new Map()); + }, [notebook]); + + const remember = (next: Set) => { + setCollapsed(next); + try { + window.localStorage.setItem(`sca-collapsed:${notebook}`, JSON.stringify([...next])); + } catch { + /* private mode: folding just does not persist */ + } + }; + const toggle = (id: string) => { + const next = new Set(collapsed); + if (next.has(id)) next.delete(id); + else next.add(id); + remember(next); + }; + + const cancelSave = (id: string) => { + const timer = saveTimers.current.get(id); + if (timer !== undefined) window.clearTimeout(timer); + saveTimers.current.delete(id); + }; + + const run = (cell: NotebookCell) => { + cancelSave(cell.id); + const code = drafts.get(cell.id) ?? cell.code; + setRunning(cell.id); + socket?.send({ type: "code.edit", cell: cell.id, code }); + window.setTimeout(() => setRunning((current) => (current === cell.id ? null : current)), 2500); + }; + + const runAbove = (cell: NotebookCell) => { + setRunning(cell.id); + socket?.send({ type: "rerun.request", cell: cell.id, above: true }); + window.setTimeout(() => setRunning((current) => (current === cell.id ? null : current)), 2500); + }; + + const remove = (cell: NotebookCell) => { + setArmed(null); + cancelSave(cell.id); + socket?.send({ type: "rollback.request", cell: cell.id }); + }; + + const insert = (after: string | null) => socket?.send({ type: "insert.request", after }); + + const edit = (cell: NotebookCell, value: string) => { + setDrafts((previous) => new Map(previous).set(cell.id, value)); + if (cell.readonly) return; + cancelSave(cell.id); + saveTimers.current.set( + cell.id, + window.setTimeout(() => { + saveTimers.current.delete(cell.id); + socket?.send({ type: "cell.save", cell: cell.id, code: value }); + }, AUTOSAVE_MS), + ); + }; + + // Forward slider bursts; the server coalesces (latest-wins rebuild). + const pendingParams = useRef(new Map()); + const paramTimer = useRef(null); + const forwardParam = (id: string, value: ParamValue) => { + pendingParams.current.set(id, value); + if (paramTimer.current !== null) return; + paramTimer.current = window.setTimeout(() => { + paramTimer.current = null; + socket?.send({ type: "param.set", values: Object.fromEntries(pendingParams.current) }); + pendingParams.current.clear(); + }, 60); + }; + + // Disarm delete when clicking anywhere outside the armed button. + useEffect(() => { + if (!armed) return; + const onPointerDown = (event: PointerEvent) => { + const target = event.target as HTMLElement | null; + if (!target?.closest(`[data-delete-armed="${CSS.escape(armed)}"]`)) setArmed(null); + }; + window.addEventListener("pointerdown", onPointerDown, true); + return () => window.removeEventListener("pointerdown", onPointerDown, true); + }, [armed]); + + if (cells.length === 0) { + return

Waiting for the kernel to publish cells…

; + } + + const allCollapsed = cells.every((cell) => collapsed.has(cell.id)); + return ( +
+
+ {cells.length} cells + +
+ +
+ + {cells.map((cell, index) => ( +
+ param.cell === cell.id)} + draft={drafts.get(cell.id)} + folded={collapsed.has(cell.id)} + pending={running === cell.id} + live={cell.status === "running" || cell.status === "queued"} + armed={armed === cell.id} + socket={socket} + forwardParam={forwardParam} + onToggle={() => toggle(cell.id)} + onRun={() => run(cell)} + onInterrupt={onInterrupt} + onRunAbove={() => runAbove(cell)} + onDelete={() => (armed === cell.id ? remove(cell) : setArmed(cell.id))} + onEdit={(value) => edit(cell, value)} + /> + +
+ ))} +
+
+
+ ); +} + +/** Hover divider between cells with a "+" insertion button. */ +function CellGap({ after, onInsert }: { after: string | null; onInsert: (after: string | null) => void }) { + return ( +
+
+ +
+ ); +} + +function IconButton({ + title, + onClick, + disabled, + className, + children, + ...rest +}: { + title: string; + onClick: () => void; + disabled?: boolean; + className?: string; + children: React.ReactNode; +} & Record) { + return ( + + ); +} + +/** One cell: header handle + (when open) code, errors, output, console. */ +function CellBlock({ + cell, + index, + role, + params, + draft, + folded, + pending, + live, + armed, + socket, + forwardParam, + onToggle, + onRun, + onInterrupt, + onRunAbove, + onDelete, + onEdit, +}: { + cell: NotebookCell; + index: number; + role?: string; + params: ParamMeta[]; + draft: string | undefined; + folded: boolean; + /** Run was just asked for; the kernel has not said anything yet. */ + pending: boolean; + /** The kernel is running or has queued the cell. */ + live: boolean; + armed: boolean; + socket: SocketLike; + forwardParam: (id: string, value: ParamValue) => void; + onToggle: () => void; + onRun: () => void; + onInterrupt: () => void; + onRunAbove: () => void; + onDelete: () => void; + onEdit: (value: string) => void; +}) { + const failed = cell.status === "exception" || cell.status === "error" || (cell.errors?.length ?? 0) > 0; + const stale = cell.status === "stale"; + const dirty = draft !== undefined && draft !== cell.code; + const isSelection = cell.title === "selection"; + const title = cell.title || cell.name; + const hasOutput = Boolean(cell.output) || params.length > 0; + const [copied, setCopied] = useState(false); + // Click: a reference an agent can find in the file. Alt/Shift+click: the + // reference and the cell's code. + const copyReference = (withCode: boolean) => { + const reference = cell.ref ?? title; + const text = withCode ? `${reference}\n\`\`\`python\n${draft ?? cell.code}\n\`\`\`` : reference; + void navigator.clipboard.writeText(text).then(() => { + setCopied(true); + window.setTimeout(() => setCopied(false), 1200); + }); + }; + return ( +
+
+ + {dirty && } + {cell.status === "running" && running} + {cell.status === "queued" && queued} + {failed && failed} + {cell.blocked && !failed && blocked} + {stale && !failed && stale} + {cell.readonly && function} + + + + + {/* One button, two states: run, and while the kernel is running the cell, stop. */} + + {live ? ( + + ) : pending ? ( + + ) : ( + + )} + + + + +
+ {live &&
} + {!folded && ( +
+ { + if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { + event.preventDefault(); + onRun(); + } + }} + /> + {(cell.errors?.length ?? 0) > 0 && ( +
+ {cell.errors?.map((message, errorIndex) => ( +
+                  {message}
+                
+ ))} +
+ )} + {hasOutput && ( +
+ {params.length > 0 && ( +
+
+ {params.map((param) => ( + + ))} +
+ {params.some((param) => param.modified) && ( +
+ + +
+ )} +
+ )} + {cell.output && ( +
{cell.output}
+ )} +
+ )} + {(cell.console?.length ?? 0) > 0 && ( +
+ {cell.console?.map((line, lineIndex) => ( +
+                  {line}
+                
+ ))} +
+ )} +
+ )} +
+ ); +} diff --git a/editor-web/src/components/Dock.tsx b/editor-web/src/components/Dock.tsx new file mode 100644 index 00000000..2e23c21d --- /dev/null +++ b/editor-web/src/components/Dock.tsx @@ -0,0 +1,124 @@ +import { ChevronDown, ChevronUp, Trash2 } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { cn } from "@/lib/utils"; +import type { LogEntry, LogLevel, RunBatch } from "@/lib/runlog"; + +const INK: Record = { + info: "text-muted-foreground", + run: "text-focus", + ok: "text-pass", + error: "text-fail", +}; + +const TAG: Record = { info: " ", run: "RUN", ok: "OK ", error: "ERR" }; + +const clock = (at: number) => new Date(at).toLocaleTimeString([], { hour12: false }); + +/** The bottom dock: while cells are being re-evaluated, which ones and how far + * along; the log of what happened and what failed. Opens itself on an error. */ +export function Dock({ + entries, + batch, + status, + unseenErrors, + onClear, + onSeen, +}: { + entries: LogEntry[]; + batch: RunBatch | null; + status: string; + unseenErrors: number; + onClear: () => void; + onSeen: () => void; +}) { + const [open, setOpen] = useState(false); + const [now, setNow] = useState(() => Date.now()); + const tick = batch ? Date.now() : now; + const list = useRef(null); + + // An error opens the log; the person closes it again. + useEffect(() => { + if (unseenErrors > 0) setOpen(true); + }, [unseenErrors]); + + useEffect(() => { + if (open) onSeen(); + }, [open, entries.length, onSeen]); + + useEffect(() => { + if (open && list.current) list.current.scrollTop = list.current.scrollHeight; + }, [open, entries.length]); + + useEffect(() => { + if (!batch) return; + const timer = window.setInterval(() => setNow(Date.now()), 200); + return () => window.clearInterval(timer); + }, [batch]); + + const last = entries[entries.length - 1]; + const errors = entries.filter((entry) => entry.level === "error").length; + const percent = batch && batch.total > 0 ? Math.round((batch.done / batch.total) * 100) : 0; + + return ( +
+ {open && ( +
+ {entries.length === 0 ? ( +

Nothing yet. Runs, reloads and errors are listed here.

+ ) : ( + entries.map((entry) => ( +

+ {clock(entry.at)} + {TAG[entry.level]} + {entry.text} +

+ )) + )} +
+ )} +
+ + {errors > 0 && ( + + {errors} {errors === 1 ? "error" : "errors"} + + )} + +
+
+ ); +} diff --git a/editor-web/src/components/Header.tsx b/editor-web/src/components/Header.tsx new file mode 100644 index 00000000..37a3936f --- /dev/null +++ b/editor-web/src/components/Header.tsx @@ -0,0 +1,87 @@ +import { ExternalLink, Monitor, Moon, Play, Square, Sun } from "lucide-react"; + +import type { ThemePreference } from "@/lib/theme"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; + +/** Top bar: which notebook is open, whether its kernel is alive, and the + * model's last message. Everything else lives on the cells. */ +export function Header({ + notebook, + kind, + kernelAlive, + status, + onRunAll, + running, + onStop, + marimoUrl, + themePreference, + onTheme, +}: { + notebook: string | null; + kind: string | null; + kernelAlive: boolean; + status: string; + onRunAll: () => void; + /** The kernel is running cells: the button stops them instead. */ + running: boolean; + onStop: () => void; + marimoUrl: string | null; + themePreference: ThemePreference; + onTheme: () => void; +}) { + const failing = status.startsWith("error"); + return ( +
+ SimpleCAD + + {notebook ? ( + + {notebook} + {kind && kind !== "helper" && {kind}} + + ) : ( + no notebook + )} + + + {status} + + {running ? ( + + ) : ( + + )} + {marimoUrl && ( + + )} + +
+ ); +} + +const NEXT_THEME: Record = { system: "light", light: "dark", dark: "system" }; diff --git a/editor-web/src/components/Inspector.tsx b/editor-web/src/components/Inspector.tsx new file mode 100644 index 00000000..c12f8c88 --- /dev/null +++ b/editor-web/src/components/Inspector.tsx @@ -0,0 +1,71 @@ +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { JsonTree } from "@/components/JsonTree"; +import type { SelectionItem } from "@/lib/selection"; + +/** Right-hand inspector: what is selected in the viewport, each item with + * the variable the selection cell assigns it to and its data as a tree. */ +export function Inspector({ selection }: { selection: SelectionItem[] }) { + return ( +
+
+ selection + {selection.length > 0 && {selection.length}} +
+ + {selection.length === 0 ? ( +

+ Click a solid, face, edge or vertex in the viewport. Ctrl/Cmd+click adds to the selection; the selection + is written to the notebook as selected_item. +

+ ) : ( +
    + {selection.map((item) => ( +
  • +
    + {item.kind} + + {item.variable} + +
    +
    + + + {item.definitionId && } +
    + {item.pending &&

    loading details…

    } + {(item.properties || item.geometry) && ( +
    + {item.properties && } + {item.geometry && } + {item.tags && item.tags.length > 0 && } +
    + )} +
  • + ))} +
+ )} +
+
+ ); +} + +function Line({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +function Group({ title, value }: { title: string; value: unknown }) { + return ( +
+

{title}

+ +
+ ); +} diff --git a/editor-web/src/components/JsonTree.tsx b/editor-web/src/components/JsonTree.tsx new file mode 100644 index 00000000..b0551b94 --- /dev/null +++ b/editor-web/src/components/JsonTree.tsx @@ -0,0 +1,103 @@ +import { useState } from "react"; +import { ChevronRight } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +/** A generic, collapsible view of JSON data: objects and arrays open to + * `openDepth` levels, short number arrays stay on one line, numbers are + * rounded for reading. Used wherever the editor shows raw entity data. */ +export function JsonTree({ + value, + openDepth = 2, + className, +}: { + value: unknown; + openDepth?: number; + className?: string; +}) { + return ( +
+ +
+ ); +} + +type Entry = [string, unknown]; + +function isContainer(value: unknown): value is Record | unknown[] { + return typeof value === "object" && value !== null; +} + +function entriesOf(value: Record | unknown[]): Entry[] { + return Array.isArray(value) + ? value.map((item, index) => [String(index), item]) + : Object.entries(value); +} + +/** A flat array of a few numbers reads best inline: [1.2, 0, -3]. */ +function inlineNumbers(value: unknown): string | null { + if (!Array.isArray(value) || value.length === 0 || value.length > 6) return null; + if (!value.every((item) => typeof item === "number")) return null; + return `[${value.map((item) => formatNumber(item as number)).join(", ")}]`; +} + +export function formatNumber(value: number): string { + if (!Number.isFinite(value)) return String(value); + if (Number.isInteger(value)) return String(value); + const text = value.toFixed(4).replace(/\.?0+$/, ""); + return text === "-0" ? "0" : text; +} + +function Scalar({ value }: { value: unknown }) { + if (typeof value === "number") return {formatNumber(value)}; + if (typeof value === "boolean") return {String(value)}; + if (value === null || value === undefined) return null; + return {String(value)}; +} + +function summary(value: Record | unknown[]): string { + return Array.isArray(value) ? `[${value.length}]` : `{${Object.keys(value).length}}`; +} + +function Node({ value, depth, openDepth }: { value: unknown; depth: number; openDepth: number }) { + if (!isContainer(value)) return ; + const inline = inlineNumbers(value); + if (inline) return {inline}; + const entries = entriesOf(value); + if (entries.length === 0) return {Array.isArray(value) ? "[]" : "{}"}; + return ( +
    0 && "ml-[5px] border-l border-border pl-2")}> + {entries.map(([key, child]) => ( + + ))} +
+ ); +} + +function Row({ name, value, depth, openDepth }: { name: string; value: unknown; depth: number; openDepth: number }) { + const container = isContainer(value) && inlineNumbers(value) === null; + const empty = container && entriesOf(value as Record | unknown[]).length === 0; + const [open, setOpen] = useState(depth + 1 < openDepth); + if (!container || empty) { + return ( +
  • + {name} + +
  • + ); + } + return ( +
  • + + {open && } +
  • + ); +} diff --git a/editor-web/src/components/LoadingMask.tsx b/editor-web/src/components/LoadingMask.tsx new file mode 100644 index 00000000..f6abf553 --- /dev/null +++ b/editor-web/src/components/LoadingMask.tsx @@ -0,0 +1,187 @@ +import { useEffect, useRef, useState } from "react"; + +import { cn } from "@/lib/utils"; + +/** Cells of the 3x3 spinner that form its ring, clockwise from the top left. */ +const RING = [0, 1, 2, 5, 8, 7, 6, 3]; + +const STEP = 8; // dot pitch, CSS px +const DOT = 2; // dot size, CSS px +/** Dot brightness in four steps (a 2-bit look), kept low: the veil should sit + * behind the window, not compete with it. The brightest step is the crest. */ +const LEVELS = [0.03, 0.075, 0.13, 0.22]; +/** One long swell: wavelength 2π/WAVE_NUMBER ≈ 780 px, a period of ≈ 7 s. */ +const WAVE_NUMBER = 0.008; +const WAVE_SPEED = 0.0009; + +/** A gray veil over the whole workspace while the model is being made: a + * dot-matrix with a ripple that spreads from the middle, and a small retro + * window saying what is going on and for how long. It appears only after + * `DELAY_MS`, so quick operations do not flash it. */ +export function LoadingMask({ active, message, theme }: { active: boolean; message: string; theme: string }) { + const DELAY_MS = 250; + const [visible, setVisible] = useState(false); + const [elapsed, setElapsed] = useState(0); + + useEffect(() => { + if (!active) { + setVisible(false); + setElapsed(0); + return; + } + const started = Date.now(); + const show = window.setTimeout(() => setVisible(true), DELAY_MS); + const tick = window.setInterval(() => setElapsed(Math.floor((Date.now() - started) / 1000)), 500); + return () => { + window.clearTimeout(show); + window.clearInterval(tick); + }; + }, [active]); + + if (!visible) return null; + const minutes = String(Math.floor(elapsed / 60)).padStart(2, "0"); + const seconds = String(elapsed % 60).padStart(2, "0"); + return ( +
    + +
    +
    + loading + + {minutes}:{seconds} + +
    +
    + +
    +

    + {message} +

    +

    + please wait_ +

    +
    +
    + +
    +
    + ); +} + +/** Eight squares chasing each other round a ring. */ +function Spinner() { + return ( +
    + {Array.from({ length: 9 }, (_, cell) => { + const order = RING.indexOf(cell); + return ( + = 0 && "sca-chase")} + style={order >= 0 ? { animationDelay: `${order * 0.1}s` } : undefined} + /> + ); + })} +
    + ); +} + +/** A strip of blocks filling and emptying: the load has no known length. */ +function Progress() { + return ( +
    + {Array.from({ length: 20 }, (_, index) => ( + + ))} +
    + ); +} + +/** A CSS color variable of the page (a #rrggbb token) as "r,g,b", for drawing on a canvas. */ +function cssRgb(name: string, fallback: string): string { + const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + const match = /^#([0-9a-f]{6})$/i.exec(value); + if (!match) return fallback; + const n = parseInt(match[1], 16); + return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`; +} + +/** The dot-matrix: square dots whose brightness follows a ring-shaped wave + * travelling out from the middle, quantised to four levels. */ +function Ripple({ theme }: { theme: string }) { + const canvasRef = useRef(null); + // The dots take the theme's ink: the crest is the focus color, the rest the foreground. + const inks = useRef({ crest: "255,154,46", base: "255,255,255" }); + useEffect(() => { + inks.current = { crest: cssRgb("--focus", "255,154,46"), base: cssRgb("--foreground", "255,255,255") }; + }, [theme]); + + useEffect(() => { + const canvas = canvasRef.current; + const context = canvas?.getContext("2d"); + if (!canvas || !context) return; + const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + let width = 0; + let height = 0; + let frame = 0; + let last = 0; + + const resize = () => { + const ratio = Math.min(window.devicePixelRatio || 1, 2); + width = canvas.clientWidth; + height = canvas.clientHeight; + canvas.width = Math.max(1, Math.round(width * ratio)); + canvas.height = Math.max(1, Math.round(height * ratio)); + context.setTransform(ratio, 0, 0, ratio, 0, 0); + }; + + const draw = (time: number) => { + context.clearRect(0, 0, width, height); + const cx = width / 2; + const cy = height / 2; + const reach = Math.hypot(cx, cy); + const paths = LEVELS.map(() => new Path2D()); + for (let y = STEP / 2; y < height; y += STEP) { + for (let x = STEP / 2; x < width; x += STEP) { + const distance = Math.hypot(x - cx, y - cy); + // a long swell spreading outwards, fading gently with distance + const wave = (Math.sin(distance * WAVE_NUMBER - time * WAVE_SPEED) + 1) / 2; + const strength = wave * (1 - 0.4 * Math.min(distance / reach, 1)); + const level = Math.min(LEVELS.length - 1, Math.floor(strength * LEVELS.length)); + paths[level].rect(x - DOT / 2, y - DOT / 2, DOT, DOT); + } + } + paths.forEach((path, level) => { + context.fillStyle = `rgba(${level === LEVELS.length - 1 ? inks.current.crest : inks.current.base},${LEVELS[level]})`; + context.fill(path); + }); + }; + + const loop = (time: number) => { + if (time - last > 42) { + last = time; + draw(time); + } + frame = window.requestAnimationFrame(loop); + }; + + resize(); + const observer = new ResizeObserver(() => { + resize(); + if (still) draw(0); + }); + observer.observe(canvas); + if (still) draw(0); + else frame = window.requestAnimationFrame(loop); + return () => { + window.cancelAnimationFrame(frame); + observer.disconnect(); + }; + }, []); + + return ; +} diff --git a/editor-web/src/components/ModuleView.tsx b/editor-web/src/components/ModuleView.tsx new file mode 100644 index 00000000..4dc7c04b --- /dev/null +++ b/editor-web/src/components/ModuleView.tsx @@ -0,0 +1,135 @@ +import { useEffect, useRef, useState } from "react"; +import CodeMirror from "@uiw/react-codemirror"; +import { python } from "@codemirror/lang-python"; +import { EditorView } from "@codemirror/view"; +import { X } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ParamControl, type ParamMeta, type ParamValue } from "@/components/ParamControl"; +import { cadTheme } from "@/lib/cm-theme"; + +/** A local module of the workspace (shared dimensions, helpers): what the + * server sends as the `module` envelope. */ +export type ModuleState = { + path: string; + name: string; + code: string; + used_by: string[]; + params: ParamMeta[]; + origin?: string; +}; + +const AUTOSAVE_MS = 700; +const EXTENSIONS = [python(), EditorView.lineWrapping]; + +/** A module open next to the notebooks: its dataclass fields as controls, its + * source below. Every edit is written to the file at once, and every notebook + * that imports it follows (the server reloads it and reruns what reads it). */ +export function ModuleView({ + module, + socket, + onClose, +}: { + module: ModuleState; + socket: { send: (message: Record) => void } | null; + onClose: () => void; +}) { + const [draft, setDraft] = useState(module.code); + const dirty = useRef(false); + const saveTimer = useRef(null); + const paramTimer = useRef(null); + const pending = useRef(new Map()); + + // A different module, an external edit or a parameter edit replaces the + // text; our own autosave echo must not clobber what is being typed. + useEffect(() => { + if (module.origin === "save" && dirty.current) return; + setDraft(module.code); + dirty.current = false; + }, [module.path, module.code, module.origin]); + + const edit = (value: string) => { + setDraft(value); + dirty.current = true; + if (saveTimer.current !== null) window.clearTimeout(saveTimer.current); + saveTimer.current = window.setTimeout(() => { + saveTimer.current = null; + dirty.current = false; + socket?.send({ type: "module.save", path: module.path, code: value }); + }, AUTOSAVE_MS); + }; + + const forward = (id: string, value: ParamValue) => { + pending.current.set(id, value); + if (paramTimer.current !== null) return; + paramTimer.current = window.setTimeout(() => { + paramTimer.current = null; + socket?.send({ type: "module.param", path: module.path, values: Object.fromEntries(pending.current) }); + pending.current.clear(); + }, 120); + }; + + const groups = new Map(); + for (const param of module.params) { + const list = groups.get(param.group) ?? []; + list.push(param); + groups.set(param.group, list); + } + + return ( +
    +
    + + {module.path} + + {module.used_by.length > 0 ? ( + + used by {module.used_by.map((path) => path.replace(/\.py$/, "")).join(", ")} + + ) : ( + unused + )} + +
    + +
    + {[...groups].map(([group, params]) => ( +
    +
    + {group} + {params.length} fields +
    +
    + {params.map((param) => ( + + ))} +
    +
    + ))} +
    +
    + source + autosaved +
    + +
    +
    +
    +
    + ); +} diff --git a/editor-web/src/components/NotebookList.tsx b/editor-web/src/components/NotebookList.tsx new file mode 100644 index 00000000..05510151 --- /dev/null +++ b/editor-web/src/components/NotebookList.tsx @@ -0,0 +1,84 @@ +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import type { ModuleRow, NotebookRow } from "@/lib/api"; + +/** The workspace: its SimpleCAD notebooks (assemblies show what they use) and + * the local modules they import. Editing a module changes every notebook + * that imports it. */ +export function NotebookList({ + notebooks, + current, + modules, + openModule, + onOpen, + onOpenModule, +}: { + notebooks: NotebookRow[]; + /** Workspace-relative path of the open notebook. */ + current: string | null; + modules: ModuleRow[]; + openModule: string | null; + onOpen: (path: string) => void; + onOpenModule: (path: string) => void; +}) { + if (notebooks.length === 0 && modules.length === 0) { + return

    No SimpleCAD notebooks in this workspace.

    ; + } + return ( + +

    notebooks

    +
      + {notebooks.map((row) => ( +
    • + +
    • + ))} +
    + {modules.length > 0 && ( + <> +

    modules

    +
      + {modules.map((module) => ( +
    • + +
    • + ))} +
    + + )} +
    + ); +} diff --git a/editor-web/src/components/ParamControl.tsx b/editor-web/src/components/ParamControl.tsx new file mode 100644 index 00000000..74f1dd22 --- /dev/null +++ b/editor-web/src/components/ParamControl.tsx @@ -0,0 +1,150 @@ +import { useEffect, useRef, useState } from "react"; + +import { Slider } from "@/components/ui/slider"; +import { cn } from "@/lib/utils"; + +export type ParamValue = number | boolean; +/** A parameter the notebook declares (see `sca params`); `cell` is the id of + * the declaring cell. `modified`: the value differs from the source. */ +export type ParamMeta = { + id: string; + label: string; + cell: string; + group: string; + kind: "float" | "int" | "bool"; + control: "slider" | "number" | "toggle"; + min: number | null; + max: number | null; + step: number | null; + unit: string | null; + default: ParamValue; + value: ParamValue; + modified?: boolean; + comment?: string | null; + issues?: string[]; +}; + +/** One parameter: a slider, a number box or a toggle, by its `# ui:` comment. */ +export function ParamControl({ + param, + forwardParam, +}: { + param: ParamMeta; + forwardParam: (id: string, value: ParamValue) => void; +}) { + const label = param.label || param.id; + const title = [param.comment, ...(param.issues ?? [])].filter(Boolean).join("\n") || undefined; + const ink = param.modified ? "text-focus" : "text-name"; + if (param.control === "toggle") { + const on = Boolean(param.value); + return ( + + ); + } + const value = Number(param.value); + return ( +
    +
    + {label} + + {param.control === "number" ? ( + forwardParam(param.id, next)} + /> + ) : ( + {value} + )} + {param.unit && {param.unit}} + +
    + {param.control === "slider" && param.min !== null && param.max !== null && ( + forwardParam(param.id, next)} + /> + )} +
    + ); +} + +/** A number box that keeps what is being typed: the text is a local draft, + * committed when typing pauses, on Enter, or on blur. (A controlled box that + * sends every keystroke gets the server's echo of the first digit back while + * the second is being typed.) */ +function NumberField({ + value, + min, + max, + step, + onCommit, +}: { + value: number; + min: number | null; + max: number | null; + step: number | null; + onCommit: (value: number) => void; +}) { + const [text, setText] = useState(String(value)); + const editing = useRef(false); + const timer = useRef(null); + + useEffect(() => { + if (!editing.current) setText(String(value)); + }, [value]); + + const commit = (raw: string) => { + if (timer.current !== null) window.clearTimeout(timer.current); + timer.current = null; + const next = Number(raw); + if (raw.trim() !== "" && Number.isFinite(next) && next !== value) onCommit(next); + }; + + return ( + { + editing.current = true; + }} + onChange={(event) => { + setText(event.target.value); + if (timer.current !== null) window.clearTimeout(timer.current); + const raw = event.target.value; + timer.current = window.setTimeout(() => commit(raw), 700); + }} + onBlur={(event) => { + editing.current = false; + commit(event.target.value); + setText(String(value)); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + commit((event.target as HTMLInputElement).value); + (event.target as HTMLInputElement).blur(); + } + }} + /> + ); +} diff --git a/editor-web/src/components/ui/badge.tsx b/editor-web/src/components/ui/badge.tsx new file mode 100644 index 00000000..7bef5877 --- /dev/null +++ b/editor-web/src/components/ui/badge.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center border px-1.5 py-px text-[10px] font-semibold uppercase leading-4 tracking-[0.08em]", + { + variants: { + variant: { + default: "border-primary bg-primary text-primary-foreground", + secondary: "border-input text-muted-foreground", + outline: "border-input text-foreground", + focus: "border-focus text-focus", + pass: "border-pass text-pass", + fail: "border-fail text-fail", + name: "border-name text-name", + destructive: "border-fail bg-fail text-primary-foreground", + }, + }, + defaultVariants: { variant: "default" }, + }, +); + +function Badge({ + className, + variant, + ...props +}: React.ComponentProps<"span"> & VariantProps) { + return ; +} + +export { Badge, badgeVariants }; diff --git a/editor-web/src/components/ui/button.tsx b/editor-web/src/components/ui/button.tsx new file mode 100644 index 00000000..45fea79b --- /dev/null +++ b/editor-web/src/components/ui/button.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-1.5 whitespace-nowrap border-2 text-[11px] font-semibold uppercase tracking-[0.08em] transition-colors focus-visible:outline-none focus-visible:border-focus disabled:pointer-events-none disabled:opacity-40 [&_svg]:size-3.5 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "border-primary bg-primary text-primary-foreground hover:border-focus hover:bg-focus", + destructive: "border-fail bg-fail text-primary-foreground hover:bg-transparent hover:text-fail", + outline: "border-input bg-transparent text-foreground hover:border-primary", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:border-input", + ghost: "border-transparent bg-transparent text-muted-foreground hover:text-foreground", + link: "border-transparent text-name underline-offset-4 hover:underline", + }, + size: { + default: "h-8 px-3", + sm: "h-7 px-2.5", + lg: "h-9 px-4", + icon: "h-7 w-7", + }, + }, + defaultVariants: { variant: "default", size: "default" }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button"; + return ; +} + +export { Button, buttonVariants }; diff --git a/editor-web/src/components/ui/resizable.tsx b/editor-web/src/components/ui/resizable.tsx new file mode 100644 index 00000000..24f3c6c1 --- /dev/null +++ b/editor-web/src/components/ui/resizable.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import { Group, Panel, Separator } from "react-resizable-panels"; + +import { cn } from "@/lib/utils"; + +/** Horizontal split container: … */ +function ResizablePanelGroup({ + className, + ...props +}: React.ComponentProps) { + return ; +} + +const ResizablePanel = Panel; + +function ResizableHandle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/editor-web/src/components/ui/scroll-area.tsx b/editor-web/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..61bb5856 --- /dev/null +++ b/editor-web/src/components/ui/scroll-area.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; + +import { cn } from "@/lib/utils"; + +/** Scrolls without a visible bar until the pointer is over it. */ +function ScrollArea({ className, children, ...props }: React.ComponentProps) { + return ( + + {/* Radix sets `display: table` inline on the viewport, which lets wide + children (code editors) stretch the panel. Force block + full width + so inner scrollers own the overflow instead. */} + {children} + + + + ); +} + +function ScrollBar({ className, ...props }: React.ComponentProps) { + return ( + + + + ); +} + +export { ScrollArea, ScrollBar }; diff --git a/editor-web/src/components/ui/separator.tsx b/editor-web/src/components/ui/separator.tsx new file mode 100644 index 00000000..4cbc1776 --- /dev/null +++ b/editor-web/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/lib/utils"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/editor-web/src/components/ui/slider.tsx b/editor-web/src/components/ui/slider.tsx new file mode 100644 index 00000000..15d8ce3b --- /dev/null +++ b/editor-web/src/components/ui/slider.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import { cn } from "@/lib/utils"; + +/** A flat rule with a square handle; the range turns orange once the value + * differs from the source (`modified`). */ +function Slider({ + className, + modified, + ...props +}: React.ComponentProps & { modified?: boolean }) { + return ( + + + + + + + ); +} + +export { Slider }; diff --git a/editor-web/src/components/ui/tabs.tsx b/editor-web/src/components/ui/tabs.tsx new file mode 100644 index 00000000..c60fc777 --- /dev/null +++ b/editor-web/src/components/ui/tabs.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +function Tabs({ className, ...props }: React.ComponentProps) { + return ; +} + +function TabsList({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function TabsTrigger({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function TabsContent({ className, ...props }: React.ComponentProps) { + return ; +} + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/editor-web/src/index.css b/editor-web/src/index.css new file mode 100644 index 00000000..a9d4e548 --- /dev/null +++ b/editor-web/src/index.css @@ -0,0 +1,239 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +/* Black and white, square, hard 2px rules (after the SimpleCAD landing page). + * Color is reserved for meaning: focus (selection, what is being edited), + * pass, fail, name (the names of steps and parameters). */ +/* Light is the default; `html.dark` (set before first paint from the saved choice or + * the system, see index.html and lib/theme.ts) restores the dark palette. */ +:root { + --radius: 0px; + --background: #ffffff; + --foreground: #000000; + --card: #f6f6f6; + --card-foreground: #000000; + --popover: #ffffff; + --popover-foreground: #000000; + --primary: #000000; + --primary-foreground: #ffffff; + --secondary: #eeeeee; + --secondary-foreground: #000000; + --muted: #eeeeee; + --muted-foreground: #555555; + --accent: #e4e4e4; + --accent-foreground: #000000; + --destructive: #d12b3c; + --destructive-foreground: #ffffff; + --border: #cfcfcf; + --input: #9a9a9a; + --ring: #d56a00; + --focus: #d56a00; + --pass: #12803f; + --fail: #d12b3c; + --name: #1a62c4; + --comment: #6b6b6b; + --code-selection: rgba(0, 0, 0, 0.15); + --scrollbar: #bdbdbd; + --scrollbar-hover: #7a7a7a; + --selection-bg: #000000; + --selection-fg: #ffffff; + --overlay: rgba(255, 255, 255, 0.78); + --shadow-hard: rgba(0, 0, 0, 0.14); + --rule: 2px; + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; +} + +:root.dark { + --background: #000000; + --foreground: #ffffff; + --card: #050505; + --card-foreground: #ffffff; + --popover: #000000; + --popover-foreground: #ffffff; + --primary: #ffffff; + --primary-foreground: #000000; + --secondary: #111111; + --secondary-foreground: #ffffff; + --muted: #111111; + --muted-foreground: #a8a8a8; + --accent: #1c1c1c; + --accent-foreground: #ffffff; + --destructive: #ff4d5e; + --destructive-foreground: #000000; + --border: #262626; + --input: #3a3a3a; + --ring: #ff9a2e; + --focus: #ff9a2e; + --pass: #3ddc84; + --fail: #ff4d5e; + --name: #4aa3ff; + --comment: #777777; + --code-selection: rgba(255, 255, 255, 0.2); + --scrollbar: #333333; + --scrollbar-hover: #777777; + --selection-bg: #ffffff; + --selection-fg: #000000; + --overlay: rgba(27, 27, 27, 0.75); + --shadow-hard: rgba(255, 255, 255, 0.1); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-focus: var(--focus); + --color-pass: var(--pass); + --color-fail: var(--fail); + --color-name: var(--name); + --color-scrollbar: var(--scrollbar); + --color-scrollbar-hover: var(--scrollbar-hover); + --color-overlay: var(--overlay); + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 0px; + --radius-xl: 0px; + --font-mono: var(--mono); +} + +@layer base { + * { + @apply border-border outline-ring/50; + border-radius: 0; + /* Thin, square scrollbars everywhere; the thumb shows on hover. */ + scrollbar-width: thin; + scrollbar-color: var(--scrollbar) transparent; + } + *::-webkit-scrollbar { + width: 6px; + height: 6px; + } + *::-webkit-scrollbar-thumb { + background: var(--scrollbar); + } + *::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-hover); + } + *::-webkit-scrollbar-track, + *::-webkit-scrollbar-corner { + background: transparent; + } + html, + body { + background: var(--background); + } + body { + @apply bg-background text-foreground; + font-family: var(--mono); + font-size: 12px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + } + ::selection { + background: var(--selection-bg); + color: var(--selection-fg); + } + .font-mono, + code, + pre { + font-family: var(--mono); + } + button { + cursor: pointer; + } + /* Notebook cells: the editor fills its block and wraps long lines, so a + * cell never shows a horizontal scrollbar. */ + .cm-editor { + width: 100%; + background: transparent !important; + } + .cm-editor .cm-scroller { + overflow-x: hidden; + overflow-y: hidden; + scrollbar-width: none; + font-family: var(--mono); + } + .cm-editor .cm-scroller::-webkit-scrollbar { + display: none; + } + .cm-editor.cm-focused { + outline: none; + } +} + +/* Small caps label used for section titles and badges. */ +.label { + font-size: 10px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted-foreground); +} + +/* Loading mask: the spinner's chase, the progress strip, the blinking cursor. */ +@keyframes sca-chase { + 0% { + opacity: 1; + } + 100% { + opacity: 0.12; + } +} +@keyframes sca-bar { + 0%, + 100% { + opacity: 0.12; + } + 50% { + opacity: 1; + } +} +@keyframes sca-blink { + 50% { + opacity: 0; + } +} +.sca-chase { + animation: sca-chase 0.8s steps(5, end) infinite; +} +.sca-bar { + animation: sca-bar 1.4s steps(4, end) infinite; +} +.sca-blink { + animation: sca-blink 1s steps(1, end) infinite; +} +@media (prefers-reduced-motion: reduce) { + .sca-chase, + .sca-bar, + .sca-blink { + animation: none; + } +} + +/* A cell the kernel is running or has queued: a thin bar marching under its header. */ +.cell-running { + background: repeating-linear-gradient(90deg, var(--focus, #f5a623) 0 12px, transparent 12px 24px); + background-size: 24px 100%; + animation: cell-march 0.6s linear infinite; +} +@keyframes cell-march { + to { + background-position: 24px 0; + } +} diff --git a/editor-web/src/lib/api.ts b/editor-web/src/lib/api.ts new file mode 100644 index 00000000..8509c6c0 --- /dev/null +++ b/editor-web/src/lib/api.ts @@ -0,0 +1,91 @@ +/** REST surface of the runtime server (`sca edit `). + * + * The WebSocket (`lib/envelope.ts`) carries live model events; these calls + * cover discovery, control, and the agent-facing selection contract. + */ + +export type NotebookRow = { + name: string; + path: string; + entry: boolean; + kind: "part" | "assembly" | "helper"; + current: boolean; + /** Definition id from the notebook header. */ + id?: string; + /** Notebooks this one runs with use(...), workspace-relative. */ + uses?: string[]; + error?: string | null; +}; + +/** A local module some notebook imports (shared dimensions, helpers). */ +export type ModuleRow = { name: string; path: string; used_by: string[] }; + +export type SelectedInfo = { + selected: Record; + notebook: string | null; +}; + +async function json(response: Response): Promise { + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new Error(`${response.status} ${response.statusText}: ${body.slice(0, 200)}`); + } + return (await response.json()) as T; +} + +/** Notebooks in the workspace with entry/helper classification. */ +export function getWorkspace(): Promise<{ notebooks: NotebookRow[]; modules: ModuleRow[] }> { + return fetch("/api/workspace").then((response) => json<{ notebooks: NotebookRow[]; modules: ModuleRow[] }>(response)); +} + +/** Switch the active entry notebook (server restarts the kernel). */ +export function openNotebook(path: string): Promise<{ ok: boolean }> { + return fetch("/api/workspace/open", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path }), + }).then((response) => json<{ ok: boolean }>(response)); +} + +/** Provision a new entry notebook from the server template and open it. */ +export function newNotebook(name: string, kind: "part" | "assembly"): Promise<{ created: string }> { + return fetch("/api/workspace/new", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, kind }), + }).then((response) => json<{ created: string }>(response)); +} + +/** What the viewport shows (PNG data URL), for coding agents to look at. */ +export async function postViewport(dataUrl: string): Promise { + const blob = await (await fetch(dataUrl)).blob(); + await fetch("/api/studio/viewport", { method: "POST", headers: { "Content-Type": "image/png" }, body: blob }); +} + +/** The latest preview of the model (`simplecadapi.scene.preview`). */ +export async function fetchPreview(): Promise { + const response = await fetch("/api/preview/latest"); + if (!response.ok) throw new Error("no preview published yet"); + return response.arrayBuffer(); +} + +/** Latest in-memory scadpkg bytes (rule 4: never a disk read). */ +export async function fetchPackage(): Promise { + const response = await fetch("/api/package/latest"); + if (!response.ok) throw new Error("no package published yet"); + return new Uint8Array(await response.arrayBuffer()); +} + +/** Publish the viewport selection so agents can read it. */ +export function select(selection: Record): Promise<{ ok: boolean }> { + return fetch("/api/studio/select", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(selection), + }).then((response) => json<{ ok: boolean }>(response)); +} + +/** Agent-facing read: what the human has selected right now. */ +export function getSelected(): Promise { + return fetch("/api/studio/selected").then((response) => json(response)); +} diff --git a/editor-web/src/lib/cm-theme.ts b/editor-web/src/lib/cm-theme.ts new file mode 100644 index 00000000..8ca55991 --- /dev/null +++ b/editor-web/src/lib/cm-theme.ts @@ -0,0 +1,44 @@ +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { EditorView } from "@codemirror/view"; +import { tags } from "@lezer/highlight"; + +/** Code on the cell's own ground; color only where it carries meaning: names (blue), + * literals (orange), strings (green), comments (dim). Every color is a CSS variable of + * the page (index.css), so one theme serves light and dark and follows a switch live. */ +const theme = EditorView.theme( + { + "&": { color: "var(--foreground)", backgroundColor: "transparent", fontSize: "12px" }, + ".cm-content": { caretColor: "var(--focus)", padding: "6px 0" }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--focus)", borderLeftWidth: "2px" }, + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { + backgroundColor: "var(--code-selection)", + }, + ".cm-activeLine": { backgroundColor: "transparent" }, + ".cm-gutters": { display: "none" }, + ".cm-line": { padding: "0 10px" }, + // completion and lint popups sit on the page, not on the cell + ".cm-tooltip": { + backgroundColor: "var(--popover)", + color: "var(--popover-foreground)", + border: "2px solid var(--border)", + }, + ".cm-tooltip-autocomplete ul li[aria-selected]": { + backgroundColor: "var(--primary)", + color: "var(--primary-foreground)", + }, + }, + { dark: false }, +); + +const highlight = HighlightStyle.define([ + { tag: [tags.keyword, tags.controlKeyword, tags.definitionKeyword, tags.moduleKeyword], color: "var(--foreground)", fontWeight: "700" }, + { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: "var(--name)" }, + { tag: [tags.definition(tags.variableName), tags.className], color: "var(--name)" }, + { tag: [tags.number, tags.bool, tags.null, tags.atom], color: "var(--focus)" }, + { tag: [tags.string, tags.special(tags.string)], color: "var(--pass)" }, + { tag: [tags.comment, tags.lineComment, tags.blockComment], color: "var(--comment)", fontStyle: "italic" }, + { tag: [tags.operator, tags.punctuation, tags.bracket], color: "var(--muted-foreground)" }, + { tag: tags.self, color: "var(--foreground)", fontStyle: "italic" }, +]); + +export const cadTheme = [theme, syntaxHighlighting(highlight)]; diff --git a/editor-web/src/lib/envelope.ts b/editor-web/src/lib/envelope.ts new file mode 100644 index 00000000..eb1423c2 --- /dev/null +++ b/editor-web/src/lib/envelope.ts @@ -0,0 +1,67 @@ +/** Live event socket: server-pushed model envelopes over /ws. + * + * Envelope types (workspace_server.replay order): params.schema, + * params.values, geometry, tree, notebook.cells, notebook.features, + * package, selection, status, hello. + */ + +export type Envelope = Record & { type: string }; +type Handler = (envelope: Envelope) => void; + +export class EnvelopeSocket { + private ws: WebSocket | null = null; + private handlers = new Map>(); + private backoffMs = 500; + private closed = false; + + constructor(private readonly url: string) {} + + on(type: string, handler: Handler): () => void { + let set = this.handlers.get(type); + if (!set) { + set = new Set(); + this.handlers.set(type, set); + } + set.add(handler); + return () => set!.delete(handler); + } + + connect(): void { + this.closed = false; + const ws = new WebSocket(this.url); + this.ws = ws; + ws.onopen = () => { + this.backoffMs = 500; + }; + ws.onmessage = (event) => { + let envelope: Envelope; + try { + envelope = JSON.parse(String(event.data)); + } catch { + return; + } + const set = this.handlers.get(envelope.type); + if (set) for (const handler of set) handler(envelope); + const anySet = this.handlers.get("*"); + if (anySet) for (const handler of anySet) handler(envelope); + }; + ws.onclose = () => { + if (this.closed) return; + // Kernel/server restarts must transparently re-attach the UI. + setTimeout(() => this.connect(), this.backoffMs); + this.backoffMs = Math.min(this.backoffMs * 2, 8000); + }; + } + + /** Send one ui command (param.set / export.request / code.edit / rerun.request). */ + send(message: Record): void { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } + } + + close(): void { + this.closed = true; + this.ws?.close(); + } +} diff --git a/editor-web/src/lib/preview.ts b/editor-web/src/lib/preview.ts new file mode 100644 index 00000000..1f12b374 --- /dev/null +++ b/editor-web/src/lib/preview.ts @@ -0,0 +1,102 @@ +/** The preview a kernel makes of its model (`simplecadapi.scene.preview`): + * the arrays a viewer draws, the index range of every face and edge, each part + * once and where each of its instances stands. + * + * Layout: `SCAPREV1`, a u32 header length, 4 bytes of padding, the JSON header + * (padded to 8 bytes), then the little-endian arrays the header points into. + */ + +export type PreviewGroup = { entity_id: string; first_index: number; index_count: number }; +export type PreviewTransform = { + origin: [number, number, number]; + x_axis: [number, number, number]; + y_axis: [number, number, number]; + z_axis: [number, number, number]; +}; + +export type PreviewPart = { + key: string; + definitionId: string; + /** glTF metres (+Y up), as in the package's GLB assets. */ + positions: Float32Array; + normals: Float32Array; + indices: Uint32Array; + edgePositions: Float32Array; + edgeIndices: Uint32Array; + vertexPositions: Float32Array; + vertexIds: string[]; + faceGroups: PreviewGroup[]; + edgeGroups: PreviewGroup[]; + solidEntityId: string; +}; + +export type PreviewNode = { + nodeId: string; + partKey: string; + definitionId: string; + displayName: string; + transform: PreviewTransform; +}; + +export type PreviewData = { parts: Map; nodes: PreviewNode[] }; + +type ArrayRef = { offset: number; count: number }; +type HeaderPart = { + key: string; + definition_id: string; + positions: ArrayRef; + normals: ArrayRef; + indices: ArrayRef; + edge_positions: ArrayRef; + edge_indices: ArrayRef; + vertex_positions: ArrayRef; + vertex_ids: string[]; + face_groups: PreviewGroup[]; + edge_groups: PreviewGroup[]; + solid_entity_id: string; +}; +type Header = { + schema: string; + parts: HeaderPart[]; + nodes: { node_id: string; part: string; definition_id: string; display_name: string; transform: PreviewTransform }[]; +}; + +const MAGIC = "SCAPREV1"; + +export function parsePreview(buffer: ArrayBuffer): PreviewData { + const bytes = new Uint8Array(buffer); + if (new TextDecoder().decode(bytes.subarray(0, MAGIC.length)) !== MAGIC) throw new Error("not a preview"); + const view = new DataView(buffer); + const headerLength = view.getUint32(MAGIC.length, true); + const headerStart = MAGIC.length + 8; + const header = JSON.parse(new TextDecoder().decode(bytes.subarray(headerStart, headerStart + headerLength))) as Header; + if (header.schema !== "sca-preview-1") throw new Error(`unsupported preview schema: ${header.schema}`); + const base = headerStart + headerLength; + const floats = (ref: ArrayRef) => new Float32Array(buffer, base + ref.offset, ref.count); + const uints = (ref: ArrayRef) => new Uint32Array(buffer, base + ref.offset, ref.count); + const parts = new Map(); + for (const part of header.parts) { + parts.set(part.key, { + key: part.key, + definitionId: part.definition_id, + positions: floats(part.positions), + normals: floats(part.normals), + indices: uints(part.indices), + edgePositions: floats(part.edge_positions), + edgeIndices: uints(part.edge_indices), + vertexPositions: floats(part.vertex_positions), + vertexIds: part.vertex_ids, + faceGroups: part.face_groups, + edgeGroups: part.edge_groups, + solidEntityId: part.solid_entity_id, + }); + } + const nodes: PreviewNode[] = header.nodes.map((node) => ({ + nodeId: node.node_id, + partKey: node.part, + definitionId: node.definition_id, + displayName: node.display_name, + transform: node.transform, + })); + return { parts, nodes }; +} diff --git a/editor-web/src/lib/runlog.ts b/editor-web/src/lib/runlog.ts new file mode 100644 index 00000000..8a895428 --- /dev/null +++ b/editor-web/src/lib/runlog.ts @@ -0,0 +1,163 @@ +import { useCallback, useRef, useState } from "react"; + +export type LogLevel = "info" | "run" | "ok" | "error"; + +export interface LogEntry { + id: number; + at: number; + level: LogLevel; + text: string; +} + +/** Cells being re-evaluated together: a parameter change, a run, a reload. */ +export interface RunBatch { + total: number; + done: number; + /** Title of the cell running now, if one is. */ + current: string | null; + startedAt: number; +} + +const MAX_ENTRIES = 400; +/** The gap between two rounds of one run is shorter than this; a longer quiet ends the batch. */ +const BATCH_QUIET_MS = 700; + +const ACTIVE = new Set(["queued", "running"]); +const FAILED = new Set(["exception", "error", "marimo-error"]); +/** Not run because a cell it depends on failed: counted, but the cause is what is logged. */ +const BLOCKED = "cancelled"; + +const seconds = (ms: number) => `${(ms / 1000).toFixed(ms < 10_000 ? 2 : 1)} s`; + +/** The editor's log (what happened, what failed) and the progress of the cells + * being re-evaluated, both fed by the kernel's cell states. Errors belong here, + * not in the header: they stay, with the time and the cell they came from. */ +export function useRunLog() { + const [entries, setEntries] = useState([]); + const [batch, setBatch] = useState(null); + const [unseenErrors, setUnseenErrors] = useState(0); + const counter = useRef(0); + // Per cell: when it began running, and the batch's bookkeeping. + const began = useRef(new Map()); + const members = useRef(new Map()); + const batchStart = useRef(0); + const quiet = useRef(null); + const titles = useRef(new Map()); + const failures = useRef(0); + const reported = useRef(new Set()); + + const push = useCallback((level: LogLevel, text: string) => { + counter.current += 1; + const entry: LogEntry = { id: counter.current, at: Date.now(), level, text }; + setEntries((previous) => [...previous.slice(-(MAX_ENTRIES - 1)), entry]); + if (level === "error") setUnseenErrors((count) => count + 1); + }, []); + + const clear = useCallback(() => { + setEntries([]); + setUnseenErrors(0); + }, []); + const markSeen = useCallback(() => setUnseenErrors(0), []); + + const refresh = useCallback((current: string | null) => { + const all = [...members.current.values()]; + setBatch({ + total: all.length, + done: all.filter((state) => state === "done").length, + current, + startedAt: batchStart.current, + }); + }, []); + + const finishBatch = useCallback(() => { + quiet.current = null; + const total = members.current.size; + if (total > 0) { + const text = `${total} ${total === 1 ? "cell" : "cells"} re-evaluated in ${seconds(Date.now() - batchStart.current)}`; + if (failures.current > 0) push("error", `${text}; ${failures.current} did not complete`); + else push("ok", text); + } + members.current.clear(); + failures.current = 0; + setBatch(null); + }, [push]); + + /** Keep the cells' titles to name them in the log, and log each error a cell has newly. */ + const knowCells = useCallback( + (cells: { id: string; title?: string; name?: string; errors?: string[] | null }[]) => { + titles.current = new Map(cells.map((cell, index) => [cell.id, cell.title || cell.name || `cell ${index + 1}`])); + const seen = new Set(); + for (const cell of cells) { + for (const message of cell.errors ?? []) { + const key = `${cell.id}:${message}`; + seen.add(key); + if (!reported.current.has(key)) push("error", `${titles.current.get(cell.id)}: ${message}`); + } + } + reported.current = seen; + }, + [push], + ); + + /** A cell's state changed in the kernel. */ + const cellStatus = useCallback( + (id: string, status: string, hint?: string | null) => { + const title = titles.current.get(id) ?? hint ?? id; + const now = Date.now(); + if (ACTIVE.has(status)) { + if (quiet.current !== null) { + window.clearTimeout(quiet.current); + quiet.current = null; + } + if (members.current.size === 0) { + batchStart.current = now; + push("run", "re-evaluating…"); + } + members.current.set(id, "pending"); + if (status === "running") began.current.set(id, now); + refresh(status === "running" ? title : null); + return; + } + const started = began.current.get(id); + began.current.delete(id); + if (!members.current.has(id)) { + return; + } + members.current.set(id, "done"); + if (status === BLOCKED) { + failures.current += 1; + } else if (FAILED.has(status)) { + failures.current += 1; + push("error", `${title} failed${started ? ` after ${seconds(now - started)}` : ""}`); + } else if (started !== undefined && now - started >= 300) { + push("info", `${title} ${seconds(now - started)}`); + } + const pending = [...members.current.values()].includes("pending"); + refresh(null); + if (!pending) { + quiet.current = window.setTimeout(finishBatch, BATCH_QUIET_MS); + } + }, + [finishBatch, push, refresh], + ); + + /** The model of the run arrived: the batch is over if every cell in it is. */ + const modelShown = useCallback(() => { + if (members.current.size > 0 && ![...members.current.values()].includes("pending")) { + if (quiet.current !== null) window.clearTimeout(quiet.current); + finishBatch(); + } + }, [finishBatch]); + + /** The run was stopped or the notebook changed: nothing is in flight any more. */ + const resetBatch = useCallback(() => { + if (quiet.current !== null) window.clearTimeout(quiet.current); + quiet.current = null; + members.current.clear(); + began.current.clear(); + failures.current = 0; + setBatch(null); + }, []); + + return { entries, batch, unseenErrors, push, clear, markSeen, knowCells, cellStatus, modelShown, resetBatch }; +} diff --git a/editor-web/src/lib/selection.ts b/editor-web/src/lib/selection.ts new file mode 100644 index 00000000..72cb7198 --- /dev/null +++ b/editor-web/src/lib/selection.ts @@ -0,0 +1,42 @@ +/** One picked entity, as the editor tracks it and sends it to the server. */ +export type SelectionItem = { + /** `nodeId::entityId`, unique per picked thing. */ + key: string; + /** `component` (an occurrence), or the entity's own kind. */ + kind: "component" | "solid" | "face" | "edge" | "vertex"; + nodeId: string; + entityId: string | null; + definitionId: string | null; + properties: Record | null; + geometry: Record | null; + tags: string[] | null; + /** The `selected_*` variable the notebook assigns it to (set by the server). */ + variable: string; + /** Its properties are being asked for (a preview carries none). */ + pending?: boolean; +}; + +/** The names the server gives the items, mirrored for the inspector: + * `selected_`, or `selected__` per kind when there are several. */ +export function variableNames(items: { kind: string }[]): string[] { + const totals = new Map(); + for (const item of items) totals.set(item.kind, (totals.get(item.kind) ?? 0) + 1); + const seen = new Map(); + return items.map((item) => { + const index = (seen.get(item.kind) ?? 0) + 1; + seen.set(item.kind, index); + return totals.get(item.kind) === 1 ? `selected_${item.kind}` : `selected_${item.kind}_${index}`; + }); +} + +/** What goes over the wire (`selection.set`). */ +export function selectionPayload(items: SelectionItem[]): Record[] { + return items.map((item) => ({ + kind: item.kind, + entity_id: item.entityId, + node_id: item.nodeId, + definition_id: item.definitionId, + properties: item.properties, + geometry: item.geometry, + })); +} diff --git a/editor-web/src/lib/theme.ts b/editor-web/src/lib/theme.ts new file mode 100644 index 00000000..54ed31d9 --- /dev/null +++ b/editor-web/src/lib/theme.ts @@ -0,0 +1,66 @@ +import { useCallback, useEffect, useState } from "react"; + +/** What the person chose; `system` follows the operating system. */ +export type ThemePreference = "system" | "light" | "dark"; +/** What is on screen. */ +export type ResolvedTheme = "light" | "dark"; + +const KEY = "sca-theme"; +const ORDER: ThemePreference[] = ["system", "light", "dark"]; + +const systemDark = () => window.matchMedia("(prefers-color-scheme: dark)").matches; + +function readPreference(): ThemePreference { + try { + const stored = window.localStorage.getItem(KEY); + if (stored === "light" || stored === "dark" || stored === "system") return stored; + } catch { + /* private mode: the choice just does not persist */ + } + return "system"; +} + +const resolve = (preference: ThemePreference): ResolvedTheme => + preference === "system" ? (systemDark() ? "dark" : "light") : preference; + +/** Put the theme on the page: the `dark` class selects the dark tokens, `color-scheme` + * makes native controls and scrollbars follow. index.html does the same before first paint. */ +function apply(theme: ResolvedTheme) { + const root = document.documentElement; + root.classList.toggle("dark", theme === "dark"); + root.style.colorScheme = theme; +} + +/** The theme: a remembered preference (system, light, dark) and the theme it resolves to. + * While the preference is `system` a change of the operating system's theme is followed live. */ +export function useTheme() { + const [preference, setPreference] = useState(readPreference); + const [resolved, setResolved] = useState(() => resolve(readPreference())); + + useEffect(() => { + const update = () => { + const next = resolve(preference); + setResolved(next); + apply(next); + }; + update(); + if (preference !== "system") return; + const query = window.matchMedia("(prefers-color-scheme: dark)"); + query.addEventListener("change", update); + return () => query.removeEventListener("change", update); + }, [preference]); + + const choose = useCallback((next: ThemePreference) => { + setPreference(next); + try { + window.localStorage.setItem(KEY, next); + } catch { + /* not persisted */ + } + }, []); + + /** system -> light -> dark -> system */ + const cycle = useCallback(() => choose(ORDER[(ORDER.indexOf(preference) + 1) % ORDER.length]), [choose, preference]); + + return { preference, resolved, choose, cycle }; +} diff --git a/editor-web/src/lib/utils.ts b/editor-web/src/lib/utils.ts new file mode 100644 index 00000000..16c20303 --- /dev/null +++ b/editor-web/src/lib/utils.ts @@ -0,0 +1,7 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** shadcn/ui class combiner. */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/editor-web/src/main.tsx b/editor-web/src/main.tsx new file mode 100644 index 00000000..2d80b4ec --- /dev/null +++ b/editor-web/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import { App } from "./App"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/editor-web/src/scene/Viewport.tsx b/editor-web/src/scene/Viewport.tsx new file mode 100644 index 00000000..21ad4999 --- /dev/null +++ b/editor-web/src/scene/Viewport.tsx @@ -0,0 +1,107 @@ +import { useEffect, useRef } from "react"; +import * as THREE from "three"; + +import { SceneView, type PickResult, type SelectionMode } from "@/scene/scene-view"; +import { PALETTES, type SceneTheme } from "@/scene/theme"; + +export type { PickResult, SelectionMode }; + + +/** What the viewport highlights: the picked entities. */ +export type Highlight = { nodeId: string; entityId: string | null }; + +/** Mounts the three.js SceneView once and draws the selection highlight. + * + * The parent owns the SceneView instance (imperative scene loading does not + * map to React state) and the selection; this component guarantees + * mount/dispose and keeps the highlight overlays in step with `selection`. + * `sceneVersion` changes whenever a scene was (re)loaded, which discards the + * old overlays with the old geometry. + */ +export function Viewport({ + viewRef, + selectionMode, + selection, + sceneVersion, + theme, + onPick, + onClear, +}: { + viewRef: React.MutableRefObject; + selectionMode: SelectionMode; + selection: Highlight[]; + sceneVersion: number; + theme: SceneTheme; + onPick: (pick: PickResult) => void; + onClear: () => void; +}) { + const hostRef = useRef(null); + const pickRef = useRef(onPick); + pickRef.current = onPick; + const clearRef = useRef(onClear); + clearRef.current = onClear; + const overlayRef = useRef([]); + // The theme the view was created with; later changes go through setTheme. + const themeRef = useRef(theme); + themeRef.current = theme; + + const dropOverlays = () => { + for (const object of overlayRef.current) { + object.parent?.remove(object); + object.traverse((child) => { + const mesh = child as THREE.Mesh; + mesh.geometry?.dispose?.(); + const material = mesh.material as THREE.Material | THREE.Material[] | undefined; + if (Array.isArray(material)) material.forEach((item) => item.dispose()); + else material?.dispose?.(); + }); + } + overlayRef.current = []; + }; + + useEffect(() => { + const host = hostRef.current; + if (!host) return; + const view = new SceneView(host, { + onPick: (result) => pickRef.current(result), + onClear: () => clearRef.current(), + selectionMode: "solid", + theme: themeRef.current, + }); + viewRef.current = view; + // Debug/agent affordance: reach the imperative scene from the console. + (window as unknown as { __studioView?: SceneView }).__studioView = view; + return () => { + dropOverlays(); + view.dispose(); + viewRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + viewRef.current?.setSelectionMode(selectionMode); + }, [selectionMode, viewRef]); + + useEffect(() => { + const view = viewRef.current; + if (!view) return; + dropOverlays(); + for (const item of selection) { + overlayRef.current.push(...view.buildEntityOverlays(item.nodeId, item.entityId, PALETTES[theme].selection, 0.8)); + } + view.engine.invalidate(); + document.documentElement.dataset.overlayCount = String(overlayRef.current.length); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selection, sceneVersion, theme]); + + // Another theme: recolor in place (camera, model and selection stay). + useEffect(() => { + const view = viewRef.current; + if (!view) return; + view.setTheme(theme); + view.engine.invalidate(); + }, [theme, viewRef]); + + return
    ; +} diff --git a/editor-web/src/scene/cad-three.ts b/editor-web/src/scene/cad-three.ts new file mode 100644 index 00000000..a84ef13b --- /dev/null +++ b/editor-web/src/scene/cad-three.ts @@ -0,0 +1,93 @@ +// Shared CAD<->glTF three.js helpers, extracted from main.ts so the package +// viewer and the reverse-engineering mode load scenes through one code path. + +import * as THREE from 'three'; +import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; +import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; +import type { SceneNode, Transform, Vec3 } from './scene2'; + +export const CAD_TO_GLTF = new THREE.Matrix4().set(0.001, 0, 0, 0, 0, 0, 0.001, 0, 0, -0.001, 0, 0, 0, 0, 0, 1); +export const GLTF_TO_CAD = new THREE.Matrix4().set(1000, 0, 0, 0, 0, 0, -1000, 0, 0, 1000, 0, 0, 0, 0, 0, 1); + +export function placementMatrix(transform: Transform): THREE.Matrix4 { + const { origin, x_axis, y_axis, z_axis } = transform; + const cad = new THREE.Matrix4().set( + x_axis[0], y_axis[0], z_axis[0], origin[0], + x_axis[1], y_axis[1], z_axis[1], origin[1], + x_axis[2], y_axis[2], z_axis[2], origin[2], + 0, 0, 0, 1, + ); + return CAD_TO_GLTF.clone().multiply(cad).multiply(GLTF_TO_CAD); +} + +export function cadPointToGltf(point: Vec3): THREE.Vector3 { + return new THREE.Vector3(point[0] / 1000, point[2] / 1000, -point[1] / 1000); +} + +export const CAD_EDGE_LINE_WIDTH = 2.4; +/** Explicit dark edge color — must read as a firm outline on the lit body + * while staying distinguishable from the pure-black viewport background. */ +export const CAD_EDGE_COLOR = '#3a414d'; +export const DEFAULT_BASE_COLOR: [number, number, number, 1] = [0.72, 0.75, 0.78, 1]; + +export type Resolution = { width: number; height: number }; + +export function materialFor(node: SceneNode): THREE.MeshStandardMaterial { + const color = node.material_id ? [0.68, 0.78, 0.88, 1] as const : DEFAULT_BASE_COLOR; + // Faces are pushed back in depth: edge lines lie exactly ON the surface, and + // without the offset the face triangles win the depth fight per-pixel — + // edges render as broken dashed fragments (classic CAD z-fighting). + return new THREE.MeshStandardMaterial({ color: new THREE.Color(color[0], color[1], color[2]), metalness: 0.08, roughness: 0.52, side: THREE.FrontSide, transparent: color[3] < 1, opacity: color[3], polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 }); +} + +export function edgeMaterial(resolution: Resolution): LineMaterial { + // Negative slope-scaled polygon offset: at silhouettes the line runs along + // a surface seen edge-on and parts of its quad sit genuinely behind the + // local surface depth — a constant bias loses there, a slope-scaled pull + // (mirror of the faces' +1 push) wins at every viewing angle. + const material = new LineMaterial({ color: CAD_EDGE_COLOR, linewidth: CAD_EDGE_LINE_WIDTH, worldUnits: false, depthTest: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 }); + material.resolution.set(Math.max(resolution.width, 1), Math.max(resolution.height, 1)); + return material; +} + +export function addWideEdgeVisual(source: THREE.LineSegments, resolution: Resolution): void { + const position = source.geometry.getAttribute('position'); + const index = source.geometry.index; + const indexCount = index?.count ?? position.count; + const positions: number[] = []; + for (let offset = 0; offset + 1 < indexCount; offset += 2) { + const a = index ? index.getX(offset) : offset; + const b = index ? index.getX(offset + 1) : offset + 1; + positions.push(position.getX(a), position.getY(a), position.getZ(a), position.getX(b), position.getY(b), position.getZ(b)); + } + const geometry = new LineSegmentsGeometry(); + geometry.setPositions(positions); + const visual = new LineSegments2(geometry, edgeMaterial(resolution)); + visual.name = 'cad-edge-visual'; + visual.userData.pickable = false; + source.add(visual); + const pickingMaterial = new THREE.LineBasicMaterial(); + pickingMaterial.visible = false; + source.material = pickingMaterial; +} + +export function disposeObject(root: THREE.Object3D): void { + const disposedGeometries = new Set(); + const disposedMaterials = new Set(); + root.traverse((object) => { + if (object instanceof THREE.Mesh || object instanceof THREE.LineSegments || object instanceof THREE.Points) { + if (!disposedGeometries.has(object.geometry)) { + object.geometry.dispose(); + disposedGeometries.add(object.geometry); + } + const materials = Array.isArray(object.material) ? object.material : [object.material]; + materials.forEach((material) => { + if (!disposedMaterials.has(material)) { + material.dispose(); + disposedMaterials.add(material); + } + }); + } + }); +} diff --git a/editor-web/src/scene/components/click-selection.ts b/editor-web/src/scene/components/click-selection.ts new file mode 100644 index 00000000..187fd82a --- /dev/null +++ b/editor-web/src/scene/components/click-selection.ts @@ -0,0 +1,73 @@ +import * as THREE from 'three'; + +type ClickSelectionControls = { + target: THREE.Vector3; + update(): void; +}; + +type ClickSelectionOptions = { + element: HTMLElement; + camera: THREE.PerspectiveCamera; + controls: ClickSelectionControls; + movementThreshold?: number; + onPointerStateChange?: (active: boolean) => void; + onClick: (event: PointerEvent) => void; +}; + +export function bindClickSelection(options: ClickSelectionOptions): () => void { + const { element, camera, controls, onClick, movementThreshold = 4 } = options; + let pointerDown: { + x: number; + y: number; + button: number; + position: THREE.Vector3; + up: THREE.Vector3; + target: THREE.Vector3; + zoom: number; + } | null = null; + + const onPointerDown = (event: PointerEvent): void => { + pointerDown = { + x: event.clientX, + y: event.clientY, + button: event.button, + position: camera.position.clone(), + up: camera.up.clone(), + target: controls.target.clone(), + zoom: camera.zoom, + }; + options.onPointerStateChange?.(true); + }; + const onPointerUp = (event: PointerEvent): void => { + const down = pointerDown; + pointerDown = null; + options.onPointerStateChange?.(false); + if (!down || down.button !== 0 || event.button !== 0) return; + if (Math.hypot(event.clientX - down.x, event.clientY - down.y) > movementThreshold) return; + + // The controls may have applied pointer movement between down and up + // (turntable damping, trackball momentum). Drain it, then restore the + // click-time camera — position and orientation — before picking. + controls.update(); + camera.position.copy(down.position); + camera.up.copy(down.up); + camera.zoom = down.zoom; + camera.updateProjectionMatrix(); + controls.target.copy(down.target); + controls.update(); + onClick(event); + }; + const onPointerLeave = (): void => { + pointerDown = null; + options.onPointerStateChange?.(false); + }; + + element.addEventListener('pointerdown', onPointerDown); + element.addEventListener('pointerup', onPointerUp); + element.addEventListener('pointerleave', onPointerLeave); + return () => { + element.removeEventListener('pointerdown', onPointerDown); + element.removeEventListener('pointerup', onPointerUp); + element.removeEventListener('pointerleave', onPointerLeave); + }; +} diff --git a/editor-web/src/scene/product-package.ts b/editor-web/src/scene/product-package.ts new file mode 100644 index 00000000..d5275c9f --- /dev/null +++ b/editor-web/src/scene/product-package.ts @@ -0,0 +1,474 @@ +import { strFromU8, unzipSync } from 'fflate'; +import type { SceneManifest } from './scene2'; + +export type PackageFiles = Record; + +export type OpenedCadPackage = { + files: PackageFiles; + packageContentHash: string; + schemaVersion: '3.0'; +}; + +type HashRecord = { + uri?: string; + path?: string; + sha256: string; + byte_length: number; +}; + +// A record whose payload is one exact ZIP member (definition, occurrence +// graph, member blob, scene projection manifest); v3 always addresses these +// by an explicit path. +type MemberRecord = { + path: string; + sha256: string; + byte_length: number; +}; + +// A scene asset record addressed by its scene URI. +type AssetRecord = HashRecord & { uri: string }; + +type DefinitionKind = 'single_solid' | 'assembly'; + +type DefinitionRecord = MemberRecord & { + definition_kind: DefinitionKind; + definition_id: string; + revision: string; + content_hash: string; +}; + +type PackageRoot = { + path: string; + definition_kind: DefinitionKind; + definition_id: string; + revision: string; + content_hash: string; +}; + +type MemberStorage = { kind: 'member'; path: string }; + +type ArchiveStorage = { + kind: 'archive'; + manifest_name: string; + members: { path: string; sha256: string }[]; +}; + +type BlobRecord = { + sha256: string; + byte_length: number; + media_type: string; + storage: MemberStorage | ArchiveStorage; +}; + +type SceneAssetSource = + | { kind: 'definition'; definition_id: string } + | { kind: 'blob'; sha256: string }; + +type SceneProjection = { + manifest: MemberRecord; + scene_id: string; + revision: string; + assets: { uri: string; source: SceneAssetSource }[]; +}; + +type ProductManifest = { + schema_version: string; + artifact_kind: string; + content_hash: string; + root: PackageRoot; + definitions: DefinitionRecord[]; + blobs: BlobRecord[]; + occurrence_graph: HashRecord; + projections: { scene?: SceneProjection }; +}; + + + +const MAX_PACKAGE_BYTES = 256 * 1024 * 1024; +const MAX_PACKAGE_MEMBERS = 10_000; +const MAX_UNPACKED_BYTES = 512 * 1024 * 1024; +const MAX_MEMBER_BYTES = 256 * 1024 * 1024; +const MAX_MANIFEST_BYTES = 32 * 1024 * 1024; +const MAX_COMPRESSION_RATIO = 100; + +const MANIFEST_KEYS = ['artifact_kind', 'blobs', 'content_hash', 'definitions', 'occurrence_graph', 'projections', 'root', 'schema_version']; + +const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/; + +function validateMemberName(name: string): void { + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,4095}$/.test(name) + || name.split('/').some((segment) => !segment || segment === '.' || segment === '..')) { + throw new Error(`invalid package member: ${name}`); + } +} + +function extractArchive(raw: Uint8Array, manifestNames: readonly string[]): PackageFiles { + if (raw.byteLength > MAX_PACKAGE_BYTES) throw new Error('package exceeds browser size limit'); + let memberCount = 0; + let unpackedBytes = 0; + const seenNames = new Set(); + unzipSync(raw, { filter: (entry) => { + validateMemberName(entry.name); + const foldedName = entry.name.toLowerCase(); + if (seenNames.has(foldedName)) throw new Error(`duplicate or case-colliding package member: ${entry.name}`); + seenNames.add(foldedName); + memberCount += 1; + unpackedBytes += entry.originalSize; + if (entry.compression !== 0 && entry.compression !== 8) throw new Error(`unsupported ZIP compression method: ${entry.name}`); + if (memberCount > MAX_PACKAGE_MEMBERS) throw new Error('package member count exceeds browser limit'); + if (entry.originalSize > MAX_MEMBER_BYTES) throw new Error(`package member exceeds browser size limit: ${entry.name}`); + if (manifestNames.includes(entry.name) && entry.originalSize > MAX_MANIFEST_BYTES) throw new Error(`${entry.name} is too large`); + if (unpackedBytes > MAX_UNPACKED_BYTES) throw new Error('package expands beyond browser size limit'); + if (entry.originalSize > MAX_COMPRESSION_RATIO * Math.max(1, entry.size)) throw new Error(`package member compression ratio is too high: ${entry.name}`); + return false; + }}); + const presentManifests = manifestNames.filter((name) => seenNames.has(name.toLowerCase())); + if (presentManifests.length !== 1) throw new Error('package must contain exactly one supported manifest'); + if (unpackedBytes > MAX_COMPRESSION_RATIO * raw.byteLength) throw new Error('package compression ratio is too high'); + const files = unzipSync(raw) as PackageFiles; + const entries = Object.entries(files); + if (entries.length !== memberCount) throw new Error('package member count changed during extraction'); + if (entries.reduce((sum, [, value]) => sum + value.byteLength, 0) !== unpackedBytes) { + throw new Error('package decoded size differs from ZIP metadata'); + } + return files; +} + +function parseManifest(files: PackageFiles, name: string): T { + const payload = files[name]; + if (!payload || payload.byteLength > MAX_MANIFEST_BYTES) throw new Error(`${name} is missing or too large`); + let value: unknown; + try { + value = JSON.parse(strFromU8(payload)); + } catch { + throw new Error(`${name} is not valid JSON`); + } + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${name} must contain an object`); + return value as T; +} + +function recordPath(record: HashRecord): string { + const path = record.uri ?? record.path; + if (typeof path !== 'string') throw new Error('package record path is missing'); + validateMemberName(path); + return path; +} + +function canonicalJson(value: unknown): string { + if (value === null || typeof value === 'boolean' || typeof value === 'string') return JSON.stringify(value); + if (typeof value === 'number') { + if (!Number.isFinite(value)) throw new Error('manifest contains a non-finite number'); + if (Number.isInteger(value) && !Number.isSafeInteger(value)) throw new Error('manifest contains an unsafe integer'); + return JSON.stringify(value); + } + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`; + if (typeof value !== 'object') throw new Error('manifest contains a non-JSON value'); + const object = value as Record; + return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(',')}}`; +} + +async function sha256(payload: Uint8Array): Promise { + const view = payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength) as ArrayBuffer; + const bytes = new Uint8Array(await crypto.subtle.digest('SHA-256', view)); + return `sha256:${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`; +} + +async function canonicalHash(value: unknown): Promise { + return sha256(new TextEncoder().encode(canonicalJson(value))); +} + +async function validateRecords(files: PackageFiles, records: HashRecord[], manifestName: string): Promise { + const referenced = new Set([manifestName]); + for (const record of records) { + const path = recordPath(record); + if (referenced.has(path)) throw new Error(`duplicate package reference: ${path}`); + referenced.add(path); + const payload = files[path]; + if (!payload) throw new Error(`package member is missing: ${path}`); + if (!Number.isSafeInteger(record.byte_length) || record.byte_length < 0 || payload.byteLength !== record.byte_length) { + throw new Error(`package member length differs: ${path}`); + } + if (!SHA256_PATTERN.test(record.sha256) || await sha256(payload) !== record.sha256) { + throw new Error(`package member hash differs: ${path}`); + } + } + const names = Object.keys(files); + if (names.length !== referenced.size || names.some((name) => !referenced.has(name))) { + throw new Error(`${manifestName} references do not match package members`); + } +} + +function requireObject(value: unknown, what: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${what} must contain an object`); + return value as Record; +} + +function requireRecordArray(value: unknown, what: string): Record[] { + if (!Array.isArray(value) || value.some((item) => typeof item !== 'object' || item === null || Array.isArray(item))) { + throw new Error(`${what} must be an array of records`); + } + return value as Record[]; +} + +function requireString(holder: Record, field: string, what: string): string { + const value = holder[field]; + if (typeof value !== 'string') throw new Error(`${what} ${field} is missing`); + return value; +} + +function requireSha256(holder: Record, field: string, what: string): string { + const value = requireString(holder, field, what); + if (!SHA256_PATTERN.test(value)) throw new Error(`${what} ${field} is not a sha256 digest`); + return value; +} + +function requireByteLength(holder: Record, what: string): number { + const value = holder.byte_length; + if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) throw new Error(`${what} byte_length is invalid`); + return value; +} + +function requireMemberPath(holder: Record, field: string, what: string): string { + const value = requireString(holder, field, what); + validateMemberName(value); + return value; +} + +function requireDefinitionKind(holder: Record, what: string): DefinitionKind { + const value = holder.definition_kind; + if (value !== 'single_solid' && value !== 'assembly') throw new Error(`${what} definition_kind is unsupported`); + return value; +} + +function requireAssetRecord(raw: Record, what: string): AssetRecord { + return { + uri: requireMemberPath(raw, 'uri', what), + sha256: requireSha256(raw, 'sha256', what), + byte_length: requireByteLength(raw, what), + }; +} + +// Strictly ascending adjacent pairs prove sortedness and uniqueness in one +// pass; member names and hex digests are pure ASCII, so JS string comparison +// equals the byte-order sort of the canonical writer. +function requireSortedUnique(values: string[], what: string): void { + if (!values.every((value, index) => index === 0 || values[index - 1] < value)) throw new Error(what); +} + +type ParsedPackage = { + definitionRecords: DefinitionRecord[]; + blobByHash: Map; + occurrenceGraph: MemberRecord; + sceneProjection: SceneProjection | null; + contentHash: string; +}; + +// Mirrors the structural invariants of product-package-3.schema.json and +// ProductPackage._validate_manifest without a JSON-Schema engine. +function parseProductManifest(manifest: Record): ParsedPackage { + const keys = Object.keys(manifest); + if (keys.length !== MANIFEST_KEYS.length || MANIFEST_KEYS.some((key) => !(key in manifest))) { + throw new Error(`product package manifest keys are not canonical: ${keys.sort().join(', ')}`); + } + + const definitionRecords = requireRecordArray(manifest.definitions, 'package definitions').map((raw) => ({ + path: requireMemberPath(raw, 'path', 'definition record'), + sha256: requireSha256(raw, 'sha256', 'definition record'), + byte_length: requireByteLength(raw, 'definition record'), + definition_kind: requireDefinitionKind(raw, 'definition record'), + definition_id: requireString(raw, 'definition_id', 'definition record'), + revision: requireString(raw, 'revision', 'definition record'), + content_hash: requireSha256(raw, 'content_hash', 'definition record'), + })) as DefinitionRecord[]; + if (definitionRecords.length === 0) throw new Error('package definitions are empty'); + requireSortedUnique(definitionRecords.map((record) => record.path), 'definition records must be unique and path-sorted'); + const definitionIds = definitionRecords.map((record) => record.definition_id); + if (new Set(definitionIds).size !== definitionIds.length) throw new Error('definition IDs must be unique'); + const definitionPathSet = new Set(definitionRecords.map((record) => record.path)); + + const rootRaw = requireObject(manifest.root, 'package root'); + if (!definitionPathSet.has(requireMemberPath(rootRaw, 'path', 'package root'))) throw new Error('package root does not resolve'); + + const blobRecords = requireRecordArray(manifest.blobs, 'package blobs').map((raw) => { + const sha256 = requireSha256(raw, 'sha256', 'blob record'); + const storageRaw = requireObject(raw.storage, `blob ${sha256} storage`); + let storage: MemberStorage | ArchiveStorage; + if (storageRaw.kind === 'member') { + storage = { kind: 'member', path: requireMemberPath(storageRaw, 'path', 'blob storage') }; + } else if (storageRaw.kind === 'archive') { + storage = { + kind: 'archive', + manifest_name: requireString(storageRaw, 'manifest_name', 'blob storage'), + members: requireRecordArray(storageRaw.members, 'archive blob members').map((member) => ({ + path: requireMemberPath(member, 'path', 'archive blob member'), + sha256: requireSha256(member, 'sha256', 'archive blob member'), + })), + }; + } else { + throw new Error(`blob ${sha256} storage kind is unsupported`); + } + return { sha256, byte_length: requireByteLength(raw, 'blob record'), media_type: requireString(raw, 'media_type', 'blob record'), storage }; + }) as BlobRecord[]; + requireSortedUnique(blobRecords.map((record) => record.sha256), 'blob records must be unique and hash-sorted'); + const blobByHash = new Map(blobRecords.map((record) => [record.sha256, record])); + for (const record of blobRecords) { + if (record.storage.kind === 'member') { + if (definitionPathSet.has(record.storage.path)) throw new Error('blob member path collides with definition'); + } else { + const memberPaths = record.storage.members.map((member) => member.path); + if (new Set(memberPaths).size !== memberPaths.length) throw new Error(`archive blob members are duplicated: ${record.sha256}`); + if (memberPaths.join('\u0000') !== [...memberPaths].sort().join('\u0000')) throw new Error('archive blob members must be path-sorted'); + for (const member of record.storage.members) { + if (!blobByHash.has(member.sha256)) throw new Error(`archive blob member is not declared: ${member.path}`); + } + } + } + + const occurrenceRaw = requireObject(manifest.occurrence_graph, 'occurrence graph record'); + const occurrenceGraph: MemberRecord = { + path: requireMemberPath(occurrenceRaw, 'path', 'occurrence graph record'), + sha256: requireSha256(occurrenceRaw, 'sha256', 'occurrence graph record'), + byte_length: requireByteLength(occurrenceRaw, 'occurrence graph record'), + }; + + const projectionsRaw = requireObject(manifest.projections, 'package projections'); + let sceneProjection: SceneProjection | null = null; + if (projectionsRaw.scene !== undefined) { + const raw = requireObject(projectionsRaw.scene, 'scene projection'); + const manifestRecord = requireObject(raw.manifest, 'scene projection manifest record'); + sceneProjection = { + manifest: { + path: requireMemberPath(manifestRecord, 'path', 'scene projection manifest'), + sha256: requireSha256(manifestRecord, 'sha256', 'scene projection manifest'), + byte_length: requireByteLength(manifestRecord, 'scene projection manifest'), + }, scene_id: requireString(raw, 'scene_id', 'scene projection'), + revision: requireString(raw, 'revision', 'scene projection'), + assets: requireRecordArray(raw.assets, 'scene projection assets').map((asset) => { + const uri = requireString(asset, 'uri', 'scene projection asset'); + const sourceRaw = requireObject(asset.source, `scene projection asset source: ${uri}`); + if (sourceRaw.kind === 'definition') { + return { uri, source: { kind: 'definition', definition_id: requireString(sourceRaw, 'definition_id', 'scene projection source') } }; + } + if (sourceRaw.kind === 'blob') { + return { uri, source: { kind: 'blob', sha256: requireSha256(sourceRaw, 'sha256', 'scene projection source') } }; + } + throw new Error(`scene projection asset source kind is unsupported: ${uri}`); + }), + }; + const definitionIdSet = new Set(definitionIds); + const seenUris = new Set(); + for (const asset of sceneProjection.assets) { + if (seenUris.has(asset.uri)) throw new Error(`scene projection asset URIs must be unique: ${asset.uri}`); + seenUris.add(asset.uri); + if (asset.source.kind === 'definition' && !definitionIdSet.has(asset.source.definition_id)) { + throw new Error(`scene projection definition does not resolve: ${asset.uri}`); + } + if (asset.source.kind === 'blob' && !blobByHash.has(asset.source.sha256)) { + throw new Error(`scene projection blob does not resolve: ${asset.uri}`); + } + } + } + + return { definitionRecords, blobByHash, occurrenceGraph, sceneProjection, contentHash: requireSha256(manifest, 'content_hash', 'product package') }; +} + +function parseSceneManifest(value: unknown, name: string): SceneManifest { + const manifest = requireObject(value, name); + if (requireString(manifest, 'schema_version', name) !== '2.0') throw new Error(`unsupported scene schema: ${String(manifest.schema_version)}`); + if (requireString(manifest, 'units', name) !== 'mm') throw new Error(`${name} units are unsupported`); + const revision = requireSha256(manifest, 'revision', name); + for (const field of ['roots', 'definitions', 'geometry_assets', 'entity_assets', 'product_assets', 'feature_graph_assets', 'source_assets', 'nodes', 'connectors', 'joints', 'feature_index', 'source_index', 'connector_index', 'joint_index']) { + if (!Array.isArray(manifest[field])) throw new Error(`${name} ${field} must be an array`); + } + requireObject(manifest.camera, `${name} camera`); + requireObject(manifest.extensions, `${name} extensions`); + const scene = manifest as unknown as SceneManifest; + for (const asset of [...scene.geometry_assets, ...scene.entity_assets, ...scene.source_assets]) { + if (typeof asset.uri !== 'string' || !SHA256_PATTERN.test(asset.sha256) || !Number.isSafeInteger(asset.byte_length)) { + throw new Error(`${name} contains an invalid asset record`); + } + } + if (!SHA256_PATTERN.test(revision)) throw new Error(`${name} revision is invalid`); + return scene; +} + +export async function openCadPackage(raw: Uint8Array): Promise { + const files = extractArchive(raw, ['package.json']); + const manifest = parseManifest(files, 'package.json'); + if (manifest.schema_version !== '3.0' || manifest.artifact_kind !== 'product_package') throw new Error('unsupported product package schema'); + const parsed = parseProductManifest(manifest as unknown as Record); + const draft = { ...(manifest as unknown as Record) }; + delete draft.content_hash; + if (parsed.contentHash !== await canonicalHash(draft)) throw new Error('product package content_hash is invalid'); + + // Member closure: every record above maps 1:1 onto a ZIP member and the + // archive may hold nothing else. Archive-kind blobs contribute no direct + // member; their inner payloads appear separately as member-kind blobs. + const memberBlobs = [...parsed.blobByHash.values()] + .filter((record) => record.storage.kind === 'member') + .map((record) => ({ path: (record.storage as MemberStorage).path, sha256: record.sha256, byte_length: record.byte_length })); + await validateRecords(files, [ + ...parsed.definitionRecords, + parsed.occurrenceGraph, + ...memberBlobs, + ...(parsed.sceneProjection ? [parsed.sceneProjection.manifest] : []), + ], 'package.json'); + + // The occurrence graph and the product/feature-graph ZIP payloads are never + // consumed by the viewer, so they are integrity-checked as members above but + // neither re-derived from definitions nor re-packed into canonical archives. + if (!parsed.sceneProjection) throw new Error('product package does not embed a scene projection'); + const projection = parsed.sceneProjection; + const sceneValue = parseManifest(files, projection.manifest.path); + const scene = parseSceneManifest(sceneValue, 'scene manifest'); + const sceneDraft = { ...(sceneValue as Record) }; + delete sceneDraft.revision; + if (scene.revision !== await canonicalHash(sceneDraft)) throw new Error('scene revision is invalid'); + if (scene.scene_id !== projection.scene_id || scene.revision !== projection.revision) { + throw new Error('embedded scene identity differs from package projection'); + } + + const sourceByUri = new Map(projection.assets.map((asset) => [asset.uri, asset.source])); + const outFiles: PackageFiles = { 'scene.json': files[projection.manifest.path] }; + const materialize = (records: HashRecord[], collection: string): void => { + for (const record of records) { + const uri = recordPath(record); + const source = sourceByUri.get(uri); + if (!source) throw new Error(`scene ${collection} asset is not covered by the package projection: ${uri}`); + if (source.kind !== 'blob') throw new Error(`scene ${collection} asset must resolve to a package blob: ${uri}`); + const blob = parsed.blobByHash.get(source.sha256); + if (!blob || blob.storage.kind !== 'member') throw new Error(`scene ${collection} asset resolves to an archive blob: ${uri}`); + if (record.sha256 !== blob.sha256 || record.byte_length !== blob.byte_length) { + throw new Error(`scene ${collection} asset identity differs from package blob: ${uri}`); + } + outFiles[uri] = files[blob.storage.path]; + } + }; + materialize(scene.geometry_assets, 'geometry'); + materialize(scene.entity_assets, 'entity'); + materialize(scene.source_assets, 'source'); + + for (const record of scene.product_assets) { + const uri = recordPath(record); + const source = sourceByUri.get(uri); + if (!source) throw new Error(`scene product asset is not covered by the package projection: ${uri}`); + if (source.kind !== 'definition') throw new Error(`scene product asset must resolve to a package definition: ${uri}`); + } + for (const record of scene.feature_graph_assets) { + const uri = recordPath(record); + const source = sourceByUri.get(uri); + if (!source) throw new Error(`scene feature graph asset is not covered by the package projection: ${uri}`); + if (source.kind !== 'blob') throw new Error(`scene feature graph asset must resolve to a package blob: ${uri}`); + } + + const identity = (record: { definition_kind: string; definition_id: string; revision: string; content_hash: string }): string => + [record.definition_kind, record.definition_id, record.revision, record.content_hash].join('\u0000'); + const packageIdentities = parsed.definitionRecords.map(identity).sort(); + const sceneIdentities = scene.product_assets.map(identity).sort(); + if (packageIdentities.length !== sceneIdentities.length + || packageIdentities.some((item, index) => item !== sceneIdentities[index])) { + throw new Error('product package definitions differ from embedded scene product assets'); + } + + return { files: outFiles, packageContentHash: parsed.contentHash, schemaVersion: '3.0' }; +} diff --git a/editor-web/src/scene/renderer/brep-renderer.ts b/editor-web/src/scene/renderer/brep-renderer.ts new file mode 100644 index 00000000..38a90512 --- /dev/null +++ b/editor-web/src/scene/renderer/brep-renderer.ts @@ -0,0 +1,1056 @@ +// Standalone BRep scene renderer component. +// +// Owns everything visual about rendering a scene-2.0 package (GLB faces + +// line edges + entity sidecars): materials, lighting (studio environment + +// key light), postprocessing (GTAO ambient occlusion), camera (free +// trackball), edge display (seam/degenerate edges split into their own +// toggleable visuals), fit/snapshot and lifecycle. It has no notion of +// selection or annotation — embed it directly, or compose it under a +// higher-level view (see src/scene-view.ts for the picking/marks layer). +// +// import { BRepRenderer } from './renderer'; +// const viewer = new BRepRenderer(hostElement); +// await viewer.loadScene(files); // scene-2.0 files (see product-package) +// viewer.setSeamsVisible(false); // default +// viewer.setSSAO(true); // default + +import * as THREE from 'three'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { GTAOPass } from 'three/addons/postprocessing/GTAOPass.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; +import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; +import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; +import { strFromU8 } from 'fflate'; +import { DEFAULT_BASE_COLOR, CAD_EDGE_LINE_WIDTH, cadPointToGltf, disposeObject, materialFor, placementMatrix } from '../cad-three'; +import { PALETTES, type SceneTheme, type ScenePalette } from '../theme'; +import type { PackageFiles } from '../product-package'; +import type { PreviewData, PreviewPart } from '../../lib/preview'; +import type { Entity, EntitySidecar, FaceGroup, SceneManifest, SceneNode, Vec3 } from '../scene2'; + +/** Render layers: faces (0, default) go through lighting + AO; edges (1) and + * annotation overlays (2) composite AFTER the AO pass so their color is + * constant — no GI darkening — while still depth-testing against faces. */ +export const FACE_LAYER = 0; +export const EDGE_LAYER = 1; +export const OVERLAY_LAYER = 2; + +export type CameraState = { + position: Vec3; + target: Vec3; + up: Vec3; + fov_deg: number; + width: number; + height: number; +}; + +export type BRepRendererOptions = { + /** The scene's theme (default dark); `setTheme` changes it later. */ + theme?: SceneTheme; + /** Overrides the theme's background (sRGB). */ + background?: string; + /** GTAO ambient occlusion pass (default on). */ + ssao?: boolean; + /** Show seam/degenerate edges (default off — pro viewers hide them). */ + seams?: boolean; +}; + +type Resolution = { width: number; height: number }; + +/** Direct geometry channel payload (no .scadpkg container): the authoring + * runtime tessellates the live solid and ships the render mesh as-is. */ +export type LiveGeometryPayload = { + schema: 'sca-live-geometry-1'; + step: string; + /** Triangle vertices in CAD space (mm, +Z up). */ + positions: number[][]; + normals: number[][]; + indices: number[]; + /** Flat segment endpoints in CAD space (x1,y1,z1,x2,y2,z2, ...). */ + edge_positions: number[]; + face_groups: { entity_id: string; first: number; count: number }[]; + stats: { volume: number; faces: number }; +}; + +function isHiddenEdge(entity: Entity | undefined): boolean { + const props = entity?.properties as { seam?: unknown; degenerate?: unknown } | undefined; + return props?.seam === true || props?.degenerate === true; +} + +/** + * Edges and faces are tessellated independently, so where a chord of the + * mesh bulges past the edge's own polyline the edge sinks behind the surface + * and breaks into dashes. Slope-scaled polygon offset cannot cover that (the + * gap is a distance, not a slope), so the line shader pulls its fragments + * towards the camera by EDGE_DEPTH_BIAS (scene units, metres): a little more + * than the mesh's chord deviation. Only depth moves; the line stays where it + * is on screen, and anything further behind than the bias still hides it. + */ +const EDGE_DEPTH_BIAS = 0.0004; + +function createEdgeMaterial(color: string): LineMaterial { + const material = new LineMaterial({ color, linewidth: CAD_EDGE_LINE_WIDTH, worldUnits: false, depthTest: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 }); + material.onBeforeCompile = (shader) => { + shader.uniforms.edgeDepthBias = { value: EDGE_DEPTH_BIAS }; + shader.vertexShader = shader.vertexShader + .replace('void main() {', 'uniform float edgeDepthBias;\nvoid main() {') + .replace('gl_Position = clip;', 'float biasedView = -clip.w + edgeDepthBias;\n\t\t\tclip.z = (projectionMatrix[2][2] * biasedView + projectionMatrix[3][2]) / -biasedView * clip.w;\n\t\t\tgl_Position = clip;'); + }; + material.customProgramCacheKey = () => 'edge-depth-bias'; + material.userData.role = 'edge'; // setTheme recolors these, and only these + return material; +} + +export class BRepRenderer { + readonly host: HTMLElement; + readonly threeScene: THREE.Scene; + readonly camera: THREE.PerspectiveCamera; + readonly renderer: THREE.WebGLRenderer; + readonly controls: TrackballControls; + readonly modelRoot = new THREE.Group(); + + manifest: SceneManifest | null = null; + files: PackageFiles | null = null; + nodeVisibility = new Map(); + + private loader = new GLTFLoader(); + private geometryCache = new Map(); + private edgeCache = new Map(); + private entityCache = new Map(); + private nodeObjects = new Map(); + private faceRT: THREE.WebGLRenderTarget | null = null; + private beautyRT: THREE.WebGLRenderTarget | null = null; + private ssaoPass: GTAOPass | null = null; + private ssaoEnabled: boolean; + private rendererClearColor: THREE.Color; + private palette: ScenePalette; + /** The gizmo's per-axis parts, kept to recolor on a theme change. */ + private gizmoAxes: { materials: THREE.MeshBasicMaterial[]; tail: THREE.MeshBasicMaterial; canvas: HTMLCanvasElement; texture: THREE.CanvasTexture; label: string }[] = []; + private blitScene: THREE.Scene | null = null; + private blitCamera: THREE.OrthographicCamera | null = null; + private copyMaterial: THREE.MeshBasicMaterial | null = null; + private presentMaterial: THREE.MeshBasicMaterial | null = null; + private edgesVisible = true; + private seamsVisible: boolean; + private observer: ResizeObserver; + private pendingFrame = true; + private disposed = false; + private grid: THREE.LineSegments | null = null; + /** Entity sidecars of the preview's parts, by node id (a preview has no package). */ + private previewSidecars = new Map(); + private previewGeometries: THREE.BufferGeometry[] = []; + private readonly gizmoScene = new THREE.Scene(); + private readonly gizmoCamera = new THREE.OrthographicCamera(-1.5, 1.5, 1.5, -1.5, 0.1, 20); + + constructor(host: HTMLElement, options: BRepRendererOptions = {}) { + this.host = host; + this.ssaoEnabled = options.ssao ?? true; + this.seamsVisible = options.seams ?? false; + this.threeScene = new THREE.Scene(); + this.camera = new THREE.PerspectiveCamera(42, 1, 0.01, 1000); + this.camera.up.set(0, 0, 1); + this.camera.position.set(2.4, 2.1, 3.0); + this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); + // Background comes from the renderer clear color, NOT scene.background: + // a Color scene.background force-clears the framebuffer on EVERY + // renderer.render() (even autoClear=false), which would wipe the + // face/color buffer when the edge pass draws on top of it. The clear + // color is consumed raw as linear working-space values, so convert the + // sRGB design color first — otherwise the presented background is ~4x + // brighter than intended. + this.palette = PALETTES[options.theme ?? 'dark']; + this.rendererClearColor = this.clearColorFor(options.background); + this.renderer.setClearColor(this.rendererClearColor, 1); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + this.renderer.outputColorSpace = THREE.SRGBColorSpace; + this.renderer.toneMapping = THREE.ACESFilmicToneMapping; + this.renderer.toneMappingExposure = 1.1; + host.append(this.renderer.domElement); + // resize() calls setSize(w, h, updateStyle=false), so the backing store is + // host-size × pixelRatio while the CSS display size stays unset — a host + // that does not style the canvas itself then renders the HiDPI buffer at + // attribute size and crops it. Pin the display size to the host. + this.renderer.domElement.style.width = '100%'; + this.renderer.domElement.style.height = '100%'; + this.renderer.domElement.style.display = 'block'; + // Trackball rotation: no fixed orbit axis, so the model can be rolled + // past any pole — a turntable (OrbitControls) caps vertical sweep below + // 180° no matter how the polar angle is clamped. + this.controls = new TrackballControls(this.camera, this.renderer.domElement); + this.controls.rotateSpeed = 1.0; + this.controls.zoomSpeed = 1.0; + this.controls.dynamicDampingFactor = 0.35; + this.controls.minDistance = 0.01; + this.controls.maxDistance = 1000; + // Host pages may own the keyboard (text inputs); TrackballControls' + // window-level A/S/D drag modifiers must not react to typing. The tuple + // type is fixed, so park unreachable key codes instead of leaving A/S/D live. + this.controls.keys = ['__unused_rotate', '__unused_zoom', '__unused_pan']; + // Shift+left-drag pans. TrackballControls has no modifier support, but its + // `keyState` field (the keyboard A/S/D path) deliberately outranks the + // mouse-button gesture, so borrowing it reuses the controls' own pan + // seeding, damping and release. onPointerUp only resets `state`, so the + // keyState clear is on us. Capture on the host precedes the controls' + // listeners on the canvas. + host.addEventListener( + 'pointerdown', + (event) => { + if (!event.shiftKey || event.button !== 0 || this.controls.enabled === false) return; + // `keyState` drives the controls' real keyboard-pan path but is not + // in the shipped typings; the runtime field is the documented state + // machine (PAN=2, NONE=-1). + const controls = this.controls as TrackballControls & { keyState: number }; + controls.keyState = 2; // TrackballControls._STATE.PAN + const release = (): void => { + controls.keyState = -1; // TrackballControls._STATE.NONE + window.removeEventListener('pointerup', release); + window.removeEventListener('pointercancel', release); + }; + window.addEventListener('pointerup', release); + window.addEventListener('pointercancel', release); + }, + true, + ); + this.modelRoot.name = 'scene-root'; + // Package geometry is glTF (Y up, CAD z -> y, CAD y -> -z). Rotate it back + // so the world is the CAD frame: x, y, z in meters and +Z up, which is + // what the camera's up vector, the ground grid and the axis gizmo assume. + this.modelRoot.rotation.x = Math.PI / 2; + this.threeScene.add(this.modelRoot); + this.buildGizmo(); + this.setupLighting(); + this.setupPostprocessing(); + this.observer = new ResizeObserver(() => this.resize()); + this.observer.observe(host); + this.resize(); + this.renderer.setAnimationLoop(() => this.render()); + } + + private loopRunning = true; + + /** Demand-render flag: the loop always ticks (damping needs it) but the + * expensive SSAO pipeline only runs when something actually changed. */ + private needsRender = true; + + /** Schedule one repaint (scene loads, picks, selection, external edits). */ + invalidate(): void { + this.needsRender = true; + } + + /** Pause/resume the render loop. Inactive panels must not burn GPU — + * several editor modes stay mounted, each with its own WebGL context. */ + setRunning(running: boolean): void { + if (running === this.loopRunning) return; + this.loopRunning = running; + if (running) { + this.renderer.setAnimationLoop(() => this.render()); + } else { + this.renderer.setAnimationLoop(null); + } + } + + isRunning(): boolean { + return this.loopRunning; + } + + private setupLighting(): void { + // Image-based studio lighting instead of hand-placed fill lights; one + // directional key stays for crisp specular definition on flats. + const pmrem = new THREE.PMREMGenerator(this.renderer); + this.threeScene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture; + this.threeScene.environmentIntensity = 0.7; + pmrem.dispose(); + const keyLight = new THREE.DirectionalLight('#fff8e9', 1.4); + keyLight.position.set(4, 7, 5); + this.threeScene.add(keyLight, keyLight.target); + } + + private setupPostprocessing(): void { + // Manual pipeline instead of EffectComposer, so edges can composite AFTER + // the AO blend: + // 1. faces (layer 0) render into faceRT — this carries the face depth + // 2. GTAO blends beauty*ao into beautyRT; a copy brings it back onto + // faceRT, keeping faceRT's depth attachment intact + // 3. edges (layer 1) + overlays (layer 2) render into faceRT with plain + // depth testing — after AO, hence constant color, correctly occluded + // 4. faceRT presents to screen through tone mapping + const size = this.resolution(); + const pixelRatio = this.renderer.getPixelRatio(); + const width = Math.max(size.width * pixelRatio, 1); + const height = Math.max(size.height * pixelRatio, 1); + const options: THREE.RenderTargetOptions = { type: THREE.HalfFloatType, samples: 4 }; + this.faceRT = new THREE.WebGLRenderTarget(width, height, options); + this.beautyRT = new THREE.WebGLRenderTarget(width, height, options); + if (this.ssaoEnabled) { + const ssaoPass = new GTAOPass(this.threeScene, this.camera, width, height); + ssaoPass.output = GTAOPass.OUTPUT.Default; + ssaoPass.blendIntensity = 0.65; + this.tuneSSAO(ssaoPass, 0.01); + this.ssaoPass = ssaoPass; + } + this.blitScene = new THREE.Scene(); + this.blitCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + this.copyMaterial = new THREE.MeshBasicMaterial({ map: this.beautyRT.texture, toneMapped: false, depthTest: false, depthWrite: false }); + this.presentMaterial = new THREE.MeshBasicMaterial({ map: this.faceRT.texture, toneMapped: true, depthTest: false, depthWrite: false }); + const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.copyMaterial); + quad.frustumCulled = false; + this.blitScene.add(quad); + } + + /** Draws one material over the whole current render target. */ + private blit(material: THREE.Material): void { + const quad = this.blitScene!.children[0] as THREE.Mesh; + quad.material = material; + this.renderer.render(this.blitScene!, this.blitCamera!); + } + + /** GTAO works in world units — retune the radius to the model's scale. */ + private tuneSSAO(pass: GTAOPass, modelRadius: number): void { + pass.updateGtaoMaterial({ + radius: Math.max(modelRadius * 0.14, 0.002), + distanceExponent: 1.0, + thickness: 1.0, + scale: 1.0, + samples: 16, + distanceFallOff: 1.0, + screenSpaceRadius: false, + }); + } + + private lastCameraPos = new THREE.Vector3(); + private lastCameraQuat = new THREE.Quaternion(); + + /** True when the camera pose changed since the previous call. */ + private cameraMoved(): boolean { + const changed = + !this.camera.position.equals(this.lastCameraPos) || + !this.camera.quaternion.equals(this.lastCameraQuat); + this.lastCameraPos.copy(this.camera.position); + this.lastCameraQuat.copy(this.camera.quaternion); + return changed; + } + + private render(): void { + if (!this.faceRT || !this.blitScene) return; + this.controls.update(); + // Camera pose change (damping, user input) or an explicit invalidation + // triggers the expensive SSAO chain; an idle scene skips it entirely. + if (!this.cameraMoved() && !this.needsRender) return; + this.needsRender = false; + const previousTarget = this.renderer.getRenderTarget(); + const previousAutoClear = this.renderer.autoClear; + + // 1. faces + background (layer 0) — carries the face depth buffer + this.camera.layers.set(FACE_LAYER); + this.renderer.setRenderTarget(this.faceRT); + this.renderer.clear(); + this.renderer.render(this.threeScene, this.camera); + + // 2. ambient occlusion on faces only + if (this.ssaoPass && this.ssaoEnabled && this.beautyRT) { + this.ssaoPass.render(this.renderer, this.beautyRT, this.faceRT, 0, false); + // 2b. bring the AO'ed color back onto faceRT; its depth survives because + // the copy quad does not write depth + this.renderer.setRenderTarget(this.faceRT); + this.renderer.autoClear = false; + this.copyMaterial!.map = this.beautyRT.texture; + this.blit(this.copyMaterial!); + } + + // 3. edges + annotation overlays, composited after AO with plain depth + // testing against the face depth — constant color, correctly occluded + this.camera.layers.set(EDGE_LAYER); + this.camera.layers.enable(OVERLAY_LAYER); + this.renderer.setRenderTarget(this.faceRT); + this.renderer.render(this.threeScene, this.camera); + this.camera.layers.set(FACE_LAYER); + + // 4. present through tone mapping + this.renderer.setRenderTarget(previousTarget); + this.renderer.autoClear = previousAutoClear; + this.blit(this.presentMaterial!); + this.renderGizmo(); + } + + // -- ground grid + axis gizmo --------------------------------------------------- + + /** A gray grid on the XY plane (z = 0), sized to the model. */ + private updateGrid(): void { + if (this.grid) { + this.threeScene.remove(this.grid); + this.grid.geometry.dispose(); + (this.grid.material as THREE.Material).dispose(); + this.grid = null; + } + this.modelRoot.updateMatrixWorld(true); + const box = new THREE.Box3().setFromObject(this.modelRoot); + if (box.isEmpty()) return; + const reach = Math.max(Math.abs(box.min.x), Math.abs(box.max.x), Math.abs(box.min.y), Math.abs(box.max.y), 0.02) * 1.5; + // 1-2-5 step, about eight cells from the origin to the edge + const raw = reach / 8; + const decade = Math.pow(10, Math.floor(Math.log10(raw))); + const step = decade * ([1, 2, 5, 10].find((factor) => factor * decade >= raw) ?? 10); + const count = Math.ceil(reach / step); + const extent = count * step; + const positions: number[] = []; + const colors: number[] = []; + const minor = new THREE.Color(this.palette.gridMinor); + const major = new THREE.Color(this.palette.gridMajor); + for (let index = -count; index <= count; index += 1) { + const color = index % 5 === 0 ? major : minor; + const at = index * step; + positions.push(at, -extent, 0, at, extent, 0, -extent, at, 0, extent, at, 0); + for (let vertex = 0; vertex < 4; vertex += 1) colors.push(color.r, color.g, color.b); + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + const grid = new THREE.LineSegments( + geometry, + new THREE.LineBasicMaterial({ vertexColors: true, depthWrite: false, toneMapped: false }), + ); + grid.name = 'ground-grid'; + grid.userData.pickable = false; + grid.layers.set(EDGE_LAYER); // composited after AO, depth-tested against the model + grid.renderOrder = -1; + this.threeScene.add(grid); + this.grid = grid; + this.needsRender = true; + } + + /** The three axes as blocks and letters; drawn in a corner of the canvas. */ + private buildGizmo(): void { + const dirs = [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)]; + const labels = ['X', 'Y', 'Z']; + const basic = (color: string) => new THREE.MeshBasicMaterial({ color, toneMapped: false }); + dirs.forEach((dir, index) => { + const color = this.palette.axes[index]; + const shaftMaterial = basic(color); + const tipMaterial = basic(color); + const tailMaterial = basic(this.palette.axisTail); + const shaft = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.07, 1), shaftMaterial); + shaft.position.copy(dir).multiplyScalar(0.5); + shaft.lookAt(dir); + const tip = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.2), tipMaterial); + tip.position.copy(dir); + const tail = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.12, 0.12), tailMaterial); + tail.position.copy(dir).multiplyScalar(-1); + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 64; + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + const sprite = new THREE.Sprite( + new THREE.SpriteMaterial({ map: texture, depthTest: false, toneMapped: false, transparent: true }), + ); + sprite.scale.set(0.42, 0.42, 1); + sprite.position.copy(dir).multiplyScalar(1.38); + this.gizmoAxes.push({ materials: [shaftMaterial, tipMaterial], tail: tailMaterial, canvas, texture, label: labels[index] }); + this.paintGizmoLabel(this.gizmoAxes[index], color); + this.gizmoScene.add(shaft, tip, tail, sprite); + }); + } + + private vertexMaterial(): THREE.PointsMaterial { + const material = new THREE.PointsMaterial({ color: this.palette.vertex, size: 7, sizeAttenuation: false, depthWrite: false, transparent: true, opacity: 0.48 }); + material.userData.role = 'vertex'; + return material; + } + + private paintGizmoLabel(axis: { canvas: HTMLCanvasElement; texture: THREE.CanvasTexture; label: string }, color: string): void { + const context = axis.canvas.getContext('2d')!; + context.clearRect(0, 0, 64, 64); + context.font = '700 44px ui-monospace, SFMono-Regular, Menlo, monospace'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = color; + context.fillText(axis.label, 32, 34); + axis.texture.needsUpdate = true; + } + + /** The viewport background as a linear color: sRGB design value, brightened by the + * theme's boost so the tone-mapped result is the page's own background. */ + private clearColorFor(background?: string): THREE.Color { + const color = new THREE.Color(background ?? this.palette.clear).convertSRGBToLinear(); + if (background === undefined) color.multiplyScalar(this.palette.clearBoost); + return color; + } + + /** Switch the scene to another theme in place: background, edges, grid, gizmo, vertex + * points. The camera, the model and any selection overlay stay as they are. */ + setTheme(theme: SceneTheme): void { + this.palette = PALETTES[theme]; + this.rendererClearColor = this.clearColorFor(); + this.renderer.setClearColor(this.rendererClearColor, 1); + this.modelRoot.traverse((object) => { + const material = (object as THREE.Mesh).material as THREE.Material | THREE.Material[] | undefined; + if (!material || Array.isArray(material)) return; + if (material.userData.role === 'edge') (material as LineMaterial).color.set(this.palette.edge); + else if (material.userData.role === 'vertex') (material as THREE.PointsMaterial).color.set(this.palette.vertex); + }); + this.gizmoAxes.forEach((axis, index) => { + const color = this.palette.axes[index]; + axis.materials.forEach((material) => material.color.set(color)); + axis.tail.color.set(this.palette.axisTail); + this.paintGizmoLabel(axis, color); + }); + this.updateGrid(); + this.needsRender = true; + } + + private renderGizmo(): void { + const size = this.renderer.getSize(new THREE.Vector2()); + const side = 108; + const margin = 12; + const offset = this.camera.position.clone().sub(this.controls.target).normalize().multiplyScalar(6); + this.gizmoCamera.position.copy(offset); + this.gizmoCamera.up.copy(this.camera.up); + this.gizmoCamera.lookAt(0, 0, 0); + const previousAutoClear = this.renderer.autoClear; + this.renderer.autoClear = false; + this.renderer.setRenderTarget(null); + this.renderer.setScissorTest(true); + this.renderer.setViewport(size.x - side - margin, margin, side, side); + this.renderer.setScissor(size.x - side - margin, margin, side, side); + this.renderer.clearDepth(); + this.renderer.render(this.gizmoScene, this.gizmoCamera); + this.renderer.setScissorTest(false); + this.renderer.setViewport(0, 0, size.x, size.y); + this.renderer.autoClear = previousAutoClear; + } + + // -- scene loading ------------------------------------------------------- + + private bytesFor(uri: string): Uint8Array { + const value = this.files?.[uri]; + if (!value) throw new Error(`scene file is missing: ${uri}`); + return value; + } + + private async loadGlb(uri: string): Promise { + const bytes = this.bytesFor(uri); + const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; + const gltf = await this.loader.parseAsync(buffer, ''); + return gltf.scene; + } + + private resolution(): Resolution { + return { width: this.renderer.domElement.clientWidth, height: this.renderer.domElement.clientHeight }; + } + + sidecarFor(node: SceneNode | undefined): EntitySidecar | null { + const previewed = node ? this.previewSidecars.get(node.node_id) : undefined; + if (previewed) return previewed; + if (!this.manifest || !this.files || !node?.entity_asset_id) return null; + const asset = this.manifest.entity_assets.find((item) => item.asset_id === node.entity_asset_id); + if (!asset) return null; + const cached = this.entityCache.get(asset.asset_id); + if (cached) return cached; + const parsed = JSON.parse(strFromU8(this.bytesFor(asset.uri))) as EntitySidecar; + if ((parsed.schema_version !== '1.0' && parsed.schema_version !== '2.0') || parsed.definition_id !== node.definition_id) throw new Error(`entity sidecar identity differs: ${asset.uri}`); + this.entityCache.set(asset.asset_id, parsed); + return parsed; + } + + async loadScene(files: PackageFiles): Promise { + this.needsRender = true; + const hadScene = this.modelRoot.children.length > 0; + const sceneBytes = files['scene.json']; + if (!sceneBytes) throw new Error('scene.json is missing from the scene files'); + const manifest = JSON.parse(strFromU8(sceneBytes)) as SceneManifest; + if (manifest.schema_version !== '2.0' && manifest.schema_version !== 're-mode-source-1') throw new Error(`unsupported scene schema: ${manifest.schema_version}`); + this.clearScene(); + this.files = files; + this.manifest = manifest; + const nodesByParent = new Map(); + for (const node of manifest.nodes) nodesByParent.set(node.parent_node_id, [...(nodesByParent.get(node.parent_node_id) ?? []), node]); + const build = async (parent: THREE.Object3D, parentId: string | null): Promise => { + for (const node of nodesByParent.get(parentId) ?? []) { + const object = new THREE.Group(); + object.name = node.node_id; + object.matrixAutoUpdate = false; + object.matrix.copy(placementMatrix(node.transform)); + object.visible = this.nodeVisibility.get(node.node_id) ?? true; + object.userData.nodeId = node.node_id; + object.userData.definitionId = node.definition_id; + this.nodeObjects.set(node.node_id, object); + parent.add(object); + if (node.node_kind === 'part' || node.node_kind === 'solid') { + const instance = await this.instantiateNode(node); + instance.traverse((child: THREE.Object3D) => { child.userData.nodeId = node.node_id; child.userData.definitionId = node.definition_id; }); + object.add(instance); + } + await build(object, node.node_id); + } + }; + await build(this.modelRoot, null); + this.setVertexCloudsVisible(false); + // Re-frame only on the FIRST load; watched reloads (Studio live view, + // agent artifact iterations) must keep the user's camera pose and zoom. + if (!hadScene) this.pendingFrame = true; + this.updateGrid(); + this.resize(); + } + + /** Live view fast path: build the scene straight from a tessellated render + * mesh, skipping the .scadpkg container (zip, hash verification, BREP and + * feature-graph payloads entirely). Camera pose is preserved on reloads. */ + loadLiveGeometry(payload: LiveGeometryPayload): void { + const hadScene = this.modelRoot.children.length > 0; + this.clearScene(); + this.files = {}; + this.manifest = null; + const group = new THREE.Group(); + group.name = 'live-geometry'; + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.positions.flat(), 3)); + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(payload.normals.flat(), 3)); + geometry.setIndex(payload.indices); + const [r, g, b] = DEFAULT_BASE_COLOR; + const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: new THREE.Color(r, g, b), metalness: 0.08, roughness: 0.52, side: THREE.FrontSide, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 })); + mesh.name = 'live-faces'; + mesh.layers.set(FACE_LAYER); + group.add(mesh); + group.add(this.edgeLineVisual(payload.edge_positions, 'live-edges')); + this.modelRoot.add(group); + if (!hadScene) this.pendingFrame = true; + this.updateGrid(); + this.resize(); + } + + /** Build the scene from a preview (see `lib/preview.ts`): no package, no + * sidecar files. Each part's geometry is built once and shared by its + * instances; picking and highlighting use the same face and edge groups the + * package path has. Entity details are not part of a preview: a pick returns + * the entity without properties and the caller asks for them. */ + loadPreview(preview: PreviewData): void { + this.needsRender = true; + const hadScene = this.modelRoot.children.length > 0; + this.clearScene(); + this.files = {}; + const nodes: SceneNode[] = []; + const built = new Map(); + const material = (part: PreviewPart) => part && materialFor({ material_id: null } as SceneNode); + const buildPart = (part: PreviewPart) => { + const faces = new THREE.BufferGeometry(); + faces.setAttribute('position', new THREE.BufferAttribute(part.positions, 3)); + faces.setAttribute('normal', new THREE.BufferAttribute(part.normals, 3)); + faces.setIndex(new THREE.BufferAttribute(part.indices, 1)); + faces.computeBoundingSphere(); + const pick = new THREE.BufferGeometry(); + pick.setAttribute('position', new THREE.BufferAttribute(part.edgePositions, 3)); + pick.setIndex(new THREE.BufferAttribute(part.edgeIndices, 1)); + const segments: number[] = []; + for (let offset = 0; offset + 1 < part.edgeIndices.length; offset += 2) { + const a = part.edgeIndices[offset] * 3; + const b = part.edgeIndices[offset + 1] * 3; + segments.push( + part.edgePositions[a], part.edgePositions[a + 1], part.edgePositions[a + 2], + part.edgePositions[b], part.edgePositions[b + 1], part.edgePositions[b + 2], + ); + } + const edges = new LineSegmentsGeometry(); + if (segments.length) edges.setPositions(segments); + let vertices: THREE.BufferGeometry | null = null; + if (part.vertexIds.length) { + vertices = new THREE.BufferGeometry(); + vertices.setAttribute('position', new THREE.BufferAttribute(part.vertexPositions, 3)); + } + const empty = { properties: {}, geometry: {}, tags: [] as string[], parent_entity_ids: [] as string[], child_entity_ids: [] as string[], source: { kind: 'preview' } }; + const entity = (id: string, kind: 'solid' | 'face' | 'edge' | 'vertex') => ({ entity_id: id, kind, topo_id: id.split('/').pop() ?? id, ...empty }); + const sidecar: EntitySidecar = { + schema_version: '2.0', + definition_id: part.definitionId, + geometry_asset_id: '', + edge_asset_id: '', + entities: [ + entity(part.solidEntityId, 'solid'), + ...part.faceGroups.map((group) => entity(group.entity_id, 'face')), + ...part.edgeGroups.map((group) => entity(group.entity_id, 'edge')), + ...part.vertexIds.map((id) => entity(id, 'vertex')), + ], + face_groups: part.faceGroups, + edge_groups: part.edgeGroups, + }; + this.previewGeometries.push(faces, pick, edges, ...(vertices ? [vertices] : [])); + return { faces, pick, edges, vertices, sidecar }; + }; + const edgeMaterial = createEdgeMaterial(this.palette.edge); + const size = this.resolution(); + edgeMaterial.resolution.set(Math.max(size.width, 1), Math.max(size.height, 1)); + for (const item of preview.nodes) { + const part = preview.parts.get(item.partKey); + if (!part) continue; + let geometry = built.get(part.key); + if (!geometry) { + geometry = buildPart(part); + built.set(part.key, geometry); + } + const node = { + node_id: item.nodeId, parent_node_id: null, display_name: item.displayName, node_kind: 'part', definition_id: item.definitionId, + definition_kind: 'single_solid', component_id: null, instance_id: null, transform: item.transform, geometry_asset_id: null, + entity_asset_id: null, material_id: null, properties: {}, + } as SceneNode; + nodes.push(node); + this.previewSidecars.set(item.nodeId, geometry.sidecar); + const object = new THREE.Group(); + object.name = item.nodeId; + object.matrixAutoUpdate = false; + object.matrix.copy(placementMatrix(item.transform)); + object.userData.nodeId = item.nodeId; + object.userData.definitionId = item.definitionId; + const group = new THREE.Group(); + group.name = item.displayName; + const faces = new THREE.Mesh(geometry.faces, material(part)); + faces.layers.set(FACE_LAYER); + group.add(faces); + // the invisible indexed line set is what edge picking hits; the visual hangs off it + const pickEdges = new THREE.LineSegments(geometry.pick, new THREE.LineBasicMaterial({ visible: false })); + if (geometry.edges.attributes.instanceStart) { + const visual = new LineSegments2(geometry.edges, edgeMaterial); + visual.name = 'cad-edge-visual'; + visual.layers.set(EDGE_LAYER); + visual.userData.pickable = false; + pickEdges.add(visual); + } + group.add(pickEdges); + if (geometry.vertices) { + const points = new THREE.Points(geometry.vertices, this.vertexMaterial()); + points.visible = false; + points.userData.pickRole = 'vertex'; + points.userData.pickable = true; + points.userData.vertexEntityIds = part.vertexIds; + group.add(points); + } + group.traverse((child: THREE.Object3D) => { + child.userData.nodeId = item.nodeId; + child.userData.definitionId = item.definitionId; + }); + object.add(group); + this.nodeObjects.set(item.nodeId, object); + this.modelRoot.add(object); + } + this.manifest = { schema_version: '2.0', nodes } as unknown as SceneManifest; + this.setVertexCloudsVisible(false); + if (!hadScene) this.pendingFrame = true; + this.updateGrid(); + this.resize(); + } + + /** True while the scene on screen is a preview (entity details are asked for, not read). */ + get isPreview(): boolean { + return this.previewSidecars.size > 0; + } + + private async instantiateNode(node: SceneNode): Promise { + if (!this.manifest) throw new Error('scene is not loaded'); + const sidecar = this.sidecarFor(node); + const group = new THREE.Group(); + group.name = node.display_name || node.node_id; + const geometryAsset = node.geometry_asset_id ? this.manifest.geometry_assets.find((asset) => asset.asset_id === node.geometry_asset_id) : undefined; + if (geometryAsset) { + let geometry = this.geometryCache.get(geometryAsset.asset_id); + if (!geometry) { + geometry = await this.loadGlb(geometryAsset.uri); + this.geometryCache.set(geometryAsset.asset_id, geometry); + } + const renderGeometry = geometry.clone(true); + renderGeometry.traverse((child) => { + if (child instanceof THREE.Mesh) child.material = materialFor(node); + }); + group.add(renderGeometry); + } + const edgeAsset = sidecar ? this.manifest.geometry_assets.find((asset) => asset.asset_id === sidecar.edge_asset_id) : undefined; + if (edgeAsset && sidecar) { + let edge = this.edgeCache.get(edgeAsset.asset_id); + if (!edge) { + edge = await this.loadGlb(edgeAsset.uri); + this.edgeCache.set(edgeAsset.asset_id, edge); + } + const edgeInstance = edge.clone(true); + this.buildEdgeVisuals(edgeInstance, sidecar); + group.add(edgeInstance); + } + if (sidecar) { + const vertices = sidecar.entities.filter((entity) => entity.kind === 'vertex'); + if (vertices.length) { + const positions = new Float32Array(vertices.length * 3); + vertices.forEach((entity, index) => { + const position = entity.properties.position; + if (Array.isArray(position) && position.length === 3 && position.every((item) => typeof item === 'number')) cadPointToGltf(position as Vec3).toArray(positions, index * 3); + }); + const vertexGeometry = new THREE.BufferGeometry(); + vertexGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + const vertexPoints = new THREE.Points(vertexGeometry, this.vertexMaterial()); + vertexPoints.visible = false; + vertexPoints.userData.pickRole = 'vertex'; + vertexPoints.userData.pickable = true; + vertexPoints.userData.vertexEntityIds = vertices.map((entity) => entity.entity_id); + group.add(vertexPoints); + } + } + return group; + } + + /** Split the edge asset into regular and seam/degenerate wide-line visuals. */ + private buildEdgeVisuals(edgeObject: THREE.Object3D, sidecar: EntitySidecar): void { + const entitiesById = new Map(sidecar.entities.map((entity) => [entity.entity_id, entity])); + edgeObject.traverse((child) => { + if (!(child instanceof THREE.LineSegments)) return; + const source = child as THREE.LineSegments; + const index = source.geometry.index; + const position = source.geometry.getAttribute('position'); + const indexCount = index?.count ?? position.count; + const groups = [...sidecar.edge_groups].sort((a, b) => a.first_index - b.first_index); + const regular: number[] = []; + const seams: number[] = []; + const pushRange = (target: number[], first: number, count: number): void => { + for (let offset = first; offset + 1 < first + count; offset += 2) { + const a = index ? index.getX(offset) : offset; + const b = index ? index.getX(offset + 1) : offset + 1; + target.push(position.getX(a), position.getY(a), position.getZ(a), position.getX(b), position.getY(b), position.getZ(b)); + } + }; + let cursor = 0; + for (const group of groups) { + const start = Math.max(group.first_index, 0); + if (start > cursor) pushRange(regular, cursor, start - cursor); + const target = isHiddenEdge(entitiesById.get(group.entity_id)) ? seams : regular; + pushRange(target, start, group.index_count); + cursor = Math.max(cursor, start + group.index_count); + } + if (cursor < indexCount) pushRange(regular, cursor, indexCount - cursor); + // the source keeps the original geometry for index-based picking only + source.material = new THREE.LineBasicMaterial({ visible: false }); + if (regular.length) source.add(this.edgeLineVisual(regular, 'cad-edge-visual')); + if (seams.length) { + const seamVisual = this.edgeLineVisual(seams, 'cad-edge-seam'); + seamVisual.visible = this.seamsVisible && this.edgesVisible; + source.add(seamVisual); + } + }); + } + + private edgeLineVisual(positions: number[], name: string): LineSegments2 { + const geometry = new LineSegmentsGeometry(); + geometry.setPositions(positions); + // Negative slope-scaled offset mirrors the faces' push (see materialFor): + // keeps silhouette edges continuous where a flat bias loses. + const material = createEdgeMaterial(this.palette.edge); + const size = this.resolution(); + material.resolution.set(Math.max(size.width, 1), Math.max(size.height, 1)); + const visual = new LineSegments2(geometry, material); + visual.name = name; + visual.layers.set(EDGE_LAYER); + visual.userData.pickable = false; + return visual; + } + + clearScene(): void { + for (const object of this.geometryCache.values()) disposeObject(object); + for (const object of this.edgeCache.values()) disposeObject(object); + while (this.modelRoot.children.length) { + const child = this.modelRoot.children[0]; + disposeObject(child); + this.modelRoot.remove(child); + } + for (const geometry of this.previewGeometries) geometry.dispose(); + this.previewGeometries = []; + this.previewSidecars.clear(); + this.nodeObjects.clear(); + this.nodeVisibility.clear(); + this.edgeCache.clear(); + this.entityCache.clear(); + this.geometryCache.clear(); + this.manifest = null; + this.files = null; + } + + // -- display options ------------------------------------------------------- + + setEdgesVisible(visible: boolean): void { + this.edgesVisible = visible; + this.modelRoot.traverse((object) => { + if (object.name === 'cad-edge-visual') object.visible = visible; + if (object.name === 'cad-edge-seam') object.visible = visible && this.seamsVisible; + }); + } + + setSeamsVisible(visible: boolean): void { + this.seamsVisible = visible; + this.modelRoot.traverse((object) => { + if (object.name === 'cad-edge-seam') object.visible = visible && this.edgesVisible; + }); + } + + get seamsShown(): boolean { + return this.seamsVisible; + } + + setSSAO(enabled: boolean): void { + if (!this.ssaoPass || this.ssaoEnabled === enabled) return; + this.ssaoEnabled = enabled; + this.ssaoPass.enabled = enabled; + } + + get ssaoOn(): boolean { + return this.ssaoEnabled && this.ssaoPass !== null; + } + + setVertexCloudsVisible(visible: boolean): void { + this.modelRoot.traverse((child) => { + if (child.userData.pickRole === 'vertex') child.visible = visible; + }); + } + + setNodeVisible(nodeId: string, visible: boolean): void { + const object = this.nodeObjects.get(nodeId); + if (!object) return; + object.visible = visible; + this.nodeVisibility.set(nodeId, visible); + } + + // -- accessors ------------------------------------------------------------- + + get nodes(): SceneNode[] { + return this.manifest?.nodes ?? []; + } + + get domElement(): HTMLCanvasElement { + return this.renderer.domElement; + } + + nodeObject(nodeId: string): THREE.Group | undefined { + return this.nodeObjects.get(nodeId); + } + + entityFor(nodeId: string, entityId: string): Entity | null { + const node = this.manifest?.nodes.find((item) => item.node_id === nodeId); + return this.sidecarFor(node)?.entities.find((item) => item.entity_id === entityId) ?? null; + } + + // -- camera + lifecycle -------------------------------------------------------- + + frame(): void { + this.needsRender = true; + const box = new THREE.Box3().setFromObject(this.modelRoot); + if (box.isEmpty()) return; + const center = box.getCenter(new THREE.Vector3()); + const size = box.getSize(new THREE.Vector3()); + // The 0.0005 floor only guards degenerate/empty geometry; it must stay + // well below real model radii (a 0.01 floor pushed small parts 3.6x away). + const radius = Math.max(size.length() * 0.5, 0.0005); + // FIT is also the escape hatch from a rolled-around trackball orientation: + // restore the canonical Z-up before re-framing. + this.camera.up.set(0, 0, 1); + // 2.3 (not the textbook ~2.8, nor 2.05): the bounding sphere over-covers + // flat parts, but 2.05 read as slightly too large in review. + // Fit the bounding sphere in the narrower of the two view angles, so a + // tall, narrow viewport frames the model as well as a wide one. + const verticalFov = THREE.MathUtils.degToRad(this.camera.fov); + const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * this.camera.aspect); + const distance = (radius / Math.sin(Math.min(verticalFov, horizontalFov) / 2)) * 0.82; + this.camera.position.copy(center).add(new THREE.Vector3(1, 0.78, 1).normalize().multiplyScalar(distance)); + this.camera.near = Math.max(radius / 100, 1e-5); + this.camera.far = Math.max(radius * 100, 0.5); + this.camera.updateProjectionMatrix(); + this.controls.target.copy(center); + // Absolute minDistance (0.01 in the constructor) would clamp the zoomed-in + // camera for small models; frame re-derives both bounds from the radius. + this.controls.minDistance = radius * 0.2; + this.controls.maxDistance = radius * 12; + // Kill the trackball's rotational inertia — otherwise it keeps rolling the + // freshly framed camera away from the canonical pose. These internals are + // stable across three.js versions; there is no public "stop momentum" API. + const trackball = this.controls as unknown as { + _movePrev: THREE.Vector2; _moveCurr: THREE.Vector2; _lastAngle: number; + _zoomStart: THREE.Vector2; _zoomEnd: THREE.Vector2; + _panStart: THREE.Vector2; _panEnd: THREE.Vector2; + }; + trackball._movePrev.copy(trackball._moveCurr); + trackball._lastAngle = 0; + trackball._zoomStart.copy(trackball._zoomEnd); + trackball._panStart.copy(trackball._panEnd); + this.controls.update(); + if (this.ssaoPass) this.tuneSSAO(this.ssaoPass, radius); + } + + getCameraState(): CameraState { + const bounds = this.renderer.domElement.getBoundingClientRect(); + return { + position: [this.camera.position.x, this.camera.position.y, this.camera.position.z], + target: [this.controls.target.x, this.controls.target.y, this.controls.target.z], + up: [this.camera.up.x, this.camera.up.y, this.camera.up.z], + fov_deg: this.camera.fov, + width: Math.round(bounds.width), + height: Math.round(bounds.height), + }; + } + + snapshotPng(): string | null { + try { + this.render(); + return this.renderer.domElement.toDataURL('image/png'); + } catch { + return null; + } + } + + private resize(): void { + const width = this.host.clientWidth; + const height = this.host.clientHeight; + if (width <= 0 || height <= 0) return; + this.renderer.setSize(width, height, false); + // TrackballControls maps pointer deltas through the element rect, which + // this host changes on load and panel resizes. + this.controls.handleResize(); + this.camera.aspect = width / Math.max(height, 1); + this.camera.updateProjectionMatrix(); + const pixelRatio = this.renderer.getPixelRatio(); + const deviceWidth = Math.max(Math.round(width * pixelRatio), 1); + const deviceHeight = Math.max(Math.round(height * pixelRatio), 1); + this.faceRT?.setSize(deviceWidth, deviceHeight); + this.beautyRT?.setSize(deviceWidth, deviceHeight); + this.ssaoPass?.setSize(deviceWidth, deviceHeight); + // The scene may load before the host has its final layout; re-frame once. + if (this.pendingFrame && this.modelRoot.children.length) { + this.pendingFrame = false; + this.frame(); + } + const resolution = this.resolution(); + this.modelRoot.traverse((object) => { + if (object instanceof LineSegments2) object.material.resolution.set(resolution.width, resolution.height); + }); + // setSize cleared the canvas. + this.needsRender = true; + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.observer.disconnect(); + this.renderer.setAnimationLoop(null); + this.clearScene(); + if (this.grid) { + this.grid.geometry.dispose(); + (this.grid.material as THREE.Material).dispose(); + } + this.gizmoScene.traverse((object) => { + const mesh = object as THREE.Mesh; + mesh.geometry?.dispose?.(); + (mesh.material as THREE.Material | undefined)?.dispose?.(); + }); + this.controls.dispose(); + this.faceRT?.dispose(); + this.beautyRT?.dispose(); + this.ssaoPass?.dispose?.(); + if (this.blitScene) { + const quad = this.blitScene.children[0] as THREE.Mesh | undefined; + quad?.geometry.dispose(); + this.copyMaterial?.dispose(); + this.presentMaterial?.dispose(); + } + this.renderer.dispose(); + this.renderer.domElement.remove(); + } +} diff --git a/editor-web/src/scene/renderer/index.ts b/editor-web/src/scene/renderer/index.ts new file mode 100644 index 00000000..fd63aa2e --- /dev/null +++ b/editor-web/src/scene/renderer/index.ts @@ -0,0 +1,17 @@ +// Standalone BRep renderer component — public entry point. +// +// Embed this to render a scene-2.0 package (faces + edges) with CAD-grade +// presentation: image-based studio lighting, GTAO ambient occlusion, free +// trackball camera, and toggleable seam/degenerate edge display. Selection +// and annotation are NOT part of this component; build them on top +// (reference implementation: src/scene-view.ts). + +export { + BRepRenderer, + EDGE_LAYER, + FACE_LAYER, + OVERLAY_LAYER, + type BRepRendererOptions, + type CameraState, + type LiveGeometryPayload, +} from './brep-renderer'; diff --git a/editor-web/src/scene/scene-view.ts b/editor-web/src/scene/scene-view.ts new file mode 100644 index 00000000..dac278b9 --- /dev/null +++ b/editor-web/src/scene/scene-view.ts @@ -0,0 +1,374 @@ +// Interactive BRep selection view used by the reverse-engineering mode. +// A thin selection/annotation layer on top of the standalone BRepRenderer +// component (src/renderer): this class adds raycast picking, selection +// modes and annotation marks. All rendering — materials, edge display, +// lighting, ambient occlusion, camera — lives in the renderer component. + +import type { SceneTheme } from './theme'; +import * as THREE from 'three'; +import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; +import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; +import { bindClickSelection } from './components/click-selection'; +import { disposeObject } from './cad-three'; +import { BRepRenderer, OVERLAY_LAYER, type CameraState, type LiveGeometryPayload } from './renderer'; + +export type { LiveGeometryPayload }; +import type { PackageFiles } from './product-package'; +import type { PreviewData } from '../lib/preview'; +import type { Entity, EntitySidecar, FaceGroup, SceneNode } from './scene2'; + +export type SelectionMode = 'component' | 'solid' | 'face' | 'edge' | 'vertex'; +export type PickResult = { + nodeId: string; + entityId: string | null; + mode: SelectionMode; + entity: Entity | null; + /** Ctrl/Cmd was held: add to (or toggle in) the selection instead of replacing it. */ + additive?: boolean; +}; +export type { CameraState }; + +export const MARK_COLORS = ['#ff5c5c', '#ffd23f', '#4ade80', '#60a5fa', '#c084fc', '#fb923c', '#f472b6', '#34d399']; + +export type SceneViewOptions = { + onPick?: (result: PickResult) => void; + /** A click that hit nothing. */ + onClear?: () => void; + selectionMode?: SelectionMode; + background?: string; + /** The scene's theme (default dark). */ + theme?: SceneTheme; + interactive?: boolean; + /** GTAO ambient occlusion (default on). */ + ssao?: boolean; + /** Show seam/degenerate edges (default off). */ + seams?: boolean; +}; + +type Mark = { key: string; color: string; overlays: THREE.Object3D[] }; + +export class SceneView { + readonly engine: BRepRenderer; + onPick: ((result: PickResult) => void) | null; + onClear: (() => void) | null; + + private raycaster = new THREE.Raycaster(); + private pointer = new THREE.Vector2(); + selectionMode: SelectionMode; + marks: Mark[] = []; + private unbindClick: (() => void) | null = null; + private disposed = false; + + constructor(host: HTMLElement, options: SceneViewOptions = {}) { + this.onPick = options.onPick ?? null; + this.onClear = options.onClear ?? null; + this.selectionMode = options.selectionMode ?? 'face'; + this.engine = new BRepRenderer(host, { background: options.background, theme: options.theme, ssao: options.ssao, seams: options.seams }); + if (options.interactive !== false) { + this.unbindClick = bindClickSelection({ + element: this.engine.domElement, + camera: this.engine.camera, + controls: this.engine.controls, + onClick: (event) => { + const result = this.pick(event); + if (result) this.onPick?.({ ...result, additive: event.ctrlKey || event.metaKey }); + else if (!(event.ctrlKey || event.metaKey)) this.onClear?.(); + }, + }); + } + } + + // -- scene loading --------------------------------------------------------- + + async loadScene(files: PackageFiles): Promise { + this.clearMarks(); + await this.engine.loadScene(files); + this.applySelectionModeVisibility(); + } + + loadPreview(preview: PreviewData): void { + this.clearMarks(); + this.engine.loadPreview(preview); + this.applySelectionModeVisibility(); + } + + get isPreview(): boolean { + return this.engine.isPreview; + } + + loadLiveGeometry(payload: LiveGeometryPayload): void { + this.clearMarks(); + this.engine.loadLiveGeometry(payload); + } + + /** Pause/resume the render loop (inactive editor modes must not render). */ + setRunning(running: boolean): void { + this.engine.setRunning(running); + } + + clearScene(): void { + this.clearMarks(); + this.engine.clearScene(); + } + + sidecarFor(node: SceneNode | undefined): EntitySidecar | null { + return this.engine.sidecarFor(node); + } + + entityFor(nodeId: string, entityId: string): Entity | null { + return this.engine.entityFor(nodeId, entityId); + } + + get nodes(): SceneNode[] { + return this.engine.nodes; + } + + nodeObject(nodeId: string): THREE.Group | undefined { + return this.engine.nodeObject(nodeId); + } + + // -- picking --------------------------------------------------------------- + + setSelectionMode(mode: SelectionMode): void { + this.selectionMode = mode; + this.applySelectionModeVisibility(); + } + + private applySelectionModeVisibility(): void { + this.engine.setVertexCloudsVisible(this.selectionMode === 'vertex'); + } + + private isEffectivelyVisible(object: THREE.Object3D): boolean { + for (let current: THREE.Object3D | null = object; current; current = current.parent) { + if (!current.visible) return false; + if (current === this.engine.modelRoot) break; + } + return true; + } + + private pointerRay(event: PointerEvent): void { + const bounds = this.engine.domElement.getBoundingClientRect(); + this.pointer.x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1; + this.pointer.y = -((event.clientY - bounds.top) / bounds.height) * 2 + 1; + this.raycaster.setFromCamera(this.pointer, this.engine.camera); + const distance = this.engine.camera.position.distanceTo(this.engine.controls.target); + const worldPerPixel = 2 * Math.tan(THREE.MathUtils.degToRad(this.engine.camera.fov * 0.5)) * distance / Math.max(bounds.height, 1); + this.raycaster.params.Line.threshold = worldPerPixel * 5; + this.raycaster.params.Points.threshold = worldPerPixel * 8; + } + + /** A hidden seam/degenerate edge must not answer edge picks. */ + private isEdgePickable(entityId: string | undefined, sidecar: EntitySidecar | null): boolean { + if (this.engine.seamsShown) return true; + const entity = sidecar?.entities.find((item) => item.entity_id === entityId); + const props = entity?.properties as { seam?: unknown; degenerate?: unknown } | undefined; + return props?.seam !== true && props?.degenerate !== true; + } + + pick(event: PointerEvent): PickResult | null { + this.pointerRay(event); + const hits = this.raycaster.intersectObjects(this.engine.modelRoot.children, true).filter((hit) => { + if (hit.object.userData.pickable === false || !this.isEffectivelyVisible(hit.object)) return false; + return typeof hit.object.userData.nodeId === 'string'; + }); + const meshHit = hits.find((hit) => hit.object instanceof THREE.Mesh); + if (this.selectionMode === 'component' || this.selectionMode === 'solid' || this.selectionMode === 'face') { + if (!meshHit) return null; + } else { + const wanted = this.selectionMode === 'edge' + ? (hit: THREE.Intersection) => hit.object instanceof THREE.LineSegments + : (hit: THREE.Intersection) => hit.object instanceof THREE.Points; + let candidate: THREE.Intersection | undefined; + for (const hit of hits.filter(wanted)) { + if (this.selectionMode === 'edge') { + const nodeId = hit.object.userData.nodeId as string; + const node = this.engine.nodes.find((item) => item.node_id === nodeId); + const sidecar = this.engine.sidecarFor(node); + const entityId = this.edgeEntityIdForHit(hit, sidecar); + if (!this.isEdgePickable(entityId, sidecar)) continue; + } + candidate = hit; + break; + } + if (!candidate) return null; + if (meshHit) { + const occlusionAllowance = this.selectionMode === 'edge' ? this.raycaster.params.Line.threshold : this.raycaster.params.Points.threshold; + if (candidate.distance > meshHit.distance + occlusionAllowance * 1.5) return null; + } + } + const hit = (this.selectionMode === 'edge' || this.selectionMode === 'vertex') + ? hits.find((item) => this.selectionMode === 'edge' ? item.object instanceof THREE.LineSegments : item.object instanceof THREE.Points) ?? meshHit + : meshHit; + if (!hit) return null; + const nodeId = hit.object.userData.nodeId as string; + const node = this.engine.nodes.find((item) => item.node_id === nodeId); + const sidecar = this.sidecarFor(node); + if (this.selectionMode === 'component') { + return { nodeId, entityId: null, mode: 'component', entity: null }; + } + if (this.selectionMode === 'solid') { + const solid = sidecar?.entities.find((item) => item.kind === 'solid') ?? null; + return { nodeId, entityId: solid?.entity_id ?? null, mode: 'solid', entity: solid }; + } + let entityId: string | undefined; + if (this.selectionMode === 'face' && hit.object instanceof THREE.Mesh && typeof hit.faceIndex === 'number') { + const triangleOffset = hit.faceIndex * 3; + entityId = sidecar?.face_groups.find((group: FaceGroup) => triangleOffset >= group.first_index && triangleOffset < group.first_index + group.index_count)?.entity_id; + } else if (this.selectionMode === 'edge' && hit.object instanceof THREE.LineSegments && typeof hit.index === 'number') { + entityId = this.edgeEntityIdForHit(hit, sidecar); + } else if (this.selectionMode === 'vertex' && hit.object instanceof THREE.Points && typeof hit.index === 'number') { + entityId = hit.object.userData.vertexEntityIds?.[hit.index]; + } + if (!entityId) return null; + return { nodeId, entityId, mode: this.selectionMode, entity: this.entityFor(nodeId, entityId) }; + } + + private edgeEntityIdForHit(hit: THREE.Intersection, sidecar: EntitySidecar | null): string | undefined { + if (!(hit.object instanceof THREE.LineSegments) || typeof hit.index !== 'number' || !sidecar) return undefined; + return sidecar.edge_groups.find((group: FaceGroup) => hit.index! >= group.first_index && hit.index! < group.first_index + group.index_count)?.entity_id; + } + + // -- highlight + marks ------------------------------------------------------- + + private attachOverlay(source: THREE.Object3D, overlay: THREE.Object3D): THREE.Object3D { + this.engine.invalidate(); + overlay.name = 'entity-overlay'; + overlay.userData.pickable = false; + // Overlays composite in the post-AO edge pass (see BRepRenderer): their + // color must not be darkened by ambient occlusion. + overlay.layers.set(OVERLAY_LAYER); + overlay.renderOrder = 10; + overlay.matrix.copy(source.matrix); + overlay.matrixAutoUpdate = false; + overlay.visible = source.visible; + source.parent?.add(overlay); + return overlay; + } + + buildEntityOverlays(nodeId: string, entityId: string | null, color: string, opacity = 0.72): THREE.Object3D[] { + const object = this.engine.nodeObject(nodeId); + const node = this.engine.nodes.find((item) => item.node_id === nodeId); + const sidecar = this.engine.sidecarFor(node); + if (!object || !sidecar) return []; + const overlays: THREE.Object3D[] = []; + const entityGroup = entityId ? sidecar.face_groups.find((group: FaceGroup) => group.entity_id === entityId) : undefined; + const edgeGroup = entityId ? sidecar.edge_groups.find((group: FaceGroup) => group.entity_id === entityId) : undefined; + // Entity ids carry the owning graph segment (entity//solid/x); + // classify by path SEGMENT, never by a positional prefix. + const kindOf = (id: string | null | undefined) => + id?.split('/').find((part) => part === 'solid' || part === 'face' || part === 'edge' || part === 'vertex'); + const isSolid = kindOf(entityId) === 'solid'; + const isVertex = kindOf(entityId) === 'vertex'; + object.traverse((child: THREE.Object3D) => { + if (overlays.length || !this.isEffectivelyVisible(child) || !(child instanceof THREE.Mesh || child instanceof THREE.LineSegments || child instanceof THREE.Points)) return; + const source = child as THREE.Mesh | THREE.LineSegments | THREE.Points; + const range = entityGroup || edgeGroup; + if (!range && !isVertex && !isSolid) return; + if (entityGroup && !(source instanceof THREE.Mesh)) return; + if (edgeGroup && !entityGroup && !(source instanceof THREE.LineSegments)) return; + if (isVertex && !source.userData.vertexEntityIds?.includes(entityId!)) return; + if (source instanceof THREE.LineSegments && range) { + const lineGeometry = new LineSegmentsGeometry(); + const positions: number[] = []; + const index = source.geometry.index; + const position = source.geometry.getAttribute('position'); + for (let offset = range.first_index; offset < range.first_index + range.index_count; offset += 2) { + const a = index ? index.getX(offset) : offset; + const b = index ? index.getX(offset + 1) : offset + 1; + positions.push(position.getX(a), position.getY(a), position.getZ(a), position.getX(b), position.getY(b), position.getZ(b)); + } + lineGeometry.setPositions(positions); + const material = new LineMaterial({ color, linewidth: 5, worldUnits: false, transparent: true, opacity: 1, depthTest: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }); + material.resolution.set(this.engine.domElement.clientWidth, this.engine.domElement.clientHeight); + overlays.push(this.attachOverlay(source, new LineSegments2(lineGeometry, material))); + } else if (source instanceof THREE.Points) { + const geometry = source.geometry.clone(); + const vertexIndex = source.userData.vertexEntityIds.indexOf(entityId!); + geometry.setDrawRange(vertexIndex, 1); + overlays.push(this.attachOverlay(source, new THREE.Points(geometry, new THREE.PointsMaterial({ color, size: 16, sizeAttenuation: false, transparent: true, opacity: 1, depthTest: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 })))); + } else { + const geometry = source.geometry.clone(); + if (range) geometry.setDrawRange(range.first_index, range.index_count); + // Depth-tested so highlights hide behind the body like real geometry; + // the polygon offset keeps the overlay from z-fighting its own face. + overlays.push(this.attachOverlay(source, new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color, transparent: true, opacity: isSolid ? opacity * 0.55 : opacity, depthTest: true, depthWrite: false, side: THREE.DoubleSide, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 })))); + } + }); + return overlays; + } + + addMark(nodeId: string, entityId: string | null, color: string): void { + const key = `${nodeId}::${entityId ?? 'component'}`; + if (this.marks.some((mark) => mark.key === key)) return; + const overlays = this.buildEntityOverlays(nodeId, entityId, color); + if (overlays.length) this.marks.push({ key, color, overlays }); + } + + removeMark(key: string): void { + const index = this.marks.findIndex((mark) => mark.key === key); + if (index < 0) return; + for (const overlay of this.marks[index].overlays) { + overlay.parent?.remove(overlay); + disposeObject(overlay); + } + this.marks.splice(index, 1); + } + + clearMarks(): void { + this.engine.invalidate(); + for (const mark of this.marks) { + for (const overlay of mark.overlays) { + overlay.parent?.remove(overlay); + disposeObject(overlay); + } + } + this.marks = []; + } + + hasMark(nodeId: string, entityId: string | null): boolean { + return this.marks.some((mark) => mark.key === `${nodeId}::${entityId ?? 'component'}`); + } + + // -- display options + camera + lifecycle -------------------------------------- + + setEdgesVisible(visible: boolean): void { + this.engine.invalidate(); + this.engine.setEdgesVisible(visible); + } + + setSeamsVisible(visible: boolean): void { + this.engine.invalidate(); + this.engine.setSeamsVisible(visible); + } + + /** Recolor the scene for another theme without reloading it. */ + setTheme(theme: SceneTheme): void { + this.engine.setTheme(theme); + } + + setSSAO(enabled: boolean): void { + this.engine.invalidate(); + this.engine.setSSAO(enabled); + } + + frame(): void { + this.engine.invalidate(); + this.engine.frame(); + } + + getCameraState(): CameraState { + return this.engine.getCameraState(); + } + + snapshotPng(): string | null { + return this.engine.snapshotPng(); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.unbindClick?.(); + this.engine.dispose(); + } +} diff --git a/editor-web/src/scene/scene2.ts b/editor-web/src/scene/scene2.ts new file mode 100644 index 00000000..7b07aa22 --- /dev/null +++ b/editor-web/src/scene/scene2.ts @@ -0,0 +1,246 @@ +import { strFromU8 } from 'fflate'; +import type { PackageFiles } from './product-package'; + +export type Vec3 = [number, number, number]; +export type Transform = { origin: Vec3; x_axis: Vec3; y_axis: Vec3; z_axis: Vec3 }; +export type SourceRecord = Record & { + kind: string; + definition_id?: string; + graph_id?: string; + node_id?: string; + output_slot?: number; + topology_kind?: string; + topo_id?: string; +}; +export type Definition = { + definition_id: string; + definition_kind: 'single_solid' | 'assembly'; + revision: string; + content_hash: string; + product_asset_id: string; + feature_graph_asset_id: string; +}; +export type SceneNode = { + node_id: string; + parent_node_id: string | null; + display_name: string; + node_kind: 'part' | 'assembly' | 'solid'; + definition_id: string; + definition_kind: 'single_solid' | 'assembly'; + component_id: string | null; + instance_id: string | null; + transform: Transform; + geometry_asset_id: string | null; + entity_asset_id: string | null; + material_id: string | null; + properties: Record; +}; +export type Asset = { + asset_id: string; + uri: string; + media_type: string; + sha256: string; + byte_length: number; +}; +export type Entity = { + entity_id: string; + kind: 'solid' | 'face' | 'edge' | 'vertex'; + topo_id: string; + parent_entity_ids: string[]; + child_entity_ids: string[]; + source: SourceRecord; + properties: Record; + geometry: Record; + tags: string[]; +}; +export type EntitySidecar = { + schema_version: '1.0' | '2.0'; + definition_id: string; + geometry_asset_id: string; + edge_asset_id: string; + geometry_engine?: Record; + entities: Entity[]; + face_groups: FaceGroup[]; + edge_groups: FaceGroup[]; +}; +export type FaceGroup = { entity_id: string; first_index: number; index_count: number }; +export type SourceAsset = { + source_asset_id: string; + uri: string; + sha256: string; + byte_length: number; + media_type: string; + display_name: string; +}; +export type SourceSpan = { + source_asset_id: string; + uri: string; + start_line: number; + end_line: number; + symbol: string | null; +}; +export type Feature = { + feature_id: string; + definition_id: string; + graph_id: string; + node_id: string; + op: string; + label: string | null; + parameters: Record; + input_feature_ids: string[]; + output_count: number; + source_spans: SourceSpan[]; +}; +export type ConnectorSnapshot = { + connector_snapshot_id: string; + node_id: string; + definition_id: string; + definition_kind: 'single_solid' | 'assembly'; + connector_id: string; + name: string; + anchor_kind: 'placement' | 'geometry' | 'public'; + local_frame: Transform; + binding: Record | null; + source_connector_snapshot_id: string | null; + source_feature_id: string | null; +}; +export type ConnectorRef = { + definition_id: string; + component_id: string; + instance_id: string; + connector_id: string; + connector_snapshot_id: string; +}; +export type Joint = { + joint_id: string; + assembly_definition_id: string; + joint_type: 'fixed' | 'revolute' | 'prismatic' | 'cylindrical' | 'planar' | 'ball' | 'gear' | 'belt' | 'rack_pinion'; + connector_a: ConnectorRef; + connector_b: ConnectorRef; + parameters: Record; + limits: Record | null; + source_feature_id: string | null; +}; +export type SceneManifest = { + schema_version: '2.0' | 're-mode-source-1'; + artifact_kind?: 're_mode_source_scene'; + scene_id: string; + revision: string; + units: 'mm'; + roots: string[]; + definitions: Definition[]; + geometry_assets: Asset[]; + entity_assets: Asset[]; + product_assets: Array, + feature_graph_assets: Array; + source_assets: SourceAsset[]; + nodes: SceneNode[]; + connectors: ConnectorSnapshot[]; + joints: Joint[]; + feature_index: Feature[]; + source_index: Array<{ + source_index_id: string; + source_asset_id: string; + uri: string; + definition_id: string; + feature_id: string; + start_line: number; + end_line: number; + symbol: string | null; + }>; + connector_index: Array<{ + connector_index_id: string; + definition_id: string; + node_id: string; + connector_id: string; + connector_snapshot_id: string; + source_feature_id: string | null; + }>; + joint_index: Array<{ + joint_index_id: string; + assembly_definition_id: string; + joint_id: string; + source_feature_id: string | null; + }>; + camera: { target: Vec3; position: Vec3; up: Vec3; near: number; far: number; fit_mode: string; margin: number }; + extensions: Record; +}; +export type OperationSource = { + path?: string | null; + line: number; + column: number; + end_line: number; + end_column: number; + call_text?: string; + callsite_id: string; + assignment_targets: string[]; +}; +export type ModelNode = { + node_id: string; + definition_id: string; + graph_id: string; + local_node_id: string; + op: string; + params: Record; + inputs: string[]; + output_count: number; + display: { label?: string; category?: string; summary?: string }; + source?: OperationSource; + sources: OperationSource[]; +}; +export type ModelDocument = { graph: { graph_id: string; nodes: ModelNode[] }; leaf_ids: string[] }; + +export function buildFederatedFeatureModel( + manifest: SceneManifest, + files: PackageFiles, +): { model: ModelDocument; sources: Map } { + const pathBySourceId = new Map(); + const sources = new Map(); + for (const asset of manifest.source_assets) { + let path = asset.display_name; + const content = strFromU8(files[asset.uri]); + const existing = sources.get(path); + if (existing !== undefined && existing !== content) { + path = `${path} [${asset.source_asset_id.slice(7, 15)}]`; + } + sources.set(path, content); + pathBySourceId.set(asset.source_asset_id, path); + } + + const nodes: ModelNode[] = manifest.feature_index.map((feature) => { + const featureSources = feature.source_spans.map((span, index) => { + const sourcePath = pathBySourceId.get(span.source_asset_id); + return { + path: sourcePath ?? null, + line: span.start_line, + column: 0, + end_line: span.end_line, + end_column: 0, + callsite_id: `${feature.feature_id}/source/${index}`, + assignment_targets: span.symbol ? [span.symbol] : [], + } satisfies OperationSource; + }); + return { + node_id: feature.feature_id, + definition_id: feature.definition_id, + graph_id: feature.graph_id, + local_node_id: feature.node_id, + op: feature.op, + params: feature.parameters, + inputs: feature.input_feature_ids, + output_count: feature.output_count, + display: { + label: feature.label ?? undefined, + category: feature.definition_id, + }, + source: featureSources[0], + sources: featureSources, + }; + }); + const consumed = new Set(nodes.flatMap((node) => node.inputs)); + const leafIds = nodes.filter((node) => !consumed.has(node.node_id)).map((node) => node.node_id); + return { + model: { graph: { graph_id: 'federated', nodes }, leaf_ids: leafIds }, + sources, + }; +} diff --git a/editor-web/src/scene/theme.ts b/editor-web/src/scene/theme.ts new file mode 100644 index 00000000..ff1ad23d --- /dev/null +++ b/editor-web/src/scene/theme.ts @@ -0,0 +1,47 @@ +/** The colors of the 3D scene in each theme. The page's own colors are CSS variables + * (src/index.css); the scene is drawn by three.js and needs its own copy. */ +export type SceneTheme = 'light' | 'dark'; + +export interface ScenePalette { + /** Viewport background, sRGB. */ + clear: string; + /** Multiplier on the linear background: the present pass tone-maps it (ACES), which + * leaves a pure white at about 0.9; a brighter input lands on the page's white. */ + clearBoost: number; + /** Outline color of the model's edges. */ + edge: string; + gridMinor: string; + gridMajor: string; + /** X, Y, Z. */ + axes: [string, string, string]; + /** The negative end of each axis. */ + axisTail: string; + vertex: string; + /** Highlight of picked entities. */ + selection: string; +} + +export const PALETTES: Record = { + dark: { + clear: '#000000', + clearBoost: 1, + edge: '#3a414d', + gridMinor: '#262626', + gridMajor: '#444444', + axes: ['#ff4d5e', '#3ddc84', '#4aa3ff'], + axisTail: '#555555', + vertex: '#d8ff83', + selection: '#ff9a2e', + }, + light: { + clear: '#ffffff', + clearBoost: 8, + edge: '#1b1f27', + gridMinor: '#e0e0e0', + gridMajor: '#bdbdbd', + axes: ['#d12b3c', '#12803f', '#1a62c4'], + axisTail: '#9a9a9a', + vertex: '#2a7a00', + selection: '#d56a00', + }, +}; diff --git a/editor-web/src/vite-env.d.ts b/editor-web/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/editor-web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/editor-web/tsconfig.app.json b/editor-web/tsconfig.app.json new file mode 100644 index 00000000..6830b6f7 --- /dev/null +++ b/editor-web/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/editor-web/tsconfig.json b/editor-web/tsconfig.json new file mode 100644 index 00000000..2da88b0d --- /dev/null +++ b/editor-web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/editor-web/tsconfig.node.json b/editor-web/tsconfig.node.json new file mode 100644 index 00000000..8455dcbc --- /dev/null +++ b/editor-web/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/editor-web/vite.config.ts b/editor-web/vite.config.ts new file mode 100644 index 00000000..64b77d11 --- /dev/null +++ b/editor-web/vite.config.ts @@ -0,0 +1,29 @@ +import path from "node:path"; + +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// The studio webui. In dev, API/WebSocket traffic proxies to the runtime +// server (`sca edit`, default :7180); `npm run build` emits the +// static bundle the server hosts from simplecadapi/editor/webui. +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "./src"), + }, + }, + server: { + port: 5180, + proxy: { + "/api": "http://127.0.0.1:7180", + "/ws": { target: "ws://127.0.0.1:7180", ws: true }, + }, + }, + build: { + outDir: "../src/simplecadapi/editor/webui", + emptyOutDir: true, + chunkSizeWarningLimit: 1600, + }, +}); diff --git a/examples/README.md b/examples/README.md index 76af09ce..aac6e796 100644 --- a/examples/README.md +++ b/examples/README.md @@ -46,6 +46,7 @@ uv run python examples/four_bar_linkage/export.py # package + exchange files | `u_link_motor_mount/` | assembly | `u_link_motor_mount.py` | `export.py`, `verify/acceptance.py` | `u_link_assembly.scadpkg` + STEP/STL + renders (`demo/index.html` session replay) | | `l_link_motor_mount/` | assembly (part family) | `l_link_motor_mount.py` (`--id l-link-motor-mount- --set PRESET=`) | `export.py`, `verify/acceptance.py`, `verify/export_artifacts.py` | `l_link_assembly_.scadpkg` + STEP/STL per adapter preset + renders | | `pcg_attractor_canopy/` | part (procedural) | `attractor_canopy.py` | `export.py`, `verify.py` | `attractor_canopy.scadpkg` + STEP/STL/OBJ + GIF | +| `pcg_facade_tower/` | procedural building (plain modules) | — | `export.py [rect\|chamfer\|l_shape] [n_floors]`, then `verify/s1_verify.py` … `verify/s8_verify.py` | `facade_tower.scadpkg` + AP242 STEP + panel-schedule CSV + rationalization/scale sweeps + verify renders | | `compact_two_stage_planetary_reducer/` | assembly (part families) | `compact_two_stage_planetary_reducer.py` | `export.py`, `export_mjcf.py` | `compact_two_stage_planetary_reducer.scadpkg` + STEP/FCStd/MJCF | | `integrated_bldc_joint_actuator/` | product (sub-assembly notebooks) | `integrated_bldc_joint_actuator.py` | `export_all.py`, `render_showcase.py` | `integrated_bldc_joint_actuator.scadpkg` + STEP/FCStd/MJCF + renders | | `ap242_gmsh_volume_mesh/` | FEM / data exchange | `bracket.py` | see below | AP242 + Gmsh volume mesh + CalculiX statics | diff --git a/examples/SDK_DEFECTS.md b/examples/SDK_DEFECTS.md index 0f2c2614..93703c28 100644 --- a/examples/SDK_DEFECTS.md +++ b/examples/SDK_DEFECTS.md @@ -301,6 +301,33 @@ that surfaced it is in brackets. it without the cache), or treat a failed state write as a warning; optionally let the state directory be relocated (e.g. an env var or a `--state-dir`). +## 19. The executor raised on any cell that is not in the notebook [editor] + +- **Symptom:** in the editor kernel, every scratch snippet (the bridge's + `execute`) failed with `KeyError: '__scratch__'`. +- **Trigger:** a notebook with `[tool.simplecadapi]` running under marimo's + editor, then any cell marimo runs that the notebook does not contain. +- **Root cause:** `SimpleCadExecutor` looked the cell's key up in the notebook + cells without checking that the cell belongs to the notebook. +- **Status:** fixed. `NotebookCells.__contains__`; the executor delegates such + cells to marimo's default executor. Test: + `test_scratch_cells_are_not_recorded`. +- **Fix direction:** done. The headless runner only ever runs notebook cells, so + only the editor kernel could hit it; test in a kernel, not only headlessly. + +## 20. `ql.vertices()` found nothing on a solid or a face [editor] + +- **Symptom:** `scad.ql.vertices().resolve(solid)` returned no vertices, so a + selector for a picked vertex raised "expected exactly 1 vertex(s), got 0". +- **Trigger:** resolving `ql.vertices()` against a Solid, Face or Wire; only an + Edge scope worked. +- **Root cause:** `_resolve_scope_items` looked only at the scope's direct + children for vertices, and a solid's children are shells. +- **Status:** fixed. It falls back to the vertices of the scope's edges + (`_boundary_items`); test `test_vertices_resolve_through_a_solid_and_a_face`. +- **Fix direction:** done. Every `ql` factory should resolve against any scope + that contains its kind; check each (kind, scope) pair when adding a factory. + ## Heuristics - Run every notebook three times: fresh, all cached, `--no-cache`. The three diff --git a/examples/pcg_facade_tower/BUILD_PLAN.md b/examples/pcg_facade_tower/BUILD_PLAN.md new file mode 100644 index 00000000..09976683 --- /dev/null +++ b/examples/pcg_facade_tower/BUILD_PLAN.md @@ -0,0 +1,135 @@ +# BUILD_PLAN: pcg_facade_tower + +> 时点说明(2026-10-09):本文「同参数同 id 命中缓存」与 S7 增量重建条目写于 part cache 移除前 +> (dev PR #17);dev 上同参数为同一 Part 身份、全量重建,S7 改为计时报告。几何 / 布局策略部分不受影响。 + +Datum: 原点 = 地坪 ∩ 平面参考点(RECT/CHAMFER/L_SHAPE 的 SW 角),Z up,+X 东 +Y 北;平面多边形 CCW, +边 i 自顶点 i → i+1,切向 t_i,外法向 n_i = (t_y, −t_x)。building line = tier 多边形。 +板件局部系:x 沿边(0 = 板左端缝中线),y 向上(0 = 楼层标高),z 外法向(0 = building line)。 +placement:origin = V_i + s0·t_i + z_floor·ẑ,x_axis = t_i,y_axis = ẑ(右手:z_axis = t_i × ẑ = n_i)。 +角件局部系:origin = building line 顶点,x = 入边切向,y 向上,z = 入边外法向;平面轮廓在局部 (x, z) 内。 +竖挺局部系:轴同板件;origin = 板局部 (w − joint/2 − frame_sight/2, 0, frame_d)(右竖框中线 × 框外面)。 + +构造策略:**布局引擎 (layout.py) 是唯一的 PCG 源**,只产数据(FacadeSchedule:tier 多边形、每层每边的板序列、 +板型表、floor 类型表);几何文件只消费板型参数。板型 = `scad.part(id=)` 工厂,参数全部经 builder +实参进入 build_key → 同参数同 id 命中缓存;floor 类型 = `scad.assemble` 子装配,塔楼只放 floor 实例。 + +## 文件 + +| 文件 | 内容 | +| --- | --- | +| `layout.py` | FacadeParams、平面/退台、分格、收口、角部、波场 + 量化、抖动 WWR、板型/floor 类型去重 | +| `panels.py` | 板件族 builder(vision / solid / fin)+ Part 工厂 `make_panel_part(type_id, spec, params)` | +| `corners.py` | 凸角 / 凹角收口件(草图多边形拉伸) | +| `slabs.py` | 楼板 / 屋面退台板 / 雨棚 | +| `floor.py` | 带子装配(floor 类型 / lobby / 女儿墙;板 + 竖挺 + 角件,全 ground) | +| `tower.py` | 塔楼装配 | +| `export.py` | scadpkg / STEP / CSV / 渲染 | +| `verify/` | `sN_verify.py`、`render_stage.py`、`_common.py` | + +## 阶段 + +- **S1 布局引擎**(无几何) + - 输出边界: `build_schedule(params) -> FacadeSchedule`(JSON 可序列化)。 + - 验证: 每边 Σ宽 = 边长(1e-9);收口宽 ∈ [closer_min, closer_min + w_std/2);守卫矩阵(边过短 / 收口越界 / K<2 / 退台过深) + 恰等即拒;设计 WWR 与目标差 ≤ 0.03;板型数 / floor 类型数;三种平面都能出表。 + - 渲染: massing(tier 多边形拉伸 + lobby)四视图 + matplotlib 平面分格图(每 tier 的板/收口/角件着墨)。 +- **S2 板件族** + - 输出边界: 每 kind 至少一个板型单实体;玻璃面 tag。 + - 验证: V3 尺寸;框外面在 z=frame_d、building line 侧 z=0;缝 joint/2;玻璃 tag 面积 = 设计开口; + V5 竖挺角(各 bin);单实体 + brep valid。 + - 渲染: 每板型一张四视图(含一张 K 档竖挺并排)。 +- **S3 角件 + 楼板 + 雨棚** + - 验证: V7;角件多边形与相邻板端(布局值)缝 = joint;楼板内缩 slab_gap;雨棚在南边入口居中。 + - 渲染: 凸 90 / 凸 135 / 凹 270 角件各一 + 与两侧各一块板的局部拼装。 +- **S4 楼层子装配**(先 tier0 第 1 层) + - 验证: V1、V2(全层)、V4(本层)、ground 全部、solve 报告。 + - 渲染: 单层 iso + 顶视(缝可见)+ 角部特写。 +- **S5 塔楼装配** + - 验证: V1/V2 抽样(每 floor 类型一层 + 退台上下层)、V4 全楼、V5 包络、V6、V8、实例数 / 定义数。 + - 渲染: 四视图 + 南立面 + 退台特写。 +- **S6 合理化 / 尺度 / 平面扫描** + - 验证: K ∈ {2,3,4,6,8,12,24}:板型数单调、最大量化角误差 = 90/(2(K−1));层数 ∈ {8,20,36,48}(setback_every = ceil(n/3),保持 3 tier):标准板型集合不变; + RECT/CHAMFER/L_SHAPE 全过 V1/V4。 + - 产物: `out/rationalization.png`(板型数 vs 角误差)、`out/scale_sweep.json`。 +- **S7 增量重建** + - 验证: V9(cold → warm 全 hit;fin_depth 改 → miss = 竖挺件;n_floors 改 → 板型 hit);耗时表。 +- **S8 导出** + - 验证: V10;CSV 板块清单(type_id, kind, w, h, count)。 + +## Stage checklist + +```text +[x] Requirements +[x] Plan +[x] S1 layout [x] S1 verify [x] S1 render +[x] S2 panels [x] S2 verify [x] S2 render +[x] S3 corners [x] S3 verify [x] S3 render +[x] S4 floor [x] S4 verify [x] S4 render +[x] S5 tower [x] S5 verify [ ] S5 render +[ ] S6 sweeps [ ] S6 verify +[ ] S7 incremental [ ] S7 verify +[ ] S8 export [ ] S8 verify +``` + +## Feasibility probe(开工前,/tmp/pcg_probe) + +- 12 板型 × 3000 实例平铺单层装配:SDK 原 `OperationGraph.is_dag` 递归 DFS 在 ~1000 个 add_component 时 + RecursionError;`Assembly.__post_init__` 的 `ids.count` 去重使每次 add 为 O(N²)(3000 实例 69 s)。 + 已修(Kahn 迭代 + Counter)→ 3000 实例装配 12.6 s、capture 5.8 s、STEP 5.8 s(3000 occurrence,3.4 MB)。 +- 渲染 3000 个 Located 共享 TShape 的复合体 ~10 s。 +- `@scad.part` 的 part_id 必须等于装饰器 id → 每板型用 `scad.part(id=type_id)(builder)` 工厂。 +- 仍用楼层子装配分层:塔楼只放 ~40 个 floor 实例,floor 类型复用(tier × 相位)。 + +## Stage log + +- **S1**(`s1_verify.py` PASS,`s1_known_bad.py` 9/9 as designed) + - WWR 目标下调为 N 0.50 / E,W 0.40 / S 0.35:原 N 0.55 超出物理上限(框 + 层间 + 实体收口 → 全 vision 约 0.54), + 引擎现在对不可达目标直接报错。 + - WWR 分配改为**全楼按朝向合池** + **Bayer 秩选择**(按 4×4 阈值排序取前 round(f·N) 格):逐边 / 逐 tier 合池在 + 切角、缺口、顶 tier 短边上不可达;纯阈值法把比例量化到 1/16(误差到 0.024)。现三种平面 × 8/20/36 层误差 ≤ 0.005。 + - 默认 36 层 CHAMFER:26 板型(solid 12 / vision 8 / corner 6),标准 vision 7 = K+1,12 floor 类型,2735 实例。 + - 48 层 + 每 12 层退 3000 → 4 个 tier,CHAMFER 顶 tier 切角边 1029 < 2c+2·closer_min、L_SHAPE 缺口边反向 — + 守卫正确拒绝;S6 层数扫描改为 setback_every = ceil(n_floors/3)。 + - 观察(留到调参):W/E 立面 θ_sun = ±30 叠加幅值 30 → 大量饱和在 ±45 档。 +- **S2**(`s2_verify.py` PASS 20 型:solid 12 / vision 2 / fin 6;`s2_known_bad.py` 12/12 as designed) + - V3 逼出两处设计修改(回写 S1,S1 gate 加 C10 + 竖挺记录一致性检查,KB9/KB10): + 竖挺改为独立 Part(45° 挺装在板上 bbox ≈ 1676 > 1600)→ vision 板型不再乘 K,标准板型 = 2 + K 个竖挺件; + lobby 5400 > 4200 → 两排 2700。默认 CHAMFER 现 26 型(solid 12 / vision 2 / corner 6 / fin 6),3697 实例。 + - 框环 = 一张草图外环 + 1–2 个内环(全约束),玻璃 / 层间板为凹进板,向框内各伸 frame_sight/2 以融合成单实体。 + - SDK 行为:变换(translate/rotate)或后续布尔后,面 tag 只留在 lineage 作用域,`ql.tag(...)` 默认 effective 查不到; + 因此玻璃 tag 放在最后一步,渲染高亮用板局部坐标(不变换)。S4/S5 的 V4 用定义体上的 tag 面积 × 实例数。 +- **S3**(`s3_verify.py` PASS;`s3_known_bad.py` 14/14 as designed) + - 角件 9 型(turn −90 / +45 / +90 × h 2700 / 3600 / 1200);三种平面 × 每 (tier, 带类) 一带共 105 处角部在世界坐标实测: + 外面与两侧板带外面共面、building line 面、端面在 corner_c − joint/2,与两侧板最小距离 = joint。 + - K6 实测到 A7 的舍入:收口板按 0.001 mm 取整建型、按精确起点放置,其后的缝偏差 ≤ 3.1e-4 mm; + 缝容差取 A7 声明的 5e-4(不是放宽几何,是把 A7 写进 gate)。 + - 楼板:每 tier 一型,38 个实例(地坪 + 各层 + 各 tier 屋面;上 tier 首层立在下 tier 屋面上,不另设板); + 楼板与同层板件 / 角件最小距离 = slab_gap。雨棚 6 模数,两端落在 lobby 板缝上、居中、外挑起点距板外面 = joint, + 底面在 lobby 两排的横缝上。 + - SDK 修复(建筑尺度首次触发,均带回归测试 `test_building_scale_part_hashes_and_round_trips_its_state`): + ① 几何指纹按 1e-7 量化,36 m 楼板体积 / 1e-7 超出 2^53 → 哈希被 JSON 安全整数检查拒绝; + ② RFC 8785 把 < 1e21 的整值 double(楼板惯量 ≈ 1.2e19)写成无指数整数,读回 latest-parts 状态时被当作越界整数拒绝。 + - (S5 规划时回改)屋面板原按楼板惯例占 [roof, roof + slab_t],会切进上 tier 首层(其板件从 roof + j/2 起、位于上 tier + building line 上,而屋面板平面是下 tier 的)→ 屋面板改占 [roof − slab_t, roof](顶面 = 屋面 / 退台面,藏在顶层板带头部后)。 + K3 改写:屋面板顶 = 女儿墙标高、与顶层板件距离 = slab_gap、与上 tier 首层板件 / 角件 / 竖挺距离 = joint/2; + `s3_known_bad` 加 KB10(楼板式屋面板)→ 15/15。SDK 缺陷统一记在 `SDK_DEFECTS.md`。 +- **S4**(`s4_verify.py` PASS 全部 16 带;`s4_known_bad.py` 13/13 as designed,每例须触发列出的 gate) + - 带子装配 = `scad.assemble(id="band-")`,定义 = 本带用到的板型 Part(floor 带 11–12 个、lobby / 女儿墙 5 个); + 构件 id `r{row}-e{edge}-s{slot}` / `…-fin` / `r{row}-v{vertex}`,全部 ground。楼板、雨棚留给塔楼放置(REQUIREMENTS 6 已改)。 + - 验证在**求解后的 Placement** 上独立重建 gp_Trsf(不走布局帧),并与 SDK `make_compound_from_assembly` 的体积 / bbox 对账; + V1 每排每边「角件 | 板按 slot | 角件」沿 t 顺序 + 缝 = joint(A7 容差)+ z 跨度;V2 对 bbox 相交的全部构件对做 Common + (floor 带 22–70 对,含竖挺与所在板、lobby 上下排);V4 定义体 tag 玻璃面积 = 布局设计值。 + - 本带 WWR(仅报告,目标按全楼朝向合池,S5 检):tier0-phase0 E .530 / N .544 / S .439 / W .549,phase1/3 低至 .21–.35。 + - 重建全 warm(无 dirty instance,定义 hash 不变);16 带 cold + warm + 全部检查 88 s。 +- **S5**(`s5_verify.py` PASS;`s5_known_bad.py` 11/11 as designed) + - 塔楼 = `scad.assemble(id="facade-tower")`,79 构件(lobby + 36 floor + 3 女儿墙 + 38 楼板 / 屋面板 + 雨棚), + 20 定义(16 带 + 3 tier 楼板 + 雨棚),展开 3697 件 / 26 型;SDK 展平复合体 3736 实体(+ 板 / 雨棚),高 136.2 m。 + - 验证在「塔楼 placement × 带 placement」合成的 gp_Trsf 上重建世界几何:T0 构件 / 定义 / 叶子型集 / 件数; + T1 全部 placement 为纯 z 平移 + SDK 投影对账;V8 楼层 / 女儿墙 / 屋面板标高与 tier,建筑线内缩 = k·setback_depth; + V1 / V5 / V2 抽样 19 个实现(lobby、首末层、每 floor 类型首层、每次退台上下层、全部女儿墙),18 个接缝窗口 1845 对 Common。 + - V4 全楼(定义体 tag 玻璃面积 × 实例数):E .400 / N .499 / S .351 / W .398,与 `design_wwr` 一致、距目标 ≤ 0.001。 + - V6:标准板型 `solid-w1500-h3600` + `vision-w1500-h3600-s1100`,竖挺 6 型 = K。 + - known-bad 逼出的两处 gate 修正:KB2(缺一层)T1 是 SDK-对-我们的一致性检查,自洽时不该响 → 期望改 T0 + V4; + KB4(女儿墙放错 roof)按 schedule id 选接缝窗口会漏掉被放错的构件 → 窗口改为「实测 z 区间与窗口相交的全部构件」。 + - 耗时:塔楼 build 47.2 s,零改动 rebuild 45.8 s(warm 不比 cold 快,见 SDK_DEFECTS D7),全部检查 169 s。 diff --git a/examples/pcg_facade_tower/README.md b/examples/pcg_facade_tower/README.md new file mode 100644 index 00000000..9bfd424e --- /dev/null +++ b/examples/pcg_facade_tower/README.md @@ -0,0 +1,44 @@ +# pcg_facade_tower + +PCG 单元式幕墙塔楼:同一份参数化代码,任意平面轮廓(RECT / CHAMFER / L_SHAPE)、层数与退台, +自动生成**可加工**的外立面——板型 Part 只有一种身份, placement 才是数量(3,697 个实例落在 +22–43 个类型上,量化误差有硬上界)。设计策略全部写在代码里:分格规则、竖挺波场按 K 量化、 +窗墙比按朝向有序抖动到目标值、角件按转角+高度归型;不可行的参数直接 `LayoutError`,不静默降级。 + +## Run + +```bash +# 完整导出(包 + AP242 STEP + 板型清单 CSV + 主渲染),可带平面类型与层数 +../../.venv/bin/python export.py [rect|chamfer|l_shape] [n_floors] + +# 验证门(几何 / 布局 / 有效性 / 装配 / 导出回读),s1 最快,s4-s8 较慢 +../../.venv/bin/python verify/s1_verify.py +../../.venv/bin/python verify/s6_verify.py # 参数化扫描(K 量化 / 规模 / 平面) +../../.venv/bin/python verify/s6_verify.py --no-geometry +``` + +## 模块 + +- `layout.py` — 纯数据布局引擎:平面 → 退台 → 逐边分格 → 板型清单(所有程序化规则只在这里)。 +- `panels.py` / `corners.py` / `slabs.py` — 按 type id 的 Part 工厂(同参数即同一身份)。 +- `floor.py` / `tower.py` — 带 子装配(每 band 建一次,按标高平移放置)+ 塔楼装配。 +- `export.py` — scadpkg 闭包 + XCAF AP242 STEP(每个 definition 只写一次,occurrence 为定位引用) + + 板型清单 CSV + 主渲染。 +- `verify/` — s1 体量 / s2 板型 / s3 角件·楼板·雨棚 / s4 band 装配 / s5 塔楼 / s6 扫描 / + s7 重建计时 / s8 导出回读;`s*_known_bad.py` 证明每道门在故意做错的构件上会失败。 + +## 产物(out/,git 忽略) + +`facade_tower.scadpkg`、`facade_tower.step`、`facade_tower_panels.csv`、 +`rationalization.png`、`scale_sweep.json`、`incremental.json`、`verify/renders/*.png`。 + +## Known limitations + +- **L_SHAPE 全塔验证未闭合**:反射角(270°,转角 -90°)的角件轮廓为填充内凹缺口沿法向伸出 + 440 mm(> frame_d 150),在塔楼装配的 V5 包络检查(角件沿边法向 ≤ frame_d,为凸角所写)下 + 不成立——S6 的平面几何扫描因此只含 RECT / CHAMFER,L_SHAPE 目前仅做布局层验证 + (`facade_sweep` / s1 / s3 的 m90 角件局部框架检查)。`SDK_DEFECTS.md` D9 记录了该开放问题, + 欢迎贡献修复。 + +设计笔记:`REQUIREMENTS.md`(需求与验收门表)、`BUILD_PLAN.md`(布局引擎的构造顺序)、 +`SDK_DEFECTS.md`(把例子当压力测试记下的 SDK 设计问题;缓存时代的历史条目见文首时点说明)。 diff --git a/examples/pcg_facade_tower/REQUIREMENTS.md b/examples/pcg_facade_tower/REQUIREMENTS.md new file mode 100644 index 00000000..7cb512c8 --- /dev/null +++ b/examples/pcg_facade_tower/REQUIREMENTS.md @@ -0,0 +1,98 @@ +# REQUIREMENTS: pcg_facade_tower + +> 时点说明(2026-10-09):本文写于 SDK 移除 part cache(dev PR #17,notebook runtime 取代)之前。 +> 第 9 条与 V9 的「增量重建 / 命中 / solve 报告」需求描述的是旧设计;dev 上 S7 已改为全量重建计时报告, +> 装配层 A2 检查改为两次构建 definition hash 一致。其余几何 / 布局 / 有效性需求不受影响。 + +PCG 单元式幕墙塔楼:同一份代码,任意平面轮廓 / 层数 / 退台,自动生成**可加工**的外立面。 +论点:参数化的是**实例化**(少量板型 Part × 大量 placement),不是几何堆砌;GN 网格没有"板型"。 + +## CAD brief + +```text +- Model: pcg-facade-tower, assembly (panel/corner/slab Parts → floor subassemblies → tower) +- Inputs: prose (本会话讨论);inspiration, not reproduction +- Units: mm, degrees +- Coordinate convention: Z up;原点 = 地坪 z=0 ∩ 平面参考点;+X 东、+Y 北;平面多边形 CCW +- Overall dimensions (default CHAMFER plan): 36000 x 24000 底平面,NE 角 45° 切角 6000; + lobby 5400 + 36 层 x 3600 → 屋面 135000;每 12 层四边内退 3000(3 个 tier) +- Functional features: 标准板(vision + 遮阳竖挺 / vision 无挺 / solid 实体板)、收口板 closer、 + lobby 通高玻璃板、女儿墙板、角部收口件(凸角 90°/135°、凹角 270°)、楼板/屋面/退台板、主入口雨棚 +- Positioning/mating: 板件落在 building line(楼板外沿参考线)外侧 y∈[0, frame_d]; + placement 由布局引擎自具名基准(building line 顶点、楼层标高)计算;幕墙固定于结构 → 全部 ground +- Parameters: 见 layout.py FacadeParams(全部具名;板件尺寸经 builder 参数传入以进入 build_key) +- Paths: examples/pcg_facade_tower/{layout,panels,corners,slabs,floor,tower,export}.py;out/*.scadpkg|step|csv|png +- Validation targets: 见下文 V1–V10 +- Assumptions: 见下文 A1–A8 +``` + +## 设计参数(默认值) + +| 组 | 参数 | 默认 | 说明 | +| --- | --- | --- | --- | +| 平面 | `plan_kind` | CHAMFER | RECT / CHAMFER / L_SHAPE | +| | `plan_w`, `plan_d` | 36000, 24000 | X / Y 外包 | +| | `chamfer` | 6000 | CHAMFER:NE 角沿两边各切 6000(45° 边) | +| | `notch_w`, `notch_d` | 12000, 9000 | L_SHAPE:NE 角缺口 → 1 个 270° 凹角 | +| 竖向 | `n_floors` | 36 | lobby 之上的标准层数 | +| | `floor_h`, `lobby_h` | 3600, 5400 | | +| | `setback_every`, `setback_depth` | 12, 3000 | 每 tier 12 层,四边同时内退 | +| 模数 | `w_std` | 1500 | 标准板宽(含缝) | +| | `closer_min` | 450 | 收口板最小宽(含缝);两端对称 → 上限 closer_min + w_std/2(不含) | +| | `joint` | 20 | 板间缝(四周各 joint/2) | +| | `corner_c` | 450 | 角部收口件沿各边占宽(自 building line 顶点) | +| 板件 | `frame_d`, `frame_sight` | 150, 60 | 框深 / 框面宽 | +| | `spandrel_h` | 1100 | 层间实体区高(盖住楼板边) | +| | `glass_t`, `glass_recess` | 24, 40 | 玻璃厚 / 距框外面凹进 | +| 遮阳 | `fin_t`, `fin_depth` | 40, 300 | 竖挺厚 / 伸出深度(自框外面) | +| | `fin_amp`, `fin_wavelength`, `fin_period_floors` | 30°, 18000, 4 | 波场:θ = θ_sun(边) + A·sin(2π(u/λ + f/P)) | +| | `fin_bins` K | 6 | θ 量化到 [-45°, 45°] 的 K 档(合理化旋钮) | +| 窗墙比 | `wwr_target` | S 0.35 / E,W 0.40 / N 0.50 | 按边外法向归到 4 个朝向;全楼按朝向合池,上限约 0.54(不可达即报错) | +| 太阳 | `sun_azimuth` | 180° | 自北顺时针;朝北边(n·sun ≤ 0)无竖挺 | +| 板型 | `type_round_mm` | 1e-3 | 板型身份 = 参数四舍五入到 0.001 mm(闭合用精确值,误差 ≤ 5e-4 由缝吸收) | + +## 需求区 + +1. **布局引擎**(纯 Python,无几何):平面 → tier 退台多边形 → 每边:角部占宽 → 标准板数 n + 两端对称收口板 → + 每块板的 (floor, edge, s0, s1, kind, fin_bin) → 板型去重 → floor 类型去重(tier × 相位 f mod P)。 +2. **窗墙比分配**:每边 vision/solid 由有序抖动(4×4 Bayer,按 bay × floor)分配,使该朝向实测 WWR 贴近目标。 +3. **板件族**:vision(标准层带层间区 / lobby 无层间区)、solid(实体标准板、收口板、lobby 收口、女儿墙)、 + fin(K 档竖挺,独立 Part,现场挂在板右竖框上);板件单实体,玻璃面打 tag `facade.vision_glass`。 + lobby 5400 超运输高度 → 上下两排 2700 板(排数 = ceil(lobby_h / 4200))。 +4. **角部收口件**:由相邻两边方向 + frame_d + corner_c 计算的平面多边形 → 草图拉伸;凸角与凹角两族。 +5. **楼板 / 屋面 / 退台 / 雨棚**:tier 多边形内缩 slab_gap 拉伸;退台 = 下 tier 屋面;南向主入口雨棚。 +6. **楼层子装配**(带子装配):每个 floor 类型 / lobby / 各 tier 女儿墙一个 `scad.assemble` 定义,内含该带所有板、竖挺与角件(ground,placement 来自布局帧,带局部 z = 0 为本带标高;lobby 含上下两排);相同 floor 类型只建一次。楼板与雨棚由塔楼装配直接放置(上 tier 首层立在下 tier 屋面上,楼板不是 floor 类型的属性)。 +7. **塔楼装配**:lobby 层 + 36 标准层(floor 类型实例)+ 各 tier 屋面/女儿墙 + 雨棚。 +8. **合理化与尺度**:K 扫描(板型数 vs 最大角度偏差);层数扫描(8/20/36/48 层:标准板型数不增长);三种平面。 +9. **增量重建**:改 `fin_depth` → 只有竖挺件 miss,其余命中;改 `n_floors` → 板型全部命中。 +10. **导出**:scadpkg + AP242 STEP(共享定义)+ 板块清单 CSV + 渲染。 + +## 验证目标 + +| # | 检查 | 方法(几何实测,不信设计值) | +| --- | --- | --- | +| V1 | 立面闭合 | 每层每边:世界坐标下板/角件沿边区间(bbox 投影)首尾相接,缝 = joint ± 1e-3,无重叠;角件与两侧板缝 = joint | +| V2 | 零干涉 | 同层相邻件、相邻层上下件:采样分类 + Common 体积 = 0(抽样:每 floor 类型 1 层全查) | +| V3 | 运输尺寸 | 每板型 tight bbox ≤ 1600 x 4200 x 600(宽 x 高 x 深) | +| V4 | 窗墙比 | Σ(实例 × tag 玻璃面面积) / 朝向毛立面积(标准层板带),与目标差 ≤ 0.03 | +| V5 | 竖挺 | 竖挺侧面法向实测角 = bin 角 ± 0.05°;挺尖离 building line ≤ frame_d + fin_depth + 1 | +| V6 | 板型数 | 标准板型(vision + solid,w_std × floor_h)= 2,竖挺件 ≤ K;48 层与 8 层标准板型集合相同 | +| V7 | 角件 | 凸/凹角件为单实体、外轮廓与两侧板带外面共面(± 1e-3) | +| V8 | 楼层与退台 | 每层 z 标高、tier 退台深度、女儿墙位于 tier 屋面边 | +| V9 | 增量 | fin_depth 改动:miss 集合 = 竖挺件;n_floors 改动:板型全 hit;装配 solve 报告符合预期 | +| V10 | 导出 | 新进程重开 scadpkg:体积和 / 实例数 / 定义数一致;STEP occurrence 数 = 实例数 | + +## 假设(需要报告的) + +- A1 板件是"外观级"单元板:框 + 玻璃 + 层间板合为一个实体(Part 只含 1 个 Solid),不分铝/玻璃材质; + 不含锚件、密封胶、排水,不做结构/热工/风压分析(manufacturing-boundaries)。 +- A2 幕墙固定于结构,不建连接件约束:placement 由布局基准计算,全部 ground(intent ladder: placement tied to named datum)。 +- A3 building line 即楼板外沿参考线;板件在其外侧;楼板内缩 slab_gap = 30,楼层板占 [level, level + slab_t](藏在层间板后); + 屋面板占 [roof − slab_t, roof](顶面 = 屋面 / 退台面,藏在顶层板上沿后);上 tier 首层立在下 tier 屋面板上 + (其板件底 = roof + joint/2;若屋面板也取 [roof, roof + slab_t],会在上 tier building line 外被上 tier 首层板件穿过 — S5 规划时发现,S3 K3 补 terrace 检查 + KB10)。 +- A4 退台四边同距内退,保持平面形状;任何边内退后长度不足(< 2·corner_c + closer_min·2)时布局报错而非降级。 +- A5 收口板两端对称分布;收口板为实体板、无竖挺。 +- A6 朝北边无竖挺;竖挺位于每块板右侧竖框中线(沿边正向的末端),为独立的现场安装件: + 固定在板上时 45° 挺使单元宽约 1676 > 1600(V3),且 fin_depth 只应影响竖挺件(V9)。 +- A7 板型身份取参数到 0.001 mm 的四舍五入值(与上次讨论一致:身份用离散化,闭合与检查用精确值 + 容差)。 +- A8 渲染为灰度实体(玻璃不透明),用于形态评审,不是效果图。 diff --git a/examples/pcg_facade_tower/SDK_DEFECTS.md b/examples/pcg_facade_tower/SDK_DEFECTS.md new file mode 100644 index 00000000..1686aab6 --- /dev/null +++ b/examples/pcg_facade_tower/SDK_DEFECTS.md @@ -0,0 +1,178 @@ +# SDK 设计缺陷记录(pcg_facade_tower 构建过程中发现) + +> 时点说明(2026-10-09):本文各条针对写作时的 SDK(part cache + 装配 solve_report 时代,dev PR #17 之前)。 +> dev 已移除 part cache 与 solve_report(notebook runtime 取代):涉及二者的条目(如 D8、缓存命中路径、 +> 增量基线)自此成为历史记录,不再对应现行代码;几何 / 布局 / 布尔相关条目仍然有效。 + +目的:把 PCG 建筑尺度的例子当作压力测试,记下每处「SDK 本身设计上的问题」——症状、触发条件、根因、 +当前处置、修复方向——之后据此对症修复。末尾「启发」一节把单条缺陷归纳成可复用的排查规则 +(下次在别处先按这些规则找同类问题)。 + +状态:`已修(未提交)` = 工作区里有补丁 + 回归测试,未 commit;`绕开` = 例子侧规避,SDK 未改;`待修` = 只记录。 + +规模参照:默认 36 层 CHAMFER 塔 = 26 板型 Part、16 个带子装配(每个 70–250 构件)、塔楼 79 构件 / 20 定义、 +共 3697 个外立面件;楼板 36 m × 36 m。 + +--- + +## D1 `OperationGraph.is_dag` 递归 DFS → 长操作链 RecursionError + +- 状态:已修(未提交)— `src/simplecadapi/topology/model.py` +- 症状:单层装配 `add_component_rassembly` 约 1000 次后抛 `RecursionError`。 +- 根因:每次 add 是 feature graph 上的一个节点,链长 = 构件数;`is_dag` 用递归 DFS,深度 = 链长。 +- 修复:`_kahn_order()` 迭代 Kahn,`is_dag` / `topological_order` 共用(后者原本还先跑一遍 DFS 再跑 Kahn)。 + 顺带把 `queue.pop(0)`(O(N))换成 `deque.popleft()`。 +- 缺:没有「≥ 数千节点链」的回归测试。 + +## D2 id 去重用 `list.count` → 每次 add O(N²) + +- 状态:已修(未提交)— `product/assembly.py`、`product/connector.py`、`product/constraint.py` +- 症状:3000 实例平铺装配 69 s;修后 12.6 s。 +- 根因:`Assembly.__post_init__` 每次构造(不可变值,每 add 一次重建)都 `ids.count(id)` 查重 → 单次 O(N²),整条链 O(N³)。 +- 修复:`Counter`。 +- 遗留:不可变 Assembly 每 add 整体重建 + 全量校验本身仍是每次 O(N),整链 O(N²); + 应提供批量 API(`add_components_rassembly(components=[...])`)或增量校验。 + +## D3 几何指纹量化整数超出 JSON 安全整数范围 + +- 状态:已修(未提交)— `artifacts/geometry_interface.py`;回归测试 + `test/test_geometry_summary.py::test_building_scale_part_hashes_and_round_trips_its_state` +- 症状:36 m 楼板 Part 建型后求指纹失败(哈希被 JSON 安全整数检查拒绝)。 +- 根因:指纹把体积 / 面积 / 坐标按 1e-7 量化成 int;36e3² × 250 / 1e-7 ≈ 3e18 > 2^53。 + 量化步长是**绝对**的,隐含了「零件尺度」假设。 +- 修复:`_json_safe` 在哈希前把超界 int 编码为 `"int:<十进制>"`;范围内的值与旧哈希一致(不失效既有缓存)。 +- 更好的方向:量化步长相对于包围盒对角线(与 `geometry_summary` 的相对容差一致),根本不产生这种量级。 + +## D4 RFC 8785 状态文件读回时拒绝大整值 double + +- 状态:已修(未提交)— `scene/canonical.py`(同 D3 回归测试) +- 症状:写入 `latest-parts.json` 成功,下次读取时报越界整数。 +- 根因:JCS 把 < 1e21 的整值 double(楼板惯量 ≈ 1.18e19)打印成无指数的整数 token + `11773562012270215000`;`parse_strict_json` 按 Python int 读回 → 超 2^53 被拒 / 重新规范化不一致。 + 即「写」和「读」对数字域的约定不对称。 +- 修复:`parse_canonical_json` 用 `parse_int=_jcs_int`,超界整数 token 读成 double。 + +## D5 面 tag 经变换 / 后续布尔后退到 lineage 作用域,默认查询查不到 + +- 状态:绕开 — 例子把玻璃 tag 放在建型最后一步;渲染高亮用板局部坐标;V4 用「定义体 tag 面积 × 实例数」。 +- 症状:`translate` / `rotate` 或其后布尔之后,`ql.tag(...)`(默认 effective)返回空。 +- 根因:tag 绑定在具体面上,变换产生新面后只在 lineage 中可追溯;effective 作用域不跟随纯刚体变换。 +- 影响:装配展平(`make_compound_from_assembly_rcompound`)后的整楼上无法按 tag 选玻璃面—— + 「按语义选面」在装配层失效,而这恰是 PCG 统计(WWR)最需要的。 +- 修复方向:纯刚体变换应保持 effective tag(面一一对应,无歧义);或装配投影时按实例 placement 转发定义体 tag。 + +## D6 `@scad.part` 的 part_id 必须等于装饰器 id → 参数化族需要工厂写法 + +- 状态:绕开 — `scad.part(id=type_id, revision=..., project_root=ROOT)(builder)(**kwargs)`,每个板型动态造一个装饰器。 +- 根因:Part 身份 = 装饰器 id,而 PCG 的「类型」是运行时由参数算出来的(`solid-w1600-h3600`), + 装饰器在 import 时就要定 id。 +- 修复方向:官方支持 `part_family` / `id=lambda **kw: ...`,或文档化工厂写法作为推荐模式。 + +## D7 装配 geometry_summary 递归内嵌子摘要 + 每次 assemble 全量读写状态文件 → warm 比 cold 慢 + +- 状态:待修(本分支 commit 8b218d4 引入的摘要设计) +- 症状:塔楼 cold 构建 31 s,**warm 49 s**(无任何改动的重建反而更慢)。 +- 证据:`.simplecad/state/latest-assemblies.json` 4.7 MB,其中 facade-tower 一条 3.6 MB + (`public_interface.geometry_summary.instances` 2.6 MB、`dependency_interfaces` 1.0 MB)。 + profiler(~114 s 含开销):`load_latest_assembly_state` 57 s / 34 次,`canonical_json_bytes` 60 s, + `update_latest_assembly_state` 29 s,part cache 读取 22 s,`validate_assembly_definition_graph` 17 s。 +- 根因(两层叠加): + 1. `assembly_geometry_summary`(`artifacts/geometry_summary.py`,调用处 `build/assembly_builder.py` + `_assembly_geometry_summary`)把每个子定义的**完整摘要**按实例复制进父摘要:塔楼 ~40 个 floor 实例 × + 每个带摘要 50–115 KB。同一定义被实例化 N 次就存 N 份,嵌套再乘一层。 + 2. 每次 `assemble` 都把整个状态文件解析 + JCS 规范化校验一遍,写回时再全量序列化 → 代价 ∝ 调用次数 × 全项目状态大小, + 与本次改动无关。 +- 修复方向: + - 摘要里实例只存 `(instance_id, definition_id, placement)`,子摘要按 `definition_id` 去重存一份 + (或只存子摘要 hash,等价判定时按引用取);`geometry_summaries_equivalent` 相应按引用比较。 + - 状态存储按条目分文件(`state/assemblies/.json`)或进程内缓存 + 只写脏条目。 +- 需要的回归测试:N 个同定义实例的父摘要大小不随 N 线性增长;warm 重建耗时 ≤ cold。 +- S5 复测(z 区间窗口版 gate):塔楼 warm 构建 47.2 s、再 warm 45.8 s(零改动),而 3697 件的 S5 全部几何检查才 169 s —— + 一次「什么都没变」的重建占到完整验证的 ~1/4。S6 扫描 6 座塔 / S7 六步各付一次这个代价。 + +## D8 增量基线是「每个 assembly id 全项目唯一的 latest 槽」→ 参数族 / 多进程互相覆盖基线 + +- 状态:待修(设计层面) +- 症状:`solve_report.dirty_instances` / `geometry_dirty_instances` 的对比对象是 `latest-assemblies.json` 里该 + assembly id 的**最后一次**构建,不论是谁、以什么参数建的。S6 在同一 id `facade-tower` 下依次建 8/20/36/48 层 + + RECT/L_SHAPE,每次都把上一座当基线 → 报告的是「和上一个扫描点的差」,不是「和这组参数上次的差」;两个进程 + (例如 S6 扫描与 S7 增量 gate)同时跑时,彼此的基线在中途被替换,dirty 报告不可复现。 + 状态文件本身有 O_EXCL 锁(`artifacts/interface.py` `_state_lock`),所以不会写坏,但**语义**上互相污染。 +- 对照:part 缓存按 build_key 内容寻址,所有变体并存、回退即命中 —— 同一 SDK 里 part 层可以多变体、装配层只有一个槽。 +- 触发:PCG 参数族(同一 id 多参数集)、CI 并行、notebook 与脚本并行。 +- 修复方向:基线槽按 (assembly id, build_key/参数指纹) 分;或 `assemble(..., baseline=…)` 显式给出对比对象 + (上一次同参数 / 指定 build);`solve_report` 里写出基线来自哪次构建(build_key + 时间),让报告可解释。 +- 需要的回归测试:同一 id 交替构建参数 A、B、A → 第三次对 A 报告零 dirty。 +- 规避(本例):几何 gate 串行运行;S7 在同一进程里自建基线链(prime 步只建立基线、不判定)。 + +## D9 读 .scadpkg 的代价 ∝ 实例数 × 重复规范化 / 哈希 / schema 校验(读 20 s,materialize 只要 2.5 s) + +- 状态:待修 +- 症状:可行性探针的平铺包(12 板型 × 3000 实例,单层装配)在新进程里 `read_product_package` 20 s(profiler 下 40.7 s), + 其后 `materialize_definition` 仅 2.5 s —— 读包比把几何建出来还贵一个量级。 +- profiler(`read_product_package`,40.7 s): + - `_decode_definitions` 38.4 s;其中 `canonical_bytes` 5401 次 / 22.9 s、`content_hash` 3913 次 / 15.0 s + (次数 ≈ 实例数,而不是定义数);rfc8785 `dump` 递归 744 万次。 + - `validate_assembly_definition_graph` 20.1 s(读包时整图再验一遍,而 `_mark_package_validated` 之后 materialize 又查一次标记)。 + - `load_feature_graph_artifact` 13 次 / 17.2 s(`FeatureGraph.from_dict` 12.7 s、`__post_init__` 8.5 s)。 + - jsonschema `validate_manifest` 53 次 / 11.7 s(per-item `items` / `ref` 递归 58 万次)。 +- 根因:读路径把「完整性」拆成了逐对象、逐实例的重复工作:每个 component / placement 记录单独 JCS 规范化 + 哈希, + 同一份内容在 decode、图校验、feature graph 构造里各算一遍;schema 校验对大数组逐项走完整 `$ref` 解析。 +- 修复方向: + - 每个 blob 只按 zip 成员哈希一次(manifest 已记内容 hash),decode 后的派生对象不再重算 `content_hash`,缓存 canonical bytes。 + - 读包路径一次图校验,结果随对象携带(已有 `_validated` 标记的思路,推广到 assembly graph / feature graph)。 + - 实例数组用紧凑 schema(或跳过逐项 jsonschema、用类型化解码代替)。 +- 需要的回归测试:读包耗时对实例数近似线性且常数小(例如 3000 实例 < 2 s);同一 blob 在一次读中只哈希一次。 + +## D10 SDK 自己的输出类型不被兄弟 API 接受:`make_compound_from_assembly_rcompound` → `render_screenshot_rpath` 报错 + +- 状态:待修(小,但每次都撞) +- 症状:`render_screenshot_rpath(shapes=[scad.make_compound_from_assembly_rcompound(assembly=asm)])` → + `ValueError: render_screenshot_rpath supports Solid or raw TopoDS_Shape inputs`;要手动 `.wrapped` 降到 OCP 原始类型。 + 而且错误在构建 + 展平 3700 件(~5 min 的整个渲染脚本)之后才抛出。 +- 根因:渲染入口按 `Solid | TopoDS_Shape` 白名单判型,没有覆盖 SDK 自己的 `Compound` / `Shell` / `Face` 包装类型; + 没有「SDK 返回的几何值都可直接喂给 inspect / render」的类型闭包约定。 +- 修复方向:渲染 / 测量入口统一接受任何带 `.wrapped` 的 SDK 形状包装(或一个 `ShapeLike` 协议);输入判型放在最前面。 +- 需要的回归测试:对 SDK 每个返回形状的 `make_*` / `*_rcompound`,结果都能直接 render / measure。 + +## D11 反射角(270°)角件轮廓与 V5 包络规则冲突:L_SHAPE 全塔验证未闭合(开放,欢迎贡献) + +- 状态:开放问题(示例侧设计缺口,非 SDK 缺陷;2026-10-09 首次全塔验证时暴露) +- 症状:L_SHAPE 36F 全塔 S5 检查 40 项 V5 失败,全部同一模式——每个 band 在反射角顶点 v3 + `corner 440.0 > frame_d`(凸角全部通过)。归档的 `out/scale_sweep.json` 里 `plan: []`: + 作者从未完成平面几何扫描,该矛盾在入库前未被暴露。 +- 根因:`layout.corner_profile_local` 的凸角分支正确(miter 点在 frame_d,两腿 440 = + corner_c − joint/2,由分格规则预留);凹角分支为填充内凹缺口,轮廓沿入边法向伸出 440 + (local y[0, +440]),而 S5 的 V5 规则写的是「角件沿边法向 ≤ frame_d」——规则为凸角所写, + 两条约定在凹角处矛盾。s3 只在角件局部框架内验证 m90 角件(自洽),到塔楼 fit 才互相矛盾。 +- 修复方向(三选一,作者/贡献者拍板): + 1. 重写凹角轮廓,使角件收进两邻边的 frame_d 包络(改 `corner_profile_local` 的 turn<0 分支; + s3 的 m90 渲染与局部检查需随之重验); + 2. V5 为凹角加豁免:以「填满内凹缺口」的实测规则替代 frame_d 上限(改 s5_verify); + 3. 维持现状:L_SHAPE 仅做布局层验证(facade_sweep / s1 / s3),s6 平面几何扫描只含 + RECT / CHAMFER(当前实现,见 README Known limitations)。 +- 需要的回归测试:任一方案落地后,L_SHAPE 36F 全塔 S5 零失败(或豁免规则下的等价判定)。 + +--- + +## 启发(排查规则) + +1. **尺度假设**:凡是「绝对」量化步长 / 容差(1e-7、JSON 2^53、float→int)都要问:100 m 级、1e5 件级时还成立吗? + 优先相对于包围盒的容差。(D3、D4) +2. **读写对称**:序列化规范(JCS)写出来的每一种 token,解析器都必须按同一数域读回;给每种持久化格式做一次 + 极值 round-trip 测试。(D4) +3. **与用户规模成正比的递归**:图遍历、链式 feature、嵌套装配——深度随用户输入增长的地方一律迭代实现。(D1) +4. **不可变值 × 逐项追加**:每次追加都重建 + 全量校验 → 整链 O(N²);校验里再有 O(N) 查找就是 O(N³)。 + 找所有 `__post_init__` 中的 `.count` / `in list` / 全量扫描,并提供批量构造入口。(D2) +5. **实例化 ≠ 复制**:任何「按实例」记录的派生数据(摘要、接口、依赖)都应按定义去重、按引用链接; + 否则大小 ∝ 实例数 × 嵌套深度。(D7) +6. **缓存命中路径的代价**:命中路径必须 ≤ 未命中路径;凡是命中时仍要读 / 解析 / 规范化全局状态的,按项目规模退化。 + 给 warm/cold 耗时比加基准测试。(D7) +7. **语义标签要跟随刚体变换**:tag / 选择器在变换、装配投影后仍应可用,否则语义层在装配层失效。(D5) +8. **运行时类型族**:PCG 的类型是算出来的,身份(id)不能只能在 import 时声明。(D6) +9. **缓存各层的键要一致**:part 层内容寻址(多变体并存)、装配层 id 单槽(后写覆盖)——同一个「增量」概念在两层 + 语义不同,参数族与并行一来就暴露。凡是 “latest” 语义的全局状态,都要问:谁的 latest?(D8) +10. **完整性检查只做一次**:内容哈希 / 规范化 / schema 校验在一次读写里按「对象」计次而不是按「引用」计次; + 给每条 I/O 路径数一下同一 blob 被哈希了几次。验证结果要能随对象携带,别让下游再验。(D7、D9) +11. **API 类型闭包**:SDK 任一函数的返回值,应能直接作为其它 SDK 函数的同类输入;判型白名单要从「SDK 输出类型全集」 + 生成,不是手写。(D10) diff --git a/examples/pcg_facade_tower/corners.py b/examples/pcg_facade_tower/corners.py new file mode 100644 index 00000000..a50dc305 --- /dev/null +++ b/examples/pcg_facade_tower/corners.py @@ -0,0 +1,39 @@ +"""pcg_facade_tower corner pieces: one Part per (turn, height) type from the layout schedule. + +Corner-local frame (BUILD_PLAN): origin = building-line vertex, x = incoming edge tangent, y up (0 = band level), +z = incoming outward normal. The plan polygon (layout.corner_profile_local) is bounded by the building line, +both panel bands' outer faces (offset frame_d) and the two band end lines at corner_c - joint/2 from the vertex, +so the piece sits one joint off each neighbouring panel. Convex (turn > 0) and reflex (turn < 0) share the builder. +""" +from __future__ import annotations + +from types import SimpleNamespace + +import simplecadapi as scad + +import layout as L +import panels as P + + +def corner_section(turn: float, corner_c: float, frame_d: float, joint: float) -> list[tuple[float, float]]: + return L.corner_profile_local(turn, SimpleNamespace(corner_c=corner_c, frame_d=frame_d, joint=joint)) + + +def build_corner(*, turn: float, h: float, corner_c: float, frame_d: float, joint: float): + pts = corner_section(turn, corner_c, frame_d, joint) + + # ---- feature: corner-post (build, profile=sketch) ---- + # XZ sketch: (u, v) -> corner-local (x, z); extruded up the band inside the joint + sk = P.polygon_loop(scad.make_sketch_rsketch(name="corner-post", plane="XZ"), "c", pts) + face = scad.make_face_from_sketch_rface(sketch=sk, profile="c_l0", require_fully_constrained=True) + post = scad.extrude_rsolid(profile=face, direction=(0.0, 1.0, 0.0), distance=h - joint) + return scad.translate_shape(shape=post, vector=(0.0, joint / 2.0, 0.0)) + + +def builder_args(spec: dict, p: L.FacadeParams) -> tuple: + return build_corner, dict(turn=spec["turn"], h=spec["h"], corner_c=p.corner_c, frame_d=p.frame_d, joint=p.joint) + + +def make_corner_part(type_id: str, spec: dict, p: L.FacadeParams): + builder, kwargs = builder_args(spec, p) + return scad.part(id=type_id, revision="1.0.0", project_root=P.ROOT)(builder)(**kwargs) diff --git a/examples/pcg_facade_tower/export.py b/examples/pcg_facade_tower/export.py new file mode 100644 index 00000000..e0793b82 --- /dev/null +++ b/examples/pcg_facade_tower/export.py @@ -0,0 +1,80 @@ +"""pcg_facade_tower export: product package, AP242 STEP (shared definitions), panel schedule CSV, hero render. + + ../../.venv/bin/python export.py [rect|chamfer|l_shape] [n_floors] + +out/.scadpkg the tower closure (tower -> bands -> panel / fin / corner types; slabs, canopy) +out/.step XCAF assembly: every definition written once, every occurrence a located reference +out/_panels.csv one row per facade piece type (type_id, family, sizes, count) — the fabrication list +out/.png iso from the south-west +""" +from __future__ import annotations + +import csv +import math +import sys +import time +from pathlib import Path + +import simplecadapi as scad + +import layout as L +import tower as T + +OUT = Path(__file__).resolve().parent / "out" +CSV_FIELDS = ("type_id", "family", "w_mm", "h_mm", "spandrel_mm", "fin_angle_deg", "corner_turn_deg", "count") + + +def panel_rows(sched: L.FacadeSchedule) -> list[dict]: + counts = sched.stats["instances_by_type"] + rows = [] + for tid in sorted(sched.types): + spec = sched.types[tid] + rows.append({"type_id": tid, "family": spec["family"], "w_mm": spec.get("w", ""), "h_mm": spec["h"], + "spandrel_mm": spec.get("spandrel", ""), "fin_angle_deg": spec.get("angle", ""), + "corner_turn_deg": spec.get("turn", ""), "count": counts[tid]}) + return rows + + +def write_csv(sched: L.FacadeSchedule, path: Path) -> Path: + with path.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=CSV_FIELDS) + writer.writeheader() + writer.writerows(panel_rows(sched)) + return path + + +def export(sched: L.FacadeSchedule, stem: str = "facade_tower", render: bool = True) -> dict: + OUT.mkdir(exist_ok=True) + timings = {} + start = time.perf_counter() + result = T.make_tower(sched) + timings["build"] = time.perf_counter() - start + package = OUT / f"{stem}.scadpkg" + start = time.perf_counter() + scad.capture(result, package, include_scene=False) # the scene projection of 3700 pieces is not needed here + timings["capture"] = time.perf_counter() - start + start = time.perf_counter() + step = scad.exporter.export_product_package_to_step(package, OUT / f"{stem}.step") + timings["step"] = time.perf_counter() - start + paths = {"scadpkg": package, "step": step.output_path, "csv": write_csv(sched, OUT / f"{stem}_panels.csv")} + if render: + start = time.perf_counter() + whole = scad.make_compound_from_assembly_rcompound(assembly=result.value).wrapped + paths["png"] = Path(scad.render_screenshot_rpath( + shapes=[whole], output_path=str(OUT / f"{stem}.png"), view=(20.0, 225.0), view_up=(0.0, 0.0, 1.0), + show_callouts=False, show_legend=False)) + timings["render"] = time.perf_counter() - start + return {"result": result, "step_report": step, "paths": paths, "timings": timings} + + +if __name__ == "__main__": + kind = L.PlanKind(sys.argv[1]) if len(sys.argv) > 1 else L.PlanKind.CHAMFER + n = int(sys.argv[2]) if len(sys.argv) > 2 else None + params = (L.default_params(plan_kind=kind) if n is None else + L.default_params(plan_kind=kind, n_floors=n, setback_every=math.ceil(n / 3))) + s = L.build_schedule(params) + out = export(s, stem="facade_tower" if n is None and kind is L.PlanKind.CHAMFER else f"facade_tower_{kind.value}_{s.params.n_floors}f") + for name, path in out["paths"].items(): + print(f"{name:8s} {path} ({Path(path).stat().st_size / 1e6:.1f} MB)") + print(f"{out['step_report'].occurrence_count} STEP occurrences, {len(out['step_report'].definition_ids)} definitions; " + + ", ".join(f"{k} {v:.1f}s" for k, v in out["timings"].items())) diff --git a/examples/pcg_facade_tower/floor.py b/examples/pcg_facade_tower/floor.py new file mode 100644 index 00000000..c0d04b7e --- /dev/null +++ b/examples/pcg_facade_tower/floor.py @@ -0,0 +1,67 @@ +"""pcg_facade_tower band subassemblies: one `scad.assemble` definition per schedule band. + +A band (tier x phase floor band, the lobby, a tier's parapet) is realized many times up the tower, so it is built once +in band-local coordinates (plan axes = world, z = 0 at the band's level) and the tower places it by level. +Contents: every panel, fin and corner piece of the band, placed from the layout frames and grounded (A2); the lobby +holds its stacked rows. Slabs and the canopy are placed by the tower, not here: a tier's first floor stands on the roof +below, so a slab is not a property of the floor band. + +Component ids: r{row}-e{edge}-s{slot} (panel), r{row}-e{edge}-s{slot}-fin, r{row}-v{vertex} (corner). +""" +from __future__ import annotations + +import simplecadapi as scad + +import corners as C +import layout as L +import panels as P + + +def band_assembly_id(band_id: str) -> str: + return f"band-{band_id}" + + +def band_components(sched: L.FacadeSchedule, band: dict) -> list[dict]: + """[{component_id, type_id, frame}] in band-local coordinates (layout frames at the in-band row offsets).""" + out = [] + for row in range(band["rows"]): + z = row * band["height"] + out += [{"component_id": f"r{row}-e{r['edge']}-s{r['slot']}", "type_id": r["type_id"], + "frame": L.panel_frame(sched, band, r, z)} for r in band["panels"]] + out += [{"component_id": f"r{row}-e{r['edge']}-s{r['slot']}-fin", "type_id": r["type_id"], + "frame": L.fin_frame(sched, band, r, z)} for r in band["fins"]] + out += [{"component_id": f"r{row}-v{r['vertex']}", "type_id": r["type_id"], + "frame": L.corner_frame(sched, band, r, z)} for r in band["corners"]] + return out + + +def type_parts(sched: L.FacadeSchedule, type_ids) -> dict: + """type id -> PartBuildResult (one part per type id; identical parameters rebuild identically).""" + out = {} + for tid in sorted(set(type_ids)): + spec = sched.types[tid] + make = C.make_corner_part if spec["family"] == "corner" else P.make_panel_part + out[tid] = make(tid, spec, sched.params) + return out + + +def make_band_assembly(sched: L.FacadeSchedule, band_id: str) -> scad.AssemblyBuildResult: + band = sched.band(band_id) + comps = band_components(sched, band) + parts = type_parts(sched, (c["type_id"] for c in comps)) + aid = band_assembly_id(band_id) + + @scad.assemble(id=aid, revision="1.0.0", definitions=tuple(parts.values()), project_root=P.ROOT) + def build(band_id: str) -> scad.Assembly: + # ---- feature: band-components (assemble) ---- + # every piece placed from its layout frame (x = edge tangent, y = up, z = outward) and grounded + asm = scad.make_assembly_rassembly(assembly_id=aid, name=f"facade band {band_id}") + for c in comps: + f = c["frame"] + asm = scad.add_component_rassembly( + assembly=asm, item=parts[c["type_id"]].value, component_id=c["component_id"], + placement=scad.make_placement_rplacement(origin=f["origin"], x_axis=f["x_axis"], y_axis=f["y_axis"])) + asm = scad.ground_component_rassembly(assembly=asm, component_id=c["component_id"]) + return asm + + return build(band_id=band_id) diff --git a/examples/pcg_facade_tower/layout.py b/examples/pcg_facade_tower/layout.py new file mode 100644 index 00000000..8b4305ce --- /dev/null +++ b/examples/pcg_facade_tower/layout.py @@ -0,0 +1,719 @@ +"""PCG layout engine: plan -> setback tiers -> per-edge module subdivision -> panel schedule. + +Pure data, no geometry. Every geometric file consumes the FacadeSchedule this module emits; this is the one +place where the procedural rules live (subdivision, closers, corners, fin wave field + quantization, ordered +dithering for the window-to-wall ratio, type and floor-type rationalization). + +Frames (BUILD_PLAN datum): + world Z up, +X east, +Y north, origin = ground level at the plan's SW corner; plan polygons are CCW. + edge i from vertex i to i+1, tangent t_i, outward normal n_i = (t_y, -t_x). + panel local x along the edge (0 = left module boundary), y up (0 = level), z outward (0 = building line). + placement: origin = V_i + s0*t_i + level*Z, x_axis = t_i, y_axis = Z -> z_axis = t_i x Z = n_i. + corner local x = incoming tangent, y up, z = incoming normal; origin = the building-line vertex. + fin panel orientation; origin = right-jamb centreline on the frame outer face (panel-local + (w - joint/2 - frame_sight/2, joint/2, frame_d)). Fins are separate site-attached Parts: fixed to the + panel, a 45 deg fin pushes the unit to ~1676 mm wide, past the 1600 transport envelope (V3). +""" +from __future__ import annotations + +import json +import math +from dataclasses import asdict, dataclass, field, replace +from enum import Enum +from typing import Any + +import simplecadapi as scad + +# ---- params ---- +PLAN_W = scad.var("plan_w", 36000.0, comment="平面 X 外包(东西)", unit="mm") +PLAN_D = scad.var("plan_d", 24000.0, comment="平面 Y 外包(南北)", unit="mm") +CHAMFER = scad.var("chamfer", 6000.0, comment="CHAMFER:NE 角沿两边各切", unit="mm") +NOTCH_W = scad.var("notch_w", 12000.0, comment="L_SHAPE:NE 缺口 X 宽", unit="mm") +NOTCH_D = scad.var("notch_d", 9000.0, comment="L_SHAPE:NE 缺口 Y 深", unit="mm") +N_FLOORS = scad.var("n_floors", 36, comment="lobby 之上的标准层数") +FLOOR_H = scad.var("floor_h", 3600.0, comment="标准层高", unit="mm") +LOBBY_H = scad.var("lobby_h", 5400.0, comment="首层 lobby 层高", unit="mm") +SETBACK_EVERY = scad.var("setback_every", 12, comment="每 tier 层数") +SETBACK_DEPTH = scad.var("setback_depth", 3000.0, comment="每 tier 四边内退", unit="mm") +PARAPET_H = scad.var("parapet_h", 1200.0, comment="女儿墙高", unit="mm") +W_STD = scad.var("w_std", 1500.0, comment="标准板模数宽(含缝)", unit="mm") +CLOSER_MIN = scad.var("closer_min", 450.0, comment="收口板最小模数宽(含缝)", unit="mm") +JOINT = scad.var("joint", 20.0, comment="板缝(四周各 joint/2)", unit="mm") +CORNER_C = scad.var("corner_c", 450.0, comment="角件沿各边占宽(自 building line 顶点)", unit="mm") +FRAME_D = scad.var("frame_d", 150.0, comment="框深(building line 外)", unit="mm") +FRAME_SIGHT = scad.var("frame_sight", 60.0, comment="框面宽", unit="mm") +SPANDREL_H = scad.var("spandrel_h", 1100.0, comment="层间实体区高", unit="mm") +GLASS_T = scad.var("glass_t", 24.0, comment="玻璃厚", unit="mm") +GLASS_RECESS = scad.var("glass_recess", 40.0, comment="玻璃外面距框外面凹进", unit="mm") +FIN_T = scad.var("fin_t", 40.0, comment="竖挺厚", unit="mm") +FIN_DEPTH = scad.var("fin_depth", 300.0, comment="竖挺伸出深度(自框外面)", unit="mm") +FIN_AMP = scad.var("fin_amp", 30.0, comment="波场幅值", unit="deg") +FIN_WAVELENGTH = scad.var("fin_wavelength", 18000.0, comment="波场沿周长波长", unit="mm") +FIN_PERIOD_FLOORS = scad.var("fin_period_floors", 4, comment="波场 / 抖动竖向周期(层),须为 4 的倍数") +FIN_BINS = scad.var("fin_bins", 6, comment="竖挺角量化档数 K(合理化旋钮)") +SUN_AZIMUTH = scad.var("sun_azimuth", 180.0, comment="太阳方位角(自北顺时针)", unit="deg") +SLAB_T = scad.var("slab_t", 300.0, comment="楼板厚", unit="mm") +SLAB_GAP = scad.var("slab_gap", 30.0, comment="楼板外沿距 building line 内缩", unit="mm") +CANOPY_MODULES = scad.var("canopy_modules", 6, comment="南向入口雨棚跨越的标准模数(奇偶自动对齐到板缝)") +CANOPY_DEPTH = scad.var("canopy_depth", 3000.0, comment="雨棚外挑(自板外面 + 缝)", unit="mm") +CANOPY_T = scad.var("canopy_t", 250.0, comment="雨棚厚", unit="mm") + +FIN_LIMIT = 45.0 # 竖挺角范围 [-45°, 45°](0 = 垂直立面) +SUN_THETA_LIMIT = 30.0 # 朝向基准角 θ_sun 的限幅 +FIN_FACING_MIN = -0.2 # n·sun > -0.2 的边带竖挺(E/W/S),其余(N、NE 切角)无挺 +TYPE_ROUND_DIGITS = 3 # 板型身份:宽/角四舍五入到 0.001(A7) +WWR_TOL = 0.03 +MAX_PANEL_H = 4200.0 # transport envelope height (V3); taller bands (lobby) split into stacked rows +BAYER4 = ((0, 8, 2, 10), (12, 4, 14, 6), (3, 11, 1, 9), (15, 7, 13, 5)) + + +class PlanKind(str, Enum): + RECT = "rect" + CHAMFER = "chamfer" + L_SHAPE = "l_shape" + + +class Band(str, Enum): + LOBBY = "lobby" + TOWER = "tower" + PARAPET = "parapet" + + +def _value(value: object) -> float: + evaluate = getattr(value, "evaluate", None) + return float(evaluate()) if callable(evaluate) else float(value) + + +@dataclass(frozen=True) +class FacadeParams: + plan_kind: PlanKind = PlanKind.CHAMFER + plan_w: float = _value(PLAN_W) + plan_d: float = _value(PLAN_D) + chamfer: float = _value(CHAMFER) + notch_w: float = _value(NOTCH_W) + notch_d: float = _value(NOTCH_D) + n_floors: int = int(_value(N_FLOORS)) + floor_h: float = _value(FLOOR_H) + lobby_h: float = _value(LOBBY_H) + setback_every: int = int(_value(SETBACK_EVERY)) + setback_depth: float = _value(SETBACK_DEPTH) + parapet_h: float = _value(PARAPET_H) + w_std: float = _value(W_STD) + closer_min: float = _value(CLOSER_MIN) + joint: float = _value(JOINT) + corner_c: float = _value(CORNER_C) + frame_d: float = _value(FRAME_D) + frame_sight: float = _value(FRAME_SIGHT) + spandrel_h: float = _value(SPANDREL_H) + glass_t: float = _value(GLASS_T) + glass_recess: float = _value(GLASS_RECESS) + fin_t: float = _value(FIN_T) + fin_depth: float = _value(FIN_DEPTH) + fin_amp: float = _value(FIN_AMP) + fin_wavelength: float = _value(FIN_WAVELENGTH) + fin_period_floors: int = int(_value(FIN_PERIOD_FLOORS)) + fin_bins: int = int(_value(FIN_BINS)) + sun_azimuth: float = _value(SUN_AZIMUTH) + slab_t: float = _value(SLAB_T) + slab_gap: float = _value(SLAB_GAP) + canopy_modules: int = int(_value(CANOPY_MODULES)) + canopy_depth: float = _value(CANOPY_DEPTH) + canopy_t: float = _value(CANOPY_T) + # glass / gross per orientation; ceiling ~0.54 (frames + spandrel + solid closers), south lowest for solar gain + wwr_target: tuple = (("N", 0.50), ("E", 0.40), ("S", 0.35), ("W", 0.40)) + + def to_dict(self) -> dict: + data = asdict(self) + data["plan_kind"] = self.plan_kind.value + data["wwr_target"] = dict(self.wwr_target) + return data + + +def default_params(**overrides: Any) -> FacadeParams: + return replace(FacadeParams(), **overrides) + + +# ---------------------------------------------------------------- guards + + +class LayoutError(ValueError): + """Infeasible parameter set (raised instead of silently degrading the layout).""" + + +def assert_params(p: FacadeParams) -> None: + def need(ok: bool, message: str) -> None: + if not ok: + raise LayoutError(message) + + positive = ("plan_w", "plan_d", "floor_h", "lobby_h", "parapet_h", "w_std", "closer_min", "joint", "corner_c", + "frame_d", "frame_sight", "glass_t", "fin_t", "fin_depth", "fin_wavelength", "slab_t") + for name in positive: + need(math.isfinite(getattr(p, name)) and getattr(p, name) > 0.0, f"{name} must be positive") + need(p.n_floors >= 1 and p.setback_every >= 1, "n_floors / setback_every must be >= 1") + need(p.setback_depth >= 0.0 and p.slab_gap >= 0.0, "setback_depth / slab_gap must be >= 0") + need(p.fin_bins >= 2, "fin_bins must be >= 2") + need(p.fin_period_floors >= 4 and p.fin_period_floors % 4 == 0, "fin_period_floors must be a multiple of 4") + need(p.closer_min > p.joint + 2.0 * p.frame_sight, "closer_min must leave a frame inside the joint") + need(p.closer_min < p.w_std, "closer_min must be < w_std") + need(p.corner_c > p.frame_d + p.joint, "corner_c must exceed frame_d + joint (concave corner bands overlap)") + need(p.spandrel_h + 2.0 * p.frame_sight + p.joint < p.floor_h, "spandrel_h leaves no vision opening") + need(p.glass_recess + p.glass_t < p.frame_d, "glass must sit inside the frame depth") + need(p.fin_t < p.w_std / 2.0, "fin_t too thick for the module") + need(p.floor_h <= MAX_PANEL_H and p.parapet_h <= MAX_PANEL_H, "floor_h / parapet_h exceed the transport height") + # slab occupies [level, level + slab_t] and must hide behind the spandrel (A3); the parapet hides the roof slab + need(p.slab_t < p.spandrel_h - p.frame_sight / 2.0, "slab_t shows below the spandrel") + need(p.slab_t < p.parapet_h - p.joint / 2.0 - p.frame_sight, "slab_t shows above the parapet") + need(p.slab_gap > 0.0, "slab_gap must keep the slab off the building line") + need(p.canopy_modules >= 1 and p.canopy_depth > 0.0 and p.canopy_t > 0.0, "canopy size") + need(p.canopy_t < p.lobby_h / math.ceil(p.lobby_h / MAX_PANEL_H - 1.0e-9), "canopy_t taller than a lobby row") + need(0.0 <= p.fin_amp <= FIN_LIMIT, "fin_amp must lie in [0, 45]") + if p.plan_kind is PlanKind.CHAMFER: + need(0.0 < p.chamfer < min(p.plan_w, p.plan_d), "chamfer must lie inside the plan") + if p.plan_kind is PlanKind.L_SHAPE: + need(0.0 < p.notch_w < p.plan_w and 0.0 < p.notch_d < p.plan_d, "notch must lie inside the plan") + need(all(0.0 < value <= 1.0 for _, value in p.wwr_target), "wwr targets must lie in (0, 1]") + + +# ---------------------------------------------------------------- plan + tiers + + +def base_plan(p: FacadeParams) -> list[tuple[float, float]]: + w, d = p.plan_w, p.plan_d + if p.plan_kind is PlanKind.RECT: + return [(0.0, 0.0), (w, 0.0), (w, d), (0.0, d)] + if p.plan_kind is PlanKind.CHAMFER: + c = p.chamfer + return [(0.0, 0.0), (w, 0.0), (w, d - c), (w - c, d), (0.0, d)] + nw, nd = p.notch_w, p.notch_d + return [(0.0, 0.0), (w, 0.0), (w, d - nd), (w - nw, d - nd), (w - nw, d), (0.0, d)] + + +def _sub(a, b): + return (a[0] - b[0], a[1] - b[1]) + + +def _add(a, b): + return (a[0] + b[0], a[1] + b[1]) + + +def _mul(a, k): + return (a[0] * k, a[1] * k) + + +def _dot(a, b): + return a[0] * b[0] + a[1] * b[1] + + +def _cross(a, b): + return a[0] * b[1] - a[1] * b[0] + + +def _unit(a): + n = math.hypot(*a) + return (a[0] / n, a[1] / n) + + +def edge_frames(poly: list) -> list[dict]: + """Per edge: start vertex, tangent, outward normal, length.""" + out = [] + for i, v0 in enumerate(poly): + v1 = poly[(i + 1) % len(poly)] + t = _unit(_sub(v1, v0)) + out.append({"v0": v0, "v1": v1, "t": t, "n": (t[1], -t[0]), "length": math.hypot(*_sub(v1, v0))}) + return out + + +def signed_area(poly: list) -> float: + return 0.5 * sum(_cross(poly[i], poly[(i + 1) % len(poly)]) for i in range(len(poly))) + + +def _segments_cross(a0, a1, b0, b1) -> bool: + d1, d2 = _cross(_sub(a1, a0), _sub(b0, a0)), _cross(_sub(a1, a0), _sub(b1, a0)) + d3, d4 = _cross(_sub(b1, b0), _sub(a0, b0)), _cross(_sub(b1, b0), _sub(a1, b0)) + return d1 * d2 < 0.0 and d3 * d4 < 0.0 + + +def is_simple(poly: list) -> bool: + n = len(poly) + for i in range(n): + for j in range(i + 2, n): + if i == 0 and j == n - 1: + continue + if _segments_cross(poly[i], poly[(i + 1) % n], poly[j], poly[(j + 1) % n]): + return False + return True + + +def offset_polygon(poly: list, d: float) -> list: + """Move every edge inward by d (edge lines shift along -n); vertex = intersection of adjacent offset lines.""" + frames = edge_frames(poly) + out = [] + for i in range(len(poly)): + n_in, n_out = frames[i - 1]["n"], frames[i]["n"] + k = 1.0 + _dot(n_in, n_out) + if k < 1.0e-6: + raise LayoutError("edge reversal: adjacent edges are anti-parallel") + out.append(_sub(poly[i], _mul(_add(n_in, n_out), d / k))) + return out + + +def tier_plans(p: FacadeParams) -> list[list]: + base = base_plan(p) + n_tiers = (p.n_floors + p.setback_every - 1) // p.setback_every + plans = [] + for k in range(n_tiers): + poly = offset_polygon(base, k * p.setback_depth) if k else base + before, after = edge_frames(base), edge_frames(poly) + for b, a in zip(before, after): + if _dot(b["t"], a["t"]) < 0.999999 or a["length"] <= 0.0: + raise LayoutError(f"tier {k}: setback collapses an edge") + if signed_area(poly) <= 0.0 or not is_simple(poly): + raise LayoutError(f"tier {k}: setback polygon is not simple / CCW") + plans.append(poly) + return plans + + +# ---------------------------------------------------------------- corners + + +def corner_turn(frames: list, i: int) -> float: + """Signed turn at vertex i (incoming edge i-1 -> outgoing edge i), degrees; + = convex (left turn).""" + t_in, t_out = frames[i - 1]["t"], frames[i]["t"] + return math.degrees(math.atan2(_cross(t_in, t_out), _dot(t_in, t_out))) + + +def corner_profile_local(turn_deg: float, p: FacadeParams, joint: float | None = None) -> list[tuple[float, float]]: + """Corner piece plan polygon in the corner frame (x = incoming tangent, second coord = incoming normal). + + Bounded by the building line (two reserves of corner_c - joint/2 each side of the vertex), the two panel-band + outer faces (offset frame_d) and the band end lines. Returned CCW in (x, n) coordinates.""" + j = p.joint if joint is None else joint + c, fd = p.corner_c - j / 2.0, p.frame_d + a = math.radians(turn_deg) + # Plan coords with the incoming edge along +x; a CCW plan puts the outward normal at -y. + t_in, n_in = (1.0, 0.0), (0.0, -1.0) + t_out = (math.cos(a), math.sin(a)) + n_out = (t_out[1], -t_out[0]) + k = 1.0 + _dot(n_in, n_out) + if k < 1.0e-6: + raise LayoutError("corner too sharp") + v = (0.0, 0.0) + a0 = _mul(t_in, -c) + a1 = _add(a0, _mul(n_in, fd)) + apex = _mul(_add(n_in, n_out), fd / k) + b1 = _add(_mul(t_out, c), _mul(n_out, fd)) + b0 = _mul(t_out, c) + poly = [a0, a1, apex, b1, b0, v] + if signed_area(poly) < 0.0: + poly.reverse() + if not is_simple(poly): + raise LayoutError(f"corner profile not simple at turn {turn_deg:.3f}") + # (x, y) -> (x, outward = -y); the mirror flips orientation, so reverse to stay CCW + local = [(x, -y) for x, y in poly] + return list(reversed(local)) + + +# ---------------------------------------------------------------- field + quantization + + +def fin_bin_angles(k: int) -> list[float]: + return [-FIN_LIMIT + 2.0 * FIN_LIMIT * i / (k - 1) for i in range(k)] + + +def quantize_fin(theta: float, k: int) -> tuple[int, float]: + theta = max(-FIN_LIMIT, min(FIN_LIMIT, theta)) + index = int(round((theta + FIN_LIMIT) / (2.0 * FIN_LIMIT) * (k - 1))) + return index, fin_bin_angles(k)[index] + + +def sun_vector(p: FacadeParams) -> tuple[float, float]: + az = math.radians(p.sun_azimuth) + return (math.sin(az), math.cos(az)) + + +def edge_has_fins(n, p: FacadeParams) -> bool: + return _dot(n, sun_vector(p)) > FIN_FACING_MIN + + +def theta_sun(n, p: FacadeParams) -> float: + s = sun_vector(p) + phi = math.degrees(math.atan2(_cross(n, s), _dot(n, s))) + return max(-SUN_THETA_LIMIT, min(SUN_THETA_LIMIT, phi)) + + +def orientation(n) -> str: + """N/E/S/W by outward normal; ties (45° edges) go to N/S (|ny| >= |nx|).""" + if abs(n[1]) >= abs(n[0]): + return "N" if n[1] > 0.0 else "S" + return "E" if n[0] > 0.0 else "W" + + +# ---------------------------------------------------------------- subdivision + + +def subdivide_edge(length: float, p: FacadeParams) -> dict: + """[corner c][closer][n x w_std][closer][corner c]; closers symmetric and in [closer_min, closer_min + w_std/2).""" + eff = length - 2.0 * p.corner_c + if eff < 2.0 * p.closer_min - 1.0e-9: + raise LayoutError(f"edge {length:.3f} too short for two corners and two closers") + n = int(math.floor((eff - 2.0 * p.closer_min) / p.w_std + 1.0e-9)) + closer = (eff - n * p.w_std) / 2.0 + widths = [closer] + [p.w_std] * n + [closer] + starts, s = [], p.corner_c + for w in widths: + starts.append(s) + s += w + return {"n_std": n, "closer": closer, "widths": widths, "starts": starts} + + +def _r(value: float) -> float: + return round(value, TYPE_ROUND_DIGITS) + + +def _mm(value: float) -> str: + text = f"{_r(value):.3f}".rstrip("0").rstrip(".") + return text.replace("-", "m") + + +def solid_type_id(w: float, h: float) -> str: + return f"solid-w{_mm(w)}-h{_mm(h)}" + + +def vision_type_id(w: float, h: float, spandrel: float) -> str: + return f"vision-w{_mm(w)}-h{_mm(h)}-s{_mm(spandrel)}" + + +def fin_type_id(angle: float, h: float) -> str: + return f"fin-a{_mm(angle)}-h{_mm(h)}" + + +def fin_root_s(s0: float, w: float, p: FacadeParams) -> float: + """Fin root along the edge: centreline of the panel's right jamb (A6).""" + return s0 + w - p.joint / 2.0 - p.frame_sight / 2.0 + + +def corner_type_id(turn: float, h: float) -> str: + return f"corner-t{_mm(turn)}-h{_mm(h)}" + + +# ---------------------------------------------------------------- schedule + + +@dataclass +class FacadeSchedule: + params: FacadeParams + tiers: list # tier polygons + levels: dict # band / floor -> level z + types: dict = field(default_factory=dict) # type_id -> spec + bands: list = field(default_factory=list) # band records (floor bands, lobby, parapets) + floor_types: dict = field(default_factory=dict) # floor_type_id -> {"tier", "phase", "band"} + floors: list = field(default_factory=list) # [{"floor", "tier", "level", "floor_type"}] + stats: dict = field(default_factory=dict) + vision_fraction: dict = field(default_factory=dict) # orientation -> share of standard modules that are vision + + def to_dict(self) -> dict: + return {"params": self.params.to_dict(), "tiers": self.tiers, "levels": self.levels, "types": self.types, + "bands": self.bands, "floor_types": self.floor_types, "floors": self.floors, "stats": self.stats, + "vision_fraction": self.vision_fraction} + + def to_json(self) -> str: + return json.dumps(self.to_dict(), indent=1, ensure_ascii=False) + + def band(self, band_id: str) -> dict: + return next(b for b in self.bands if b["band_id"] == band_id) + + +def _register(types: dict, type_id: str, spec: dict) -> str: + known = types.setdefault(type_id, spec) + if known != spec: + raise LayoutError(f"type id collision: {type_id}") + return type_id + + +def _glass_area(w: float, h: float, spandrel: float, p: FacadeParams) -> float: + """Design vision opening of a vision panel (module w x h). + + Horizontally inside the jambs; vertically from the transom (centred on the spandrel line) or, with no + spandrel, the sill frame, up to the head frame.""" + gw = w - p.joint - 2.0 * p.frame_sight + top = h - p.joint / 2.0 - p.frame_sight + bottom = spandrel + p.frame_sight / 2.0 if spandrel > 0.0 else p.joint / 2.0 + p.frame_sight + return gw * (top - bottom) + + +def _band_panels(tier_poly, band: Band, height: float, p: FacadeParams, types: dict, phase: int | None, + vision_cells: set | None) -> tuple[list, list, list]: + """Panels, corners and fins of one horizontal band on one tier polygon (level-independent: local to the band).""" + frames = edge_frames(tier_poly) + panels, corners, fins_out = [], [], [] + u0 = 0.0 + for e, fr in enumerate(frames): + sub = subdivide_edge(fr["length"], p) + fins, facing = edge_has_fins(fr["n"], p), orientation(fr["n"]) + for k, (s0, w) in enumerate(zip(sub["starts"], sub["widths"])): + is_closer = k in (0, len(sub["widths"]) - 1) + record = {"edge": e, "slot": k, "s0": s0, "w": w, "orientation": facing} + if band is Band.PARAPET or is_closer: + record.update(kind="solid", type_id=_register( + types, solid_type_id(w, height), {"family": "solid", "w": _r(w), "h": _r(height)})) + elif band is Band.LOBBY: + record.update(kind="vision", type_id=_register(types, vision_type_id(w, height, 0.0), { + "family": "vision", "w": _r(w), "h": _r(height), "spandrel": 0.0})) + else: + if (e, k) in vision_cells: + fin = None + if fins: + u = u0 + s0 + w / 2.0 + raw = theta_sun(fr["n"], p) + p.fin_amp * math.sin( + 2.0 * math.pi * (u / p.fin_wavelength + phase / p.fin_period_floors)) + _, fin = quantize_fin(raw, p.fin_bins) + record["fin_raw"] = raw + fins_out.append({"edge": e, "slot": k, "s": fin_root_s(s0, w, p), "angle": fin, + "type_id": _register(types, fin_type_id(fin, height), + {"family": "fin", "angle": _r(fin), "h": _r(height)})}) + record.update(kind="vision", fin=fin, type_id=_register( + types, vision_type_id(w, height, p.spandrel_h), + {"family": "vision", "w": _r(w), "h": _r(height), "spandrel": _r(p.spandrel_h)})) + else: + record.update(kind="solid", type_id=_register( + types, solid_type_id(w, height), {"family": "solid", "w": _r(w), "h": _r(height)})) + panels.append(record) + u0 += fr["length"] + for i in range(len(frames)): + turn = corner_turn(frames, i) + corners.append({"vertex": i, "turn": turn, "type_id": _register( + types, corner_type_id(turn, height), {"family": "corner", "turn": _r(turn), "h": _r(height)})}) + return panels, corners, fins_out + + +def _vision_cells(tier_poly, p: FacadeParams, fractions: dict) -> dict: + """phase -> {(edge, slot)} of standard modules that are vision on this tier. + + Ordered dithering by rank: every (phase, standard module) cell of one orientation is ranked by its 4x4 Bayer + threshold (column = running module index along that orientation, continuous across edges; row = phase), then + exactly round(fraction x cells) lowest-ranked cells become vision. Rank selection hits the count to one cell + (plain thresholding would quantize the fraction to 1/16) while keeping the Bayer spread.""" + cells: dict = {} + column: dict = {} + for e, fr in enumerate(edge_frames(tier_poly)): + o = orientation(fr["n"]) + for k in range(1, subdivide_edge(fr["length"], p)["n_std"] + 1): + bay = column[o] = column.get(o, -1) + 1 + for phase in range(p.fin_period_floors): + rank = (BAYER4[phase % 4][bay % 4], phase // 4, bay // 4) + cells.setdefault(o, []).append((rank, phase, e, k)) + chosen: dict = {phase: set() for phase in range(p.fin_period_floors)} + for o, items in cells.items(): + items.sort() + for _, phase, e, k in items[:int(round(fractions[o] * len(items)))]: + chosen[phase].add((e, k)) + return chosen + + +def _vision_fractions(tiers: list, floors_per_tier: list, p: FacadeParams) -> dict: + """Per orientation: fraction of standard modules that are vision. + + Pooled over the whole tower (every edge of that orientation on every floor): short chamfer / notch / upper-tier + edges carry proportionally more solid closer area, so a per-edge or per-tier target is often unreachable while + the orientation total (what V4 measures) is not.""" + targets = dict(p.wwr_target) + g = _glass_area(p.w_std, p.floor_h, p.spandrel_h, p) + gross, n_std = {}, {} + for poly, reps in zip(tiers, floors_per_tier): + for fr in edge_frames(poly): + o = orientation(fr["n"]) + gross[o] = gross.get(o, 0.0) + reps * (fr["length"] - 2.0 * p.corner_c) * p.floor_h + n_std[o] = n_std.get(o, 0) + reps * subdivide_edge(fr["length"], p)["n_std"] + fraction = {} + for o in gross: + fraction[o] = targets[o] * gross[o] / g / n_std[o] if n_std[o] else 0.0 + if fraction[o] > 1.0: + raise LayoutError(f"WWR target {o}={targets[o]} unreachable: needs {fraction[o]:.3f} of modules as vision") + return fraction + + +def build_schedule(p: FacadeParams | None = None) -> FacadeSchedule: + p = p or default_params() + assert_params(p) + tiers = tier_plans(p) + types: dict = {} + levels = {"lobby": 0.0} + for f in range(1, p.n_floors + 1): + levels[f"floor{f}"] = p.lobby_h + (f - 1) * p.floor_h + sched = FacadeSchedule(params=p, tiers=[list(map(list, t)) for t in tiers], levels=levels) + + rows = math.ceil(p.lobby_h / MAX_PANEL_H - 1.0e-9) + panels, corners, fins = _band_panels(tiers[0], Band.LOBBY, p.lobby_h / rows, p, types, None, None) + sched.bands.append({"band_id": "lobby", "band": Band.LOBBY.value, "tier": 0, "height": p.lobby_h / rows, + "rows": rows, "level": 0.0, "panels": panels, "corners": corners, "fins": fins}) + floors_per_tier = [sum(1 for f in range(1, p.n_floors + 1) if (f - 1) // p.setback_every == k) + for k in range(len(tiers))] + fractions = _vision_fractions(tiers, floors_per_tier, p) + for k, poly in enumerate(tiers): + vision = _vision_cells(poly, p, fractions) + for phase in range(p.fin_period_floors): + floors = [f for f in range(1, p.n_floors + 1) + if (f - 1) // p.setback_every == k and (f - 1) % p.fin_period_floors == phase] + if not floors: + continue + band_id = f"tier{k}-phase{phase}" + panels, corners, fins = _band_panels(poly, Band.TOWER, p.floor_h, p, types, phase, vision[phase]) + sched.bands.append({"band_id": band_id, "band": Band.TOWER.value, "tier": k, "phase": phase, + "height": p.floor_h, "rows": 1, "panels": panels, "corners": corners, "fins": fins}) + sched.floor_types[band_id] = {"tier": k, "phase": phase, "floors": floors} + panels, corners, fins = _band_panels(poly, Band.PARAPET, p.parapet_h, p, types, None, None) + top = max(f for f in range(1, p.n_floors + 1) if (f - 1) // p.setback_every == k) + levels[f"roof{k}"] = p.lobby_h + top * p.floor_h + sched.bands.append({"band_id": f"parapet{k}", "band": Band.PARAPET.value, "tier": k, "height": p.parapet_h, + "rows": 1, "panels": panels, "corners": corners, "fins": fins, "level": levels[f"roof{k}"]}) + for f in range(1, p.n_floors + 1): + k, phase = (f - 1) // p.setback_every, (f - 1) % p.fin_period_floors + sched.floors.append({"floor": f, "tier": k, "level": levels[f"floor{f}"], "floor_type": f"tier{k}-phase{phase}"}) + sched.vision_fraction = fractions + sched.types = types + sched.stats = schedule_stats(sched) + return sched + + +# ---------------------------------------------------------------- design-side statistics (S1 evidence) + + +def band_instances(sched: FacadeSchedule, band_id: str) -> int: + """How many times a band is realized (floor bands repeat per floor; lobby per stacked row; parapet once).""" + b = sched.band(band_id) + return len(sched.floor_types[band_id]["floors"]) if b["band"] == Band.TOWER.value else b["rows"] + + +def design_wwr(sched: FacadeSchedule) -> dict: + """Glass / gross area per orientation over the tower floor bands (design values; S5 re-measures on geometry).""" + p = sched.params + glass, gross = {}, {} + for b in sched.bands: + if b["band"] != Band.TOWER.value: + continue + reps = band_instances(sched, b["band_id"]) + frames = edge_frames(sched.tiers[b["tier"]]) + for e, fr in enumerate(frames): + o = orientation(fr["n"]) + gross[o] = gross.get(o, 0.0) + reps * (fr["length"] - 2.0 * p.corner_c) * p.floor_h + for rec in b["panels"]: + if rec["kind"] == "vision": + glass[rec["orientation"]] = glass.get(rec["orientation"], 0.0) + reps * _glass_area( + rec["w"], p.floor_h, p.spandrel_h, p) + return {o: glass.get(o, 0.0) / gross[o] for o in sorted(gross)} + + +def schedule_stats(sched: FacadeSchedule) -> dict: + counts: dict = {} + for b in sched.bands: + reps = band_instances(sched, b["band_id"]) + for rec in b["panels"] + b["corners"] + b["fins"]: + counts[rec["type_id"]] = counts.get(rec["type_id"], 0) + reps + fam = {} + for tid, spec in sched.types.items(): + fam[spec["family"]] = fam.get(spec["family"], 0) + 1 + p = sched.params + std_types = sorted(t for t, s in sched.types.items() + if s["family"] in ("vision", "solid") and s["w"] == _r(p.w_std) and s["h"] == _r(p.floor_h)) + fin_types = sorted(t for t, s in sched.types.items() if s["family"] == "fin") + fin_err = [abs(max(-FIN_LIMIT, min(FIN_LIMIT, rec["fin_raw"])) - rec["fin"]) + for b in sched.bands for rec in b["panels"] if "fin_raw" in rec] + return { + "n_types": len(sched.types), + "types_by_family": fam, + "standard_panel_types": std_types, + "fin_types": fin_types, + "n_instances": sum(counts.values()), + "instances_by_type": dict(sorted(counts.items())), + "n_floor_types": len(sched.floor_types), + "n_floors": sched.params.n_floors, + "n_tiers": len(sched.tiers), + "design_wwr": design_wwr(sched), + "max_fin_quant_error": max(fin_err) if fin_err else 0.0, + "height": sched.levels[f"roof{len(sched.tiers) - 1}"] + sched.params.parapet_h, + } + + +if __name__ == "__main__": + s = build_schedule() + print(json.dumps({k: v for k, v in s.stats.items() if k != "instances_by_type"}, indent=1)) + + +# ---------------------------------------------------------------- world frames (placement data; y_axis is always +Z) + + +def _frame(origin2, level: float, t) -> dict: + return {"origin": (origin2[0], origin2[1], level), "x_axis": (t[0], t[1], 0.0), "y_axis": (0.0, 0.0, 1.0)} + + +def band_levels(sched: FacadeSchedule, band_id: str) -> list[float]: + """Base level of every realization of a band (per floor; per lobby row; the parapet's roof).""" + b = sched.band(band_id) + if b["band"] == Band.TOWER.value: + return [sched.levels[f"floor{f}"] for f in sched.floor_types[band_id]["floors"]] + if b["band"] == Band.LOBBY.value: + return [row * b["height"] for row in range(b["rows"])] + return [b["level"]] + + +def panel_frame(sched: FacadeSchedule, band: dict, rec: dict, level: float) -> dict: + fr = edge_frames(sched.tiers[band["tier"]])[rec["edge"]] + return _frame(_add(fr["v0"], _mul(fr["t"], rec["s0"])), level, fr["t"]) + + +def fin_frame(sched: FacadeSchedule, band: dict, rec: dict, level: float) -> dict: + fr = edge_frames(sched.tiers[band["tier"]])[rec["edge"]] + root = _add(_add(fr["v0"], _mul(fr["t"], rec["s"])), _mul(fr["n"], sched.params.frame_d)) + return _frame(root, level, fr["t"]) + + +def corner_frame(sched: FacadeSchedule, band: dict, rec: dict, level: float) -> dict: + """x = incoming edge tangent, origin = the building-line vertex.""" + poly = sched.tiers[band["tier"]] + frames = edge_frames(poly) + return _frame(tuple(poly[rec["vertex"]]), level, frames[rec["vertex"] - 1]["t"]) + + +# ---------------------------------------------------------------- slabs + canopy (data) + + +def slab_plans(sched: FacadeSchedule) -> list: + """Per tier: the building line pulled in by slab_gap (floor slabs and that tier's roof share it).""" + return [offset_polygon([tuple(v) for v in poly], sched.params.slab_gap) for poly in sched.tiers] + + +def slab_instances(sched: FacadeSchedule) -> list[dict]: + """Slab occupies [level, level + slab_t]: floor slabs from their floor level (behind the spandrel), roof slabs + below the roof level (top = the roof / terrace surface, behind the top floor's head). A tier's first floor stands + on the tier below's roof, so it gets no slab of its own; its panels start j/2 above that roof, which a floor-style + slab at the roof level would cut through outside the upper building line.""" + p = sched.params + out = [{"tier": 0, "level": 0.0, "role": "ground"}] + for rec in sched.floors: + if rec["tier"] > 0 and (rec["floor"] - 1) % p.setback_every == 0: + continue + out.append({"tier": rec["tier"], "level": rec["level"], "role": f"floor{rec['floor']}"}) + for k in range(len(sched.tiers)): + out.append({"tier": k, "level": sched.levels[f"roof{k}"] - p.slab_t, "role": f"roof{k}"}) + return out + + +def canopy_box(sched: FacadeSchedule) -> dict: + """South entrance canopy over the lobby's central modules on edge 0, snapped to module joints. + Plan x in [x0, x1] along edge 0, y in [-(frame_d + joint + depth), -(frame_d + joint)], z = [z0, z0 + canopy_t] + with z0 on the lobby row joint.""" + p = sched.params + lobby = sched.band("lobby") + fr = edge_frames(sched.tiers[0])[0] + if abs(fr["t"][0] - 1.0) > 1.0e-12 or fr["n"][1] > -1.0 + 1.0e-12: + raise LayoutError("edge 0 is not the south edge") + inner = sorted((r for r in lobby["panels"] if r["edge"] == 0), key=lambda r: r["slot"])[1:-1] + m = p.canopy_modules if (len(inner) - p.canopy_modules) % 2 == 0 else p.canopy_modules - 1 + if m < 1 or m > len(inner): + raise LayoutError(f"canopy spans {m} of {len(inner)} south modules") + first = (len(inner) - m) // 2 + x0 = fr["v0"][0] + inner[first]["s0"] + x1 = fr["v0"][0] + inner[first + m - 1]["s0"] + inner[first + m - 1]["w"] + y1 = fr["v0"][1] - (p.frame_d + p.joint) + z0 = lobby["height"] if lobby["rows"] > 1 else lobby["height"] - p.canopy_t + return {"x0": x0, "x1": x1, "y0": y1 - p.canopy_depth, "y1": y1, "z0": z0, "z1": z0 + p.canopy_t, "modules": m} diff --git a/examples/pcg_facade_tower/panels.py b/examples/pcg_facade_tower/panels.py new file mode 100644 index 00000000..4401a691 --- /dev/null +++ b/examples/pcg_facade_tower/panels.py @@ -0,0 +1,209 @@ +"""pcg_facade_tower panel family: one Part per panel type id from the layout schedule. + +Panel-local frame (BUILD_PLAN): x along the building edge (0 = left joint centreline), y up (0 = floor level), +z outward (0 = building line). The module is [0, w] x [0, h]; the unit body sits inside the joint, +[j/2, w - j/2] x [j/2, h - j/2] x [0, frame_d]. + +Fin-local frame: same axes as the panel; origin = right-jamb centreline on the frame outer face. +Fins are separate site-attached Parts (see layout.py header): theta > 0 tilts the tip toward +x. + +Families + vision frame ring (vision opening + spandrel opening when spandrel > 0) + recessed glass + spandrel infill + solid frame ring (one opening) + opaque infill (closers, parapets, the Bayer-solid modules) + fin parallelogram blade: root flush on the frame face, sides at theta, thickness fin_t, length fin_depth +""" +from __future__ import annotations + +import math +from pathlib import Path + +import simplecadapi as scad +from simplecadapi import ql + +import layout as L + +ROOT = Path(__file__).resolve().parent +GLASS_TAG = "facade.vision_glass" +PLANE_TOL = 1.0e-3 + + +# ---------------------------------------------------------------- sketch helpers + + +def _rect_loop(sk, key: str, x0: float, y0: float, x1: float, y1: float): + """Closed, fully constrained rectangle loop; profile id = f"{key}_lb" (its first line).""" + for pid, x, y in (("a", x0, y0), ("b", x1, y0), ("c", x1, y1), ("d", x0, y1)): + sk = scad.add_point_rsketch(sketch=sk, point_id=f"{key}_{pid}", x=x, y=y) + for lid, s, e in (("lb", "a", "b"), ("lr", "b", "c"), ("lt", "c", "d"), ("ll", "d", "a")): + sk = scad.add_line_rsketch(sketch=sk, entity_id=f"{key}_{lid}", start=f"{key}_{s}", end=f"{key}_{e}") + sk = scad.constrain_fix_rsketch(sketch=sk, target=f"{key}_a") + sk = scad.constrain_horizontal_rsketch(sketch=sk, line=f"{key}_lb") + sk = scad.constrain_vertical_rsketch(sketch=sk, line=f"{key}_lr") + sk = scad.constrain_horizontal_rsketch(sketch=sk, line=f"{key}_lt") + sk = scad.constrain_vertical_rsketch(sketch=sk, line=f"{key}_ll") + sk = scad.constrain_distance_x_rsketch(sketch=sk, a=f"{key}_a", b=f"{key}_b", value=x1 - x0) + sk = scad.constrain_distance_y_rsketch(sketch=sk, a=f"{key}_a", b=f"{key}_d", value=y1 - y0) + return sk + + +def polygon_loop(sk, key: str, pts): + """Closed polygon loop, fully constrained: first vertex fixed, the rest dimensioned x / y from it. + Profile id = f"{key}_l0".""" + n = len(pts) + for i, (x, y) in enumerate(pts): + sk = scad.add_point_rsketch(sketch=sk, point_id=f"{key}_p{i}", x=x, y=y) + for i in range(n): + sk = scad.add_line_rsketch(sketch=sk, entity_id=f"{key}_l{i}", start=f"{key}_p{i}", end=f"{key}_p{(i + 1) % n}") + sk = scad.constrain_fix_rsketch(sketch=sk, target=f"{key}_p0") + for i in range(1, n): + sk = scad.constrain_distance_x_rsketch(sketch=sk, a=f"{key}_p0", b=f"{key}_p{i}", value=pts[i][0] - pts[0][0]) + sk = scad.constrain_distance_y_rsketch(sketch=sk, a=f"{key}_p0", b=f"{key}_p{i}", value=pts[i][1] - pts[0][1]) + return sk + + +def _rect_plate(name: str, x0: float, y0: float, x1: float, y1: float, z0: float, z1: float): + """profile=sketch: one constrained rectangle extruded from z0 to z1.""" + sk = _rect_loop(scad.make_sketch_rsketch(name=name, plane="XY"), "o", x0, y0, x1, y1) + face = scad.make_face_from_sketch_rface(sketch=sk, profile="o_lb", require_fully_constrained=True) + plate = scad.extrude_rsolid(profile=face, direction=(0.0, 0.0, 1.0), distance=z1 - z0) + return scad.translate_shape(shape=plate, vector=(0.0, 0.0, z0)) + + +# ---------------------------------------------------------------- design openings (shared with the verifier) + + +def openings(w: float, h: float, spandrel: float, frame_sight: float, joint: float) -> dict: + """Clear openings in panel-local (x0, y0, x1, y1). 'vision' only when the panel carries glass.""" + j, s = joint, frame_sight + x0, x1 = j / 2.0 + s, w - j / 2.0 - s + top = h - j / 2.0 - s + out = {} + if spandrel > 0.0: + out["vision"] = (x0, spandrel + s / 2.0, x1, top) + out["spandrel"] = (x0, j / 2.0 + s, x1, spandrel - s / 2.0) + else: + out["vision"] = (x0, j / 2.0 + s, x1, top) + return out + + +def _grow(r, d: float): + return (r[0] - d, r[1] - d, r[2] + d, r[3] + d) + + +def _frame_ring(w, h, holes: dict, frame_d, joint): + j = joint + sk = _rect_loop(scad.make_sketch_rsketch(name="frame-ring", plane="XY"), "o", j / 2.0, j / 2.0, w - j / 2.0, + h - j / 2.0) + for key, r in holes.items(): + sk = _rect_loop(sk, key, *r) + face = scad.make_face_from_sketch_rface(sketch=sk, profile="o_lb", inner_profiles=tuple(f"{k}_lb" for k in holes), + require_fully_constrained=True) + return scad.extrude_rsolid(profile=face, direction=(0.0, 0.0, 1.0), distance=frame_d) + + +def _tag_front_face(body, rect, z: float, tag: str): + """Resolve the +Z planar face at z whose centre lies inside rect, then tag it (exactly one).""" + x0, y0, x1, y1 = rect + faces = ql.faces().where(ql.and_( + ql.prop("geom.normal.z", ">=", 1.0 - 1.0e-6), + ql.prop("geom.center.z", ">=", z - PLANE_TOL), ql.prop("geom.center.z", "<=", z + PLANE_TOL), + ql.prop("geom.center.x", ">=", x0), ql.prop("geom.center.x", "<=", x1), + ql.prop("geom.center.y", ">=", y0), ql.prop("geom.center.y", "<=", y1), + )).resolve(body) + if len(faces) != 1: + raise ValueError(f"{tag}: expected one glass face at z={z}, found {len(faces)}") + return scad.apply_tag_rselection(scope=body, targets=list(faces), tag=tag) + + +# ---------------------------------------------------------------- builders (every dimension is a builder argument) + + +def build_vision(*, w: float, h: float, spandrel: float, frame_d: float, frame_sight: float, joint: float, + glass_t: float, glass_recess: float): + s = frame_sight + ops = openings(w, h, spandrel, frame_sight, joint) + glass_z1 = frame_d - glass_recess + glass_z0 = glass_z1 - glass_t + + # ---- feature: frame-ring (build, profile=sketch) ---- + body = _frame_ring(w, h, ops, frame_d, joint) + + # ---- feature: spandrel-infill (add, profile=sketch) ---- + # opaque infill sits half a recess proud of the glass so it reads as a separate plane + if "spandrel" in ops: + body = scad.union_rsolid(body, _rect_plate("spandrel-infill", *_grow(ops["spandrel"], s / 2.0), glass_z0, + frame_d - glass_recess / 2.0)) + + # ---- feature: vision-glass (add, profile=sketch) ---- + # grown by s/2 into the jambs so the plate bonds to the ring; its exposed front = the clear opening + body = scad.union_rsolid(body, _rect_plate("vision-glass", *_grow(ops["vision"], s / 2.0), glass_z0, glass_z1)) + + # ---- feature: vision-glass-name (annotate) ---- + return _tag_front_face(body, ops["vision"], glass_z1, GLASS_TAG) + + +def build_solid(*, w: float, h: float, frame_d: float, frame_sight: float, joint: float, glass_t: float, + glass_recess: float): + s = frame_sight + opening = openings(w, h, 0.0, frame_sight, joint)["vision"] + + # ---- feature: frame-ring (build, profile=sketch) ---- + body = _frame_ring(w, h, {"op": opening}, frame_d, joint) + + # ---- feature: opaque-infill (add, profile=sketch) ---- + return scad.union_rsolid(body, _rect_plate("opaque-infill", *_grow(opening, s / 2.0), + frame_d - glass_recess - glass_t, frame_d - glass_recess / 2.0)) + + +def fin_section(angle: float, fin_t: float, fin_depth: float) -> list[tuple[float, float]]: + """Parallelogram in fin-local (x, z): root on z = 0 centred on x = 0, sides along (sin a, cos a).""" + th = math.radians(angle) + half = fin_t / (2.0 * math.cos(th)) + dx, dz = fin_depth * math.sin(th), fin_depth * math.cos(th) + return [(-half, 0.0), (half, 0.0), (half + dx, dz), (-half + dx, dz)] + + +def build_fin(*, angle: float, h: float, joint: float, fin_t: float, fin_depth: float): + (r0x, _), (r1x, _), (t1x, t1z), (t0x, _) = fin_section(angle, fin_t, fin_depth) + + # ---- feature: fin-blade (build, profile=sketch) ---- + # XZ sketch: (u, v) -> world (x, z); explicit x / z offsets keep the sign of the angle unambiguous + sk = scad.make_sketch_rsketch(name="fin-blade", plane="XZ") + for pid, x, z in (("r0", r0x, 0.0), ("r1", r1x, 0.0), ("t1", t1x, t1z), ("t0", t0x, t1z)): + sk = scad.add_point_rsketch(sketch=sk, point_id=pid, x=x, y=z) + for lid, a, b in (("root", "r0", "r1"), ("side_r", "r1", "t1"), ("tip", "t1", "t0"), ("side_l", "t0", "r0")): + sk = scad.add_line_rsketch(sketch=sk, entity_id=lid, start=a, end=b) + sk = scad.constrain_fix_rsketch(sketch=sk, target="r0") + sk = scad.constrain_horizontal_rsketch(sketch=sk, line="root") + sk = scad.constrain_horizontal_rsketch(sketch=sk, line="tip") + sk = scad.constrain_distance_x_rsketch(sketch=sk, a="r0", b="r1", value=r1x - r0x) + sk = scad.constrain_distance_x_rsketch(sketch=sk, a="r0", b="t0", value=t0x - r0x) + sk = scad.constrain_distance_y_rsketch(sketch=sk, a="r0", b="t0", value=t1z) + sk = scad.constrain_distance_x_rsketch(sketch=sk, a="t0", b="t1", value=t1x - t0x) + face = scad.make_face_from_sketch_rface(sketch=sk, profile="root", require_fully_constrained=True) + blade = scad.extrude_rsolid(profile=face, direction=(0.0, 1.0, 0.0), distance=h - joint) + return scad.translate_shape(shape=blade, vector=(0.0, joint / 2.0, 0.0)) + + +# ---------------------------------------------------------------- type table -> Part + + +def builder_args(spec: dict, p: L.FacadeParams) -> tuple: + """(builder, kwargs) for one type spec. Every value that shapes the solid is an argument (build_key).""" + common = dict(frame_d=p.frame_d, frame_sight=p.frame_sight, joint=p.joint, glass_t=p.glass_t, + glass_recess=p.glass_recess) + fam = spec["family"] + if fam == "vision": + return build_vision, dict(w=spec["w"], h=spec["h"], spandrel=spec["spandrel"], **common) + if fam == "solid": + return build_solid, dict(w=spec["w"], h=spec["h"], **common) + if fam == "fin": + return build_fin, dict(angle=spec["angle"], h=spec["h"], joint=p.joint, fin_t=p.fin_t, + fin_depth=p.fin_depth) + raise ValueError(f"not a panel family: {fam}") + + +def make_panel_part(type_id: str, spec: dict, p: L.FacadeParams): + """Part factory: part id == type id — equal parameters are the same part identity, rebuilt identically.""" + builder, kwargs = builder_args(spec, p) + return scad.part(id=type_id, revision="1.0.0", project_root=ROOT)(builder)(**kwargs) diff --git a/examples/pcg_facade_tower/slabs.py b/examples/pcg_facade_tower/slabs.py new file mode 100644 index 00000000..6d04488b --- /dev/null +++ b/examples/pcg_facade_tower/slabs.py @@ -0,0 +1,54 @@ +"""pcg_facade_tower slabs and the entrance canopy. + +Slab-local frame = world axes, z = 0 at the slab's level: each tier's building line pulled in by slab_gap, +extruded slab_t up (A3: the slab occupies [level, level + slab_t]; floor slabs from the floor level, roof slabs +ending at the roof; `layout.slab_instances`). +Floor slabs and the tier's roof share one Part per tier; placement only translates by the level. +Canopy-local frame = world (it is built where it stands; placed with the identity). +""" +from __future__ import annotations + +import simplecadapi as scad + +import layout as L +import panels as P + + +def slab_type_id(tier: int) -> str: + return f"slab-tier{tier}" + + +CANOPY_ID = "canopy-south" + + +def build_slab(*, plan: tuple, slab_t: float): + # ---- feature: slab-plate (build, profile=sketch) ---- + sk = P.polygon_loop(scad.make_sketch_rsketch(name="slab-plate", plane="XY"), "s", plan) + face = scad.make_face_from_sketch_rface(sketch=sk, profile="s_l0", require_fully_constrained=True) + return scad.extrude_rsolid(profile=face, direction=(0.0, 0.0, 1.0), distance=slab_t) + + +def build_canopy(*, x0: float, x1: float, y0: float, y1: float, z0: float, z1: float): + # ---- feature: canopy-plate (build, profile=sketch) ---- + sk = P._rect_loop(scad.make_sketch_rsketch(name="canopy-plate", plane="XY"), "o", x0, y0, x1, y1) + face = scad.make_face_from_sketch_rface(sketch=sk, profile="o_lb", require_fully_constrained=True) + plate = scad.extrude_rsolid(profile=face, direction=(0.0, 0.0, 1.0), distance=z1 - z0) + return scad.translate_shape(shape=plate, vector=(0.0, 0.0, z0)) + + +def slab_args(sched: L.FacadeSchedule, tier: int) -> dict: + plan = tuple((round(x, 6), round(y, 6)) for x, y in L.slab_plans(sched)[tier]) + return dict(plan=plan, slab_t=sched.params.slab_t) + + +def canopy_args(sched: L.FacadeSchedule) -> dict: + box = L.canopy_box(sched) + return {k: box[k] for k in ("x0", "x1", "y0", "y1", "z0", "z1")} + + +def make_slab_part(sched: L.FacadeSchedule, tier: int): + return scad.part(id=slab_type_id(tier), revision="1.0.0", project_root=P.ROOT)(build_slab)(**slab_args(sched, tier)) + + +def make_canopy_part(sched: L.FacadeSchedule): + return scad.part(id=CANOPY_ID, revision="1.0.0", project_root=P.ROOT)(build_canopy)(**canopy_args(sched)) diff --git a/examples/pcg_facade_tower/tower.py b/examples/pcg_facade_tower/tower.py new file mode 100644 index 00000000..b2ccc5e4 --- /dev/null +++ b/examples/pcg_facade_tower/tower.py @@ -0,0 +1,76 @@ +"""pcg_facade_tower tower assembly: band subassembly instances by level + slabs + the entrance canopy. + +Tower frame = world. Band subassemblies are band-local (plan axes = world, z = 0 at the band's level), so every +instance is a pure translation to its level: the lobby at 0 (it holds both rows), each floor at its floor level +(floor type tier{k}-phase{j}), each tier's parapet at its roof. Slabs translate by their instance level +(`layout.slab_instances`); the canopy is built where it stands. Everything is grounded (A2). + +Component ids: lobby, floor{f}, parapet{k}, slab-{role}, canopy. + + ../../.venv/bin/python tower.py [rect|chamfer|l_shape] # build + print counts +""" +from __future__ import annotations + +import sys +import time + +import simplecadapi as scad + +import floor as F +import layout as L +import panels as P +import slabs as S + +TOWER_ID = "facade-tower" + + +def tower_components(sched: L.FacadeSchedule) -> list[dict]: + """[{component_id, ref, level}]: ref = ("band", band_id) | ("slab", tier) | ("canopy", None).""" + out = [{"component_id": "lobby", "ref": ("band", "lobby"), "level": 0.0}] + out += [{"component_id": f"floor{r['floor']}", "ref": ("band", r["floor_type"]), "level": r["level"]} + for r in sched.floors] + out += [{"component_id": b["band_id"], "ref": ("band", b["band_id"]), "level": b["level"]} + for b in sched.bands if b["band"] == L.Band.PARAPET.value] + out += [{"component_id": f"slab-{i['role']}", "ref": ("slab", i["tier"]), "level": i["level"]} + for i in L.slab_instances(sched)] + out.append({"component_id": "canopy", "ref": ("canopy", None), "level": 0.0}) + return out + + +def definitions(sched: L.FacadeSchedule, comps: list[dict]) -> dict: + """ref -> build result (band AssemblyBuildResult / slab or canopy PartBuildResult), each built once.""" + out = {} + for ref in dict.fromkeys(c["ref"] for c in comps): + kind, key = ref + out[ref] = (F.make_band_assembly(sched, key) if kind == "band" + else S.make_slab_part(sched, key) if kind == "slab" else S.make_canopy_part(sched)) + return out + + +def make_tower(sched: L.FacadeSchedule) -> scad.AssemblyBuildResult: + comps = tower_components(sched) + defs = definitions(sched, comps) + + @scad.assemble(id=TOWER_ID, revision="1.0.0", definitions=tuple(defs.values()), project_root=P.ROOT) + def build(plan_kind: str, n_floors: int) -> scad.Assembly: + # ---- feature: tower-stack (assemble) ---- + # band instances, slabs and canopy translated to their levels (world axes) and grounded + asm = scad.make_assembly_rassembly(assembly_id=TOWER_ID, name=f"facade tower {plan_kind} {n_floors}F") + for c in comps: + asm = scad.add_component_rassembly( + assembly=asm, item=defs[c["ref"]].value, component_id=c["component_id"], + placement=scad.make_placement_rplacement(origin=(0.0, 0.0, c["level"]), x_axis=(1.0, 0.0, 0.0), + y_axis=(0.0, 1.0, 0.0))) + asm = scad.ground_component_rassembly(assembly=asm, component_id=c["component_id"]) + return asm + + return build(plan_kind=sched.params.plan_kind.value, n_floors=sched.params.n_floors) + + +if __name__ == "__main__": + kind = L.PlanKind(sys.argv[1]) if len(sys.argv) > 1 else L.PlanKind.CHAMFER + start = time.perf_counter() + s = L.build_schedule(L.default_params(plan_kind=kind)) + r = make_tower(s) + print(f"{kind.value}: {len(r.value.components)} tower components, {len(r.definition.definition_refs)} definitions, " + f"{s.stats['n_instances']} facade pieces, {time.perf_counter() - start:.1f}s") diff --git a/examples/pcg_facade_tower/verify/_common.py b/examples/pcg_facade_tower/verify/_common.py new file mode 100644 index 00000000..5bcd9273 --- /dev/null +++ b/examples/pcg_facade_tower/verify/_common.py @@ -0,0 +1,55 @@ +"""Shared verifier helpers (diagnostic only; never used inside the model graph).""" +from __future__ import annotations + +import sys +from pathlib import Path + +PART_DIR = Path(__file__).resolve().parents[1] +if str(PART_DIR) not in sys.path: + sys.path.insert(0, str(PART_DIR)) + +OUT = Path(__file__).resolve().parent / "renders" + + +def tight_bbox(shape) -> tuple: + """Exact bbox (AddOptimal, no gap); accepts a SimpleCAD shape or a raw TopoDS_Shape.""" + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + + box = Bnd_Box() + box.SetGap(0.0) + BRepBndLib.AddOptimal_s(getattr(shape, "wrapped", shape), box, useTriangulation=False) + return box.Get() + + +def report(title: str, failures: list) -> int: + print(f"== {title}: {'PASS' if not failures else 'FAIL'}") + for failure in failures: + print(f" - {failure}") + return 1 if failures else 0 + + +def place_shape(shape, frame: dict): + """Diagnostic placement (layout frame -> world) for local-frame solids: y_axis must be +Z. + Local x -> x_axis, y -> +Z, z -> x_axis x Z (the outward normal).""" + import math + + import simplecadapi as scad + + ox, oy, oz = frame["origin"] + tx, ty, _ = frame["x_axis"] + assert tuple(frame["y_axis"]) == (0.0, 0.0, 1.0) + upright = scad.rotate_shape(shape=shape, angle=90.0, axis=(1.0, 0.0, 0.0), origin=(0.0, 0.0, 0.0)) + turned = scad.rotate_shape(shape=upright, angle=math.degrees(math.atan2(ty, tx)), axis=(0.0, 0.0, 1.0), + origin=(0.0, 0.0, 0.0)) + return scad.translate_shape(shape=turned, vector=(ox, oy, oz)) + + +def min_distance(a, b) -> float: + from OCP.BRepExtrema import BRepExtrema_DistShapeShape + + dist = BRepExtrema_DistShapeShape(getattr(a, "wrapped", a), getattr(b, "wrapped", b)) + dist.Perform() + if not dist.IsDone(): + raise RuntimeError("distance computation failed") + return dist.Value() diff --git a/examples/pcg_facade_tower/verify/render_stage.py b/examples/pcg_facade_tower/verify/render_stage.py new file mode 100644 index 00000000..076c8209 --- /dev/null +++ b/examples/pcg_facade_tower/verify/render_stage.py @@ -0,0 +1,394 @@ +"""Per-stage render -> verify/renders/_*.png (Z up). + + ../../../.venv/bin/python render_stage.py s1 [rect|chamfer|l_shape ...] # massing four-view + plan + unrolled elevation + ../../../.venv/bin/python render_stage.py s2 # panel types + the K fin row + ../../../.venv/bin/python render_stage.py s3 # corner fits, slab skeletons, canopy + ../../../.venv/bin/python render_stage.py s4 # band subassemblies (solved placements) + ../../../.venv/bin/python render_stage.py s5 # tower four-view, south, setback + +S2 renders mount panel-local parts as on a south facade (x -> +X, y up -> +Z, outward z -> -Y). +""" +from __future__ import annotations + +import math +import sys +from dataclasses import replace + +import simplecadapi as scad +from _common import OUT, tight_bbox + +import layout as L + +MASSING_VIEWS = [(25, 215, "iso from SW"), (25, 35, "iso from NE"), (0, 180, "south elevation"), + (90, 0, "plan (from +Z)")] + + +def _prism(poly, z0: float, z1: float): + wire = scad.make_polyline_rwire(points=[(x, y, z0) for x, y in poly], closed=True) + face = scad.make_face_from_wire_rface(wire=wire, normal=(0.0, 0.0, 1.0)) + return scad.extrude_rsolid(profile=face, direction=(0.0, 0.0, 1.0), distance=z1 - z0) + + +def massing(sched: L.FacadeSchedule) -> list: + """Tier prisms: building line from the tier below's roof up to this tier's roof (tier 0 includes the lobby).""" + solids, z0 = [], 0.0 + for k, poly in enumerate(sched.tiers): + z1 = sched.levels[f"roof{k}"] + solids.append(_prism(poly, z0, z1)) + z0 = z1 + return solids + + +def plan_figure(sched: L.FacadeSchedule, path) -> str: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + from matplotlib.patches import Polygon + + p = sched.params + depth = 400.0 # band drawn ~2.7x frame_d so it reads at plan scale (corner reserve must stay > depth) + drawn = replace(p, frame_d=depth) + fig, axes = plt.subplots(1, len(sched.tiers), figsize=(6.2 * len(sched.tiers), 5.6)) + axes = [axes] if len(sched.tiers) == 1 else list(axes) + shades = {"vision": "#d9d9d9", "solid": "#555555"} + for k, ax in enumerate(axes): + band = sched.band(f"tier{k}-phase0") + poly = sched.tiers[k] + frames = L.edge_frames(poly) + ax.add_patch(Polygon(poly, closed=True, fill=False, lw=0.6, ls="--", ec="#888888")) + for r in band["panels"]: + fr = frames[r["edge"]] + a = L._add(fr["v0"], L._mul(fr["t"], r["s0"] + p.joint / 2.0)) + b = L._add(fr["v0"], L._mul(fr["t"], r["s0"] + r["w"] - p.joint / 2.0)) + quad = [a, b, L._add(b, L._mul(fr["n"], depth)), L._add(a, L._mul(fr["n"], depth))] + closer = r["slot"] in (0, max(q["slot"] for q in band["panels"] if q["edge"] == r["edge"])) + ax.add_patch(Polygon(quad, closed=True, lw=0.2, ec="white", + fc="#e8743b" if closer else shades[r["kind"]])) + if r.get("fin") is not None: # fin at the right jamb, rotated about the jamb by the bin angle + th = math.radians(r["fin"]) + root = L._add(b, L._mul(fr["n"], depth)) + tip_dir = L._add(L._mul(fr["n"], math.cos(th)), L._mul(fr["t"], math.sin(th))) + tip = L._add(root, L._mul(tip_dir, 2.0 * depth)) + ax.plot([root[0], tip[0]], [root[1], tip[1]], color="black", lw=0.7) + for c in band["corners"]: + prof = L.corner_profile_local(c["turn"], drawn) + fr = frames[c["vertex"] - 1] + v = tuple(poly[c["vertex"]]) + world = [L._add(L._add(v, L._mul(fr["t"], x)), L._mul(fr["n"], z)) for x, z in prof] + ax.add_patch(Polygon(world, closed=True, fc="black", ec="black", lw=0.3)) + xs = [v[0] for v in sched.tiers[0]] + ys = [v[1] for v in sched.tiers[0]] + ax.set_xlim(min(xs) - 3000, max(xs) + 3000) + ax.set_ylim(min(ys) - 3000, max(ys) + 3000) + ax.set_aspect("equal") + ax.set_title(f"tier {k} (setback {k * p.setback_depth:.0f}) phase 0", fontsize=10) + ax.tick_params(labelsize=7) + fig.suptitle(f"{p.plan_kind.value}: grey = vision, dark = solid, orange = closer, black = corner piece, " + f"line = fin (bin angle); band depth exaggerated", fontsize=10) + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + return str(path) + + +def elevation_figure(sched: L.FacadeSchedule, path) -> str: + """Unrolled perimeter elevation, every floor: the Bayer WWR dither and the quantized fin wave.""" + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + from matplotlib.colors import Normalize + from matplotlib.cm import ScalarMappable + from matplotlib.patches import Rectangle + + p = sched.params + cmap = plt.get_cmap("coolwarm") + norm = Normalize(-L.FIN_LIMIT, L.FIN_LIMIT) + fig, ax = plt.subplots(figsize=(18, 7.5)) + width = 0.0 + for rec in sched.floors: + band = sched.band(rec["floor_type"]) + frames = L.edge_frames(sched.tiers[rec["tier"]]) + starts, u = [], 0.0 + for fr in frames: + starts.append(u) + u += fr["length"] + width = max(width, u) + for r in band["panels"]: + x = starts[r["edge"]] + r["s0"] + if r["kind"] == "solid": + fc = "#444444" + elif r.get("fin") is None: + fc = "#e6e6e6" + else: + fc = cmap(norm(r["fin"])) + ax.add_patch(Rectangle((x, rec["level"]), r["w"], p.floor_h, fc=fc, ec="white", lw=0.15)) + for e, s in enumerate(starts): + ax.add_patch(Rectangle((s - p.corner_c, rec["level"]), 2 * p.corner_c, p.floor_h, fc="black", lw=0)) + ax.set_xlim(-1000, width + 1000) + ax.set_ylim(p.lobby_h - 1000, sched.stats["height"]) + ax.set_xlabel("perimeter u (mm), from SW corner CCW: S -> E -> ... ") + ax.set_ylabel("level z (mm)") + ax.set_title(f"{p.plan_kind.value}: unrolled elevation, {p.n_floors} floors. dark = solid, light = vision no fin, " + f"colour = fin bin angle (K={p.fin_bins}); WWR " + + " ".join(f"{o} {v:.3f}" for o, v in sched.stats["design_wwr"].items()), fontsize=10) + fig.colorbar(ScalarMappable(norm=norm, cmap=cmap), ax=ax, label="fin angle (deg)", fraction=0.02) + fig.tight_layout() + fig.savefig(path, dpi=130) + plt.close(fig) + return str(path) + + +# (elev, azim) are world-fixed (elev toward +Z, azim from +X); panel-local renders use view_up +y +PANEL_VIEWS_LOCAL = [(30, 35, "iso, outside"), (90, 0, "outside (+z)"), (-90, 0, "inside (-z)"), + (0, 0, "side (+x): depth profile")] + + +def _south(shape, dx: float = 0.0): + """Panel-local -> world as mounted on a south edge, shifted dx along the facade.""" + upright = scad.rotate_shape(shape=shape, angle=90.0, axis=(1.0, 0.0, 0.0), origin=(0.0, 0.0, 0.0)) + return scad.translate_shape(shape=upright, vector=(dx, 0.0, 0.0)) + + +def _fin_on_panel(fin_body, w: float, p) -> object: + """Fin-local -> panel-local: root on the right-jamb centreline of the frame outer face (A6).""" + return scad.translate_shape(shape=fin_body, vector=(w - p.joint / 2.0 - p.frame_sight / 2.0, 0.0, p.frame_d)) + + +def render_s2() -> list[str]: + import panels as P + from s2_verify import all_types + + OUT.mkdir(exist_ok=True) + p = L.default_params() + types = all_types() + body = {tid: P.make_panel_part(tid, spec, p).value.body for tid, spec in types.items()} + out = [] + for tid, spec in sorted(types.items()): # one four-view per vision type (glass tag highlighted) + if spec["family"] == "vision": + # panel-local, untransformed: a moved shape keeps its tags only in lineage scope, and the renderer + # highlights effective tags + out.append(scad.render_screenshot_rpath( + shapes=[body[tid]], output_path=str(OUT / f"s2_{tid}.png"), views=PANEL_VIEWS_LOCAL, + view_up=(0.0, 1.0, 0.0), highlight_tags=[P.GLASS_TAG], show_callouts=False, show_legend=False)) + solids = sorted((t for t, s in types.items() if s["family"] == "solid"), key=lambda t: (types[t]["h"], types[t]["w"])) + row, x = [], 0.0 + for tid in solids: # every solid type side by side, 300 apart + row.append(_south(body[tid], x)) + x += types[tid]["w"] + 300.0 + out.append(scad.render_screenshot_rpath(shapes=row, output_path=str(OUT / "s2_solid_types.png"), + views=[(20, 235, "iso, outside"), (0, 270, "outside (south)")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + std = L.vision_type_id(p.w_std, p.floor_h, p.spandrel_h) + fins = sorted((t for t, s in types.items() if s["family"] == "fin"), key=lambda t: types[t]["angle"]) + row = [] + for i, tid in enumerate(fins): # the K bins, each on a standard vision panel, adjacent modules + row.append(_south(body[std], i * p.w_std)) + row.append(_south(_fin_on_panel(body[tid], p.w_std, p), i * p.w_std)) + out.append(scad.render_screenshot_rpath( + shapes=row, output_path=str(OUT / "s2_fin_bins.png"), + views=[(25, 235, "iso, outside"), (0, 270, "outside (south)"), (60, 270, "high, outside"), + (90, 0, "plan from +Z")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + # blade sections alone, fin-local, 500 apart in x, looking down the extrusion (+y -> viewer) + sections = [scad.translate_shape(shape=body[tid], vector=(i * 500.0, 0.0, 0.0)) for i, tid in enumerate(fins)] + out.append(scad.render_screenshot_rpath( + shapes=sections, output_path=str(OUT / "s2_fin_sections.png"), + views=[(0, 90, "from +y (x runs left): " + " ".join(f"{types[t]['angle']:g}" for t in fins)), + (30, 60, "iso")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + return out + + +def _corner_assembly(sched: L.FacadeSchedule, band: dict, rec: dict, n_side: int = 2) -> tuple[list, tuple]: + """World-placed corner + n_side panels each side (s3_verify placement), shifted so the vertex is the origin. + Returns (shapes, outward bisector).""" + from _common import place_shape + from s3_verify import Parts + + parts, level = Parts(sched), L.band_levels(sched, band["band_id"])[0] + poly = [tuple(v) for v in sched.tiers[band["tier"]]] + i, frames = rec["vertex"], L.edge_frames(poly) + e_in = (i - 1) % len(poly) + near = sorted((r for r in band["panels"] if r["edge"] == e_in), key=lambda r: r["slot"])[-n_side:] + near += sorted((r for r in band["panels"] if r["edge"] == i), key=lambda r: r["slot"])[:n_side] + shapes = [parts.corner(band, rec, level)] + [parts.panel(band, r, level) for r in near] + v = poly[i] + moved = [scad.translate_shape(shape=sh, vector=(-v[0], -v[1], -level)) for sh in shapes] + return moved, L._add(frames[e_in]["n"], frames[i]["n"]) + + +def render_s3() -> list[str]: + import slabs as S + from s3_verify import Parts, fit_bands + + OUT.mkdir(exist_ok=True) + out = [] + picks = [(L.PlanKind.RECT, 90.0, "p90"), (L.PlanKind.CHAMFER, 45.0, "p45"), (L.PlanKind.L_SHAPE, -90.0, "m90")] + for kind, turn, tag in picks: # corner + two panels each side, tier-0 tower band, vertex at the origin + sched = L.build_schedule(L.default_params(plan_kind=kind)) + band = next(b for b in fit_bands(sched) if b["band"] == L.Band.TOWER.value and b["tier"] == 0) + rec = next(r for r in band["corners"] if abs(r["turn"] - turn) < 1.0e-9) + shapes, bis = _corner_assembly(sched, band, rec) + az = math.degrees(math.atan2(bis[1], bis[0])) % 360.0 + out.append(scad.render_screenshot_rpath( + shapes=shapes, output_path=str(OUT / f"s3_corner_{tag}.png"), + views=[(25, az, f"{kind.value} v{rec['vertex']} turn {turn:+g}: outside"), + (25, (az + 180.0) % 360.0, "inside"), (90, 0, "plan from +Z"), (-90, 0, "plan from below")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + close, _ = _corner_assembly(sched, band, rec, n_side=1) + out.append(scad.render_screenshot_rpath( + shapes=close, output_path=str(OUT / f"s3_corner_{tag}_plan.png"), view=(90.0, 0.0), zoom=10.0, + view_up=(0.0, 1.0, 0.0), show_callouts=False, show_legend=False)) + + for kind in L.PlanKind: # slab stack + every corner column: the structural skeleton of the facade + sched = L.build_schedule(L.default_params(plan_kind=kind)) + parts = Parts(sched) + slab = {k: S.make_slab_part(sched, k).value.body for k in range(len(sched.tiers))} + shapes = [scad.translate_shape(shape=slab[i["tier"]], vector=(0.0, 0.0, i["level"])) + for i in L.slab_instances(sched)] + for band in sched.bands: + for level in L.band_levels(sched, band["band_id"]): + shapes += [parts.corner(band, rec, level) for rec in band["corners"]] + out.append(scad.render_screenshot_rpath( + shapes=shapes, output_path=str(OUT / f"s3_skeleton_{kind.value}.png"), views=MASSING_VIEWS, + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + + sched = L.build_schedule(L.default_params()) # entrance: south lobby rows + corners + canopy + ground slab + parts, lobby = Parts(sched), sched.band("lobby") + shapes = [S.make_canopy_part(sched).value.body] + for level in L.band_levels(sched, "lobby"): + shapes += [parts.panel(lobby, r, level) for r in lobby["panels"] if r["edge"] == 0] + shapes += [parts.corner(lobby, r, level) for r in lobby["corners"] if r["vertex"] in (0, 1)] + shapes.append(S.make_slab_part(sched, 0).value.body) + out.append(scad.render_screenshot_rpath( + shapes=shapes, output_path=str(OUT / "s3_canopy.png"), + views=[(15, 240, "iso, south-west"), (0, 270, "south elevation"), (0, 0, "from east: canopy on the row joint"), + (55, 300, "high, south-east")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + return out + + +def _near(sched: L.FacadeSchedule, band: dict, shapes: dict, vertex: int, radius: float) -> list: + """Placed band components whose frame origin lies within radius (plan) of a tier vertex.""" + import floor as F + + vx, vy = sched.tiers[band["tier"]][vertex] + near = {c["component_id"] for c in F.band_components(sched, band) + if math.hypot(c["frame"]["origin"][0] - vx, c["frame"]["origin"][1] - vy) < radius} + return [s for cid, s in shapes.items() if cid in near] + + +def render_s4() -> list[str]: + import floor as F + from s4_verify import placed + + OUT.mkdir(exist_ok=True) + out = [] + sched = L.build_schedule(L.default_params()) + for band_id, views in ( + ("tier0-phase0", [(25, 225, "tier0-phase0 (floor 1): iso south-west"), (25, 45, "iso north-east (chamfer)"), + (0, 270, "south elevation"), (90, 0, "plan from +Z")]), + ("lobby", [(20, 225, "lobby, 2 rows: iso south-west"), (0, 270, "south elevation")])): + band = sched.band(band_id) + asm = F.make_band_assembly(sched, band_id).value # rendered from the solved placements + shapes = {c.component_id: placed(c) for c in asm.components} + out.append(scad.render_screenshot_rpath( + shapes=list(shapes.values()), output_path=str(OUT / f"s4_band_{band_id}.png"), views=views, + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + if band_id != "tier0-phase0": + continue + chamfer = next(r["vertex"] for r in band["corners"] if abs(r["turn"] - 45.0) < 1.0e-9) + out.append(scad.render_screenshot_rpath( # joints at plan scale: chamfer corner, fins on the east face + shapes=_near(sched, band, shapes, chamfer, 3500.0), output_path=str(OUT / "s4_plan_chamfer.png"), + view=(90.0, 0.0), view_up=(0.0, 1.0, 0.0), show_callouts=False, show_legend=False)) + sw = next(r["vertex"] for r in band["corners"] if abs(r["turn"] - 90.0) < 1.0e-9) + out.append(scad.render_screenshot_rpath( + shapes=_near(sched, band, shapes, sw, 2600.0), output_path=str(OUT / "s4_corner_sw.png"), + views=[(20, 225, f"v{sw} +90 corner: outside"), (20, 45, "inside"), (90, 0, "plan from +Z"), + (0, 270, "south elevation")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + return out + + +def _clip(shape, lo: tuple, hi: tuple): + """Diagnostic: shape ∩ axis box (for close-ups of whole-plan slabs).""" + from OCP.BRepAlgoAPI import BRepAlgoAPI_Common + from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox + from OCP.gp import gp_Pnt + + return BRepAlgoAPI_Common(shape, BRepPrimAPI_MakeBox(gp_Pnt(*lo), gp_Pnt(*hi)).Shape()).Shape() + + +def render_s5() -> list[str]: + from tower import make_tower + from s5_verify import realize + + OUT.mkdir(exist_ok=True) + out = [] + sched = L.build_schedule(L.default_params()) + p = sched.params + asm = make_tower(sched).value + whole = scad.make_compound_from_assembly_rcompound(assembly=asm).wrapped # the SDK's flattened tower (the renderer takes Solid / raw TopoDS only) + out.append(scad.render_screenshot_rpath( + shapes=[whole], output_path=str(OUT / "s5_tower.png"), + views=[(20, 225, "tower: iso south-west"), (20, 45, "iso north-east (chamfer)"), (0, 270, "south elevation"), + (90, 0, "plan from +Z")], view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + out.append(scad.render_screenshot_rpath( + shapes=[whole], output_path=str(OUT / "s5_south.png"), view=(0.0, 270.0), view_up=(0.0, 0.0, 1.0), + show_callouts=False, show_legend=False)) + # setback close-up: first tier roof at the SW corner, two floors either side, parapet, roof slab + comps = {c.component_id: c for c in asm.components} + top0 = p.setback_every + vx, vy = sched.tiers[0][0] + r = 7000.0 + lo, hi = (vx - r, vy - r, sched.levels[f"floor{top0 - 1}"] - 1.0), (vx + r, vy + r, sched.levels[f"floor{top0 + 3}"]) + shapes = [] + for cid in (f"floor{top0 - 1}", f"floor{top0}", "parapet0", "slab-roof0", f"slab-floor{top0}", + f"floor{top0 + 1}", f"floor{top0 + 2}", f"slab-floor{top0 + 2}"): + for pid, s in realize(comps[cid]).items(): + if not pid: + shapes.append(_clip(s, lo, hi)) + continue + b = tight_bbox(s) + if lo[0] < (b[0] + b[3]) / 2 < hi[0] and lo[1] < (b[1] + b[4]) / 2 < hi[1]: + shapes.append(s) + out.append(scad.render_screenshot_rpath( + shapes=shapes, output_path=str(OUT / "s5_setback.png"), + views=[(25, 225, f"tier 0 roof / tier 1 setback at v0 (floors {top0 - 1}-{top0 + 2})"), + (25, 135, "from south-east"), (0, 270, "south elevation"), (0, 180, "west elevation")], + view_up=(0.0, 0.0, 1.0), show_callouts=False, show_legend=False)) + return out + + +def render_s1(kinds) -> list[str]: + OUT.mkdir(exist_ok=True) + out = [] + for kind in kinds: + sched = L.build_schedule(L.default_params(plan_kind=L.PlanKind(kind))) + out.append(scad.render_screenshot_rpath(shapes=massing(sched), output_path=str(OUT / f"s1_massing_{kind}.png"), + views=MASSING_VIEWS, view_up=(0.0, 0.0, 1.0), show_callouts=False, + show_legend=False)) + out.append(plan_figure(sched, OUT / f"s1_plan_{kind}.png")) + out.append(elevation_figure(sched, OUT / f"s1_elevation_{kind}.png")) + return out + + +if __name__ == "__main__": + stage = sys.argv[1] if len(sys.argv) > 1 else "s1" + if stage == "s1": + for item in render_s1(sys.argv[2:] or [k.value for k in L.PlanKind]): + print(item) + elif stage == "s2": + for item in render_s2(): + print(item) + elif stage == "s3": + for item in render_s3(): + print(item) + elif stage == "s4": + for item in render_s4(): + print(item) + elif stage == "s5": + for item in render_s5(): + print(item) + else: + raise SystemExit(f"unknown stage {stage}") diff --git a/examples/pcg_facade_tower/verify/renders/s1_elevation_chamfer.png b/examples/pcg_facade_tower/verify/renders/s1_elevation_chamfer.png new file mode 100644 index 00000000..59fc719e Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_elevation_chamfer.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_elevation_l_shape.png b/examples/pcg_facade_tower/verify/renders/s1_elevation_l_shape.png new file mode 100644 index 00000000..fe9bb48f Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_elevation_l_shape.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_elevation_rect.png b/examples/pcg_facade_tower/verify/renders/s1_elevation_rect.png new file mode 100644 index 00000000..b003665b Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_elevation_rect.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_massing_chamfer.png b/examples/pcg_facade_tower/verify/renders/s1_massing_chamfer.png new file mode 100644 index 00000000..ecade156 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_massing_chamfer.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_massing_l_shape.png b/examples/pcg_facade_tower/verify/renders/s1_massing_l_shape.png new file mode 100644 index 00000000..61fbacff Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_massing_l_shape.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_massing_rect.png b/examples/pcg_facade_tower/verify/renders/s1_massing_rect.png new file mode 100644 index 00000000..ecad0a76 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_massing_rect.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_plan_chamfer.png b/examples/pcg_facade_tower/verify/renders/s1_plan_chamfer.png new file mode 100644 index 00000000..b0f36cb8 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_plan_chamfer.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_plan_l_shape.png b/examples/pcg_facade_tower/verify/renders/s1_plan_l_shape.png new file mode 100644 index 00000000..0343cd95 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_plan_l_shape.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s1_plan_rect.png b/examples/pcg_facade_tower/verify/renders/s1_plan_rect.png new file mode 100644 index 00000000..58c88ca5 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s1_plan_rect.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s2_fin_bins.png b/examples/pcg_facade_tower/verify/renders/s2_fin_bins.png new file mode 100644 index 00000000..1442acfb Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s2_fin_bins.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s2_fin_sections.png b/examples/pcg_facade_tower/verify/renders/s2_fin_sections.png new file mode 100644 index 00000000..ea415cfc Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s2_fin_sections.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s2_solid_types.png b/examples/pcg_facade_tower/verify/renders/s2_solid_types.png new file mode 100644 index 00000000..22afd4b3 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s2_solid_types.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s2_vision-w1500-h2700-s0.png b/examples/pcg_facade_tower/verify/renders/s2_vision-w1500-h2700-s0.png new file mode 100644 index 00000000..19753a21 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s2_vision-w1500-h2700-s0.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s2_vision-w1500-h3600-s1100.png b/examples/pcg_facade_tower/verify/renders/s2_vision-w1500-h3600-s1100.png new file mode 100644 index 00000000..cd85f8ca Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s2_vision-w1500-h3600-s1100.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_canopy.png b/examples/pcg_facade_tower/verify/renders/s3_canopy.png new file mode 100644 index 00000000..d72d1c4c Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_canopy.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_corner_m90.png b/examples/pcg_facade_tower/verify/renders/s3_corner_m90.png new file mode 100644 index 00000000..6e7500ff Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_corner_m90.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_corner_m90_plan.png b/examples/pcg_facade_tower/verify/renders/s3_corner_m90_plan.png new file mode 100644 index 00000000..cd2ab37d Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_corner_m90_plan.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_corner_p45.png b/examples/pcg_facade_tower/verify/renders/s3_corner_p45.png new file mode 100644 index 00000000..ee29f240 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_corner_p45.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_corner_p45_plan.png b/examples/pcg_facade_tower/verify/renders/s3_corner_p45_plan.png new file mode 100644 index 00000000..25e98de1 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_corner_p45_plan.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_corner_p90.png b/examples/pcg_facade_tower/verify/renders/s3_corner_p90.png new file mode 100644 index 00000000..e77d735d Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_corner_p90.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_corner_p90_plan.png b/examples/pcg_facade_tower/verify/renders/s3_corner_p90_plan.png new file mode 100644 index 00000000..8c2346f8 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_corner_p90_plan.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_skeleton_chamfer.png b/examples/pcg_facade_tower/verify/renders/s3_skeleton_chamfer.png new file mode 100644 index 00000000..af6c6722 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_skeleton_chamfer.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_skeleton_l_shape.png b/examples/pcg_facade_tower/verify/renders/s3_skeleton_l_shape.png new file mode 100644 index 00000000..bb3a953c Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_skeleton_l_shape.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s3_skeleton_rect.png b/examples/pcg_facade_tower/verify/renders/s3_skeleton_rect.png new file mode 100644 index 00000000..751a4ab5 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s3_skeleton_rect.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s4_band_lobby.png b/examples/pcg_facade_tower/verify/renders/s4_band_lobby.png new file mode 100644 index 00000000..849edb38 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s4_band_lobby.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s4_band_tier0-phase0.png b/examples/pcg_facade_tower/verify/renders/s4_band_tier0-phase0.png new file mode 100644 index 00000000..9f330d01 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s4_band_tier0-phase0.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s4_corner_sw.png b/examples/pcg_facade_tower/verify/renders/s4_corner_sw.png new file mode 100644 index 00000000..e39ec2c2 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s4_corner_sw.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s4_plan_chamfer.png b/examples/pcg_facade_tower/verify/renders/s4_plan_chamfer.png new file mode 100644 index 00000000..32b6e0ff Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s4_plan_chamfer.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s5_setback.png b/examples/pcg_facade_tower/verify/renders/s5_setback.png new file mode 100644 index 00000000..782dc784 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s5_setback.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s5_south.png b/examples/pcg_facade_tower/verify/renders/s5_south.png new file mode 100644 index 00000000..6bc789a9 Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s5_south.png differ diff --git a/examples/pcg_facade_tower/verify/renders/s5_tower.png b/examples/pcg_facade_tower/verify/renders/s5_tower.png new file mode 100644 index 00000000..6125fddc Binary files /dev/null and b/examples/pcg_facade_tower/verify/renders/s5_tower.png differ diff --git a/examples/pcg_facade_tower/verify/s1_known_bad.py b/examples/pcg_facade_tower/verify/s1_known_bad.py new file mode 100644 index 00000000..3c39d3db --- /dev/null +++ b/examples/pcg_facade_tower/verify/s1_known_bad.py @@ -0,0 +1,118 @@ +"""S1 known-bad proof: check_s1 must FAIL on each deliberately corrupted schedule and PASS on the clean one. + + KB0 clean CHAMFER schedule -> must pass + KB1 one standard panel 1 mm narrower -> C1 (edge sum) + C2 (inner width) + KB2 closers made asymmetric (+/- 300) -> C2 + KB3 closer turned into a vision panel -> C2 + KB4 every south solid flipped to vision -> C4 (WWR S overshoots) + KB5 fin moved off its bin by 1 degree -> C6 + KB6 a fin on the north edge -> C6 + KB7 floor 13 levelled one floor too low -> C8 + KB8 tier 1 setback 2999 instead of 3000 -> C8 + KB9 one fin record dropped -> C6 + KB10 lobby as a single 5400 row -> C10 +""" +import copy +import sys + +from _common import report # noqa: F401 (sys.path setup) +from s1_verify import check_s1 + +import layout as L + +CLEAN = L.build_schedule(L.default_params(plan_kind=L.PlanKind.CHAMFER)) + + +def mutate(fn): + sched = copy.deepcopy(CLEAN) + fn(sched) + return sched + + +def _tower(sched, band_id="tier0-phase0"): + return sched.band(band_id) + + +def kb1(s): + r = next(r for r in _tower(s)["panels"] if r["slot"] == 3) + r["w"] -= 1.0 + + +def kb2(s): + recs = [r for r in _tower(s)["panels"] if r["edge"] == 0] + recs[0]["w"] += 300.0 + recs[-1]["w"] -= 300.0 + for r in recs[1:]: + r["s0"] += 300.0 + + +def kb3(s): + _tower(s)["panels"][0]["kind"] = "vision" + + +def kb4(s): + for b in s.bands: + if b["band"] == "tower": + last = {} + for r in b["panels"]: + last[r["edge"]] = max(last.get(r["edge"], 0), r["slot"]) + for r in b["panels"]: + if r["orientation"] == "S" and r["kind"] == "solid" and 0 < r["slot"] < last[r["edge"]]: + r["kind"] = "vision" + + +def kb5(s): + r = next(r for r in _tower(s)["panels"] if r.get("fin") is not None) + r["fin"] += 1.0 + next(f for f in _tower(s)["fins"] if (f["edge"], f["slot"]) == (r["edge"], r["slot"]))["angle"] = r["fin"] + + +def kb6(s): + frames = L.edge_frames(s.tiers[0]) + north = next(e for e, fr in enumerate(frames) if not L.edge_has_fins(fr["n"], s.params)) + r = next(r for r in _tower(s)["panels"] if r["edge"] == north and r["kind"] == "vision") + r["fin"], r["fin_raw"] = 9.0, 9.0 + _tower(s)["fins"].append({"edge": north, "slot": r["slot"], "s": L.fin_root_s(r["s0"], r["w"], s.params), + "angle": 9.0, "type_id": _tower(s)["fins"][0]["type_id"]}) + + +def kb7(s): + s.floors[12]["level"] -= s.params.floor_h + + +def kb8(s): + s.tiers[1] = [list(v) for v in L.offset_polygon([tuple(v) for v in s.tiers[0]], 2999.0)] + + +def kb9(s): + _tower(s)["fins"].pop() + + +def kb10(s): + lobby = s.band("lobby") + lobby["rows"], lobby["height"] = 1, s.params.lobby_h + + +cases = { + "KB0 clean": (CLEAN, True), + "KB1 panel 1 mm narrow": (mutate(kb1), False), + "KB2 closers asymmetric": (mutate(kb2), False), + "KB3 closer as vision": (mutate(kb3), False), + "KB4 south all vision": (mutate(kb4), False), + "KB5 fin off bin": (mutate(kb5), False), + "KB6 fin on north edge": (mutate(kb6), False), + "KB7 floor 13 level": (mutate(kb7), False), + "KB8 tier1 setback 2999": (mutate(kb8), False), + "KB9 fin record dropped": (mutate(kb9), False), + "KB10 lobby single row": (mutate(kb10), False), +} +wrong = [] +for name, (sched, should_pass) in cases.items(): + failures = check_s1(sched) + passed = not failures + verdict = "as designed" if passed == should_pass else "GATE BROKEN" + print(f"--- {name:26s} => {'pass' if passed else 'fail'} ({verdict})" + + ("" if passed else f" first: {failures[0]} [+{len(failures) - 1}]")) + if passed != should_pass: + wrong.append(name) +sys.exit(report("S1 known-bad", [f"{name}: gate did not discriminate" for name in wrong])) diff --git a/examples/pcg_facade_tower/verify/s1_verify.py b/examples/pcg_facade_tower/verify/s1_verify.py new file mode 100644 index 00000000..9e7154f2 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s1_verify.py @@ -0,0 +1,227 @@ +"""S1 gate: layout engine (pure data). + + C1 closure every band, every edge: corner_c + sum(widths) + corner_c == edge length (1e-9); slots contiguous + C2 closers both end slots equal, solid, in [closer_min, closer_min + w_std/2) + C3 guards each guard rejects just past its boundary and accepts just inside (fail loudly, never degrade) + C4 WWR design glass / gross per orientation within 0.03 of target + C5 rationalized one standard vision + one standard solid type; fin types <= K; floor types == tiers x P; ids valid + C6 fins quantization error <= 90 / (2 (K - 1)); north-facing edges carry no fins; every fin panel has + exactly one fin record at its right-jamb root with the same angle + C10 transport every band height <= MAX_PANEL_H; lobby rows x row height == lobby_h + C7 corners corner profiles for every turn in use: simple, CCW, area > 0; reflex corners fit + C8 vertical floor levels, tier assignment, roof levels, setback depth per tier + C9 plans RECT / CHAMFER / L_SHAPE all schedule; JSON round-trip + + ../../../.venv/bin/python s1_verify.py +""" +from __future__ import annotations + +import json +import math +import re +import sys + +from _common import report # noqa: F401 (sys.path setup) + +import layout as L + +ID_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.:-]{0,255}$") +EPS = 1.0e-9 + + +def check_s1(sched: L.FacadeSchedule) -> list[str]: + p = sched.params + bad: list[str] = [] + + # C1 / C2 + for b in sched.bands: + frames = L.edge_frames(sched.tiers[b["tier"]]) + for e, fr in enumerate(frames): + recs = sorted((r for r in b["panels"] if r["edge"] == e), key=lambda r: r["slot"]) + if not recs: + bad.append(f"C1 {b['band_id']} edge {e}: no panels") + continue + total = 2.0 * p.corner_c + sum(r["w"] for r in recs) + if abs(total - fr["length"]) > EPS * max(1.0, fr["length"]): + bad.append(f"C1 {b['band_id']} edge {e}: widths sum {total:.9f} != length {fr['length']:.9f}") + s = p.corner_c + for r in recs: + if abs(r["s0"] - s) > EPS * max(1.0, fr["length"]): + bad.append(f"C1 {b['band_id']} edge {e} slot {r['slot']}: s0 {r['s0']:.6f} != {s:.6f}") + s = r["s0"] + r["w"] + first, last = recs[0], recs[-1] + if abs(first["w"] - last["w"]) > EPS * max(1.0, fr["length"]): + bad.append(f"C2 {b['band_id']} edge {e}: closers asymmetric {first['w']:.6f} / {last['w']:.6f}") + for r in (first, last): + if not (p.closer_min - EPS <= r["w"] < p.closer_min + p.w_std / 2.0): + bad.append(f"C2 {b['band_id']} edge {e}: closer {r['w']:.6f} outside " + f"[{p.closer_min}, {p.closer_min + p.w_std / 2.0})") + if r["kind"] != "solid": + bad.append(f"C2 {b['band_id']} edge {e}: closer slot {r['slot']} is {r['kind']}") + for r in recs[1:-1]: + if abs(r["w"] - p.w_std) > EPS: + bad.append(f"C2 {b['band_id']} edge {e} slot {r['slot']}: inner width {r['w']} != w_std") + + # C4 + wwr = L.design_wwr(sched) + for o, target in p.wwr_target: + if o in wwr and abs(wwr[o] - target) > L.WWR_TOL: + bad.append(f"C4 WWR {o}: design {wwr[o]:.4f} vs target {target}") + + # C5 + std = sched.stats["standard_panel_types"] + if len(std) > 2: + bad.append(f"C5 {len(std)} standard panel types > 2: {std}") + if len(sched.stats["fin_types"]) > p.fin_bins: + bad.append(f"C5 {len(sched.stats['fin_types'])} fin types > K = {p.fin_bins}") + expected_ft = sum(1 for k in range(len(sched.tiers)) for ph in range(p.fin_period_floors) + if any((f - 1) // p.setback_every == k and (f - 1) % p.fin_period_floors == ph + for f in range(1, p.n_floors + 1))) + if len(sched.floor_types) != expected_ft: + bad.append(f"C5 floor types {len(sched.floor_types)} != tiers x phases {expected_ft}") + used = {r["type_id"] for b in sched.bands for r in b["panels"] + b["corners"] + b["fins"]} + if used != set(sched.types): + bad.append(f"C5 type table mismatch: unused {sorted(set(sched.types) - used)}, " + f"unregistered {sorted(used - set(sched.types))}") + for tid in list(sched.types) + list(sched.floor_types): + if not ID_RE.match(tid): + bad.append(f"C5 invalid identifier {tid!r}") + + # C6 + bins = L.fin_bin_angles(p.fin_bins) + limit = 90.0 / (2.0 * (p.fin_bins - 1)) + EPS + for b in sched.bands: + frames = L.edge_frames(sched.tiers[b["tier"]]) + fins = {} + for fr_ in b["fins"]: + fins.setdefault((fr_["edge"], fr_["slot"]), []).append(fr_) + fin_panels = {(r["edge"], r["slot"]) for r in b["panels"] if r.get("fin") is not None} + if set(fins) != fin_panels: + bad.append(f"C6 {b['band_id']}: fin records {len(fins)} do not match fin panels {len(fin_panels)}") + for r in b["panels"]: + fin = r.get("fin") + if fin is None: + continue + recs = fins.get((r["edge"], r["slot"]), []) + if len(recs) != 1 or recs[0]["angle"] != fin or abs(recs[0]["s"] - L.fin_root_s(r["s0"], r["w"], p)) > EPS: + bad.append(f"C6 {b['band_id']} edge {r['edge']} slot {r['slot']}: fin record mismatch {recs}") + if not L.edge_has_fins(frames[r["edge"]]["n"], p): + bad.append(f"C6 {b['band_id']} edge {r['edge']}: fin on a non-sun edge") + if min(abs(fin - a) for a in bins) > EPS: + bad.append(f"C6 {b['band_id']}: fin {fin} not a bin angle") + err = abs(max(-L.FIN_LIMIT, min(L.FIN_LIMIT, r["fin_raw"])) - fin) + if err > limit: + bad.append(f"C6 {b['band_id']} edge {r['edge']} slot {r['slot']}: quant error {err:.4f} > {limit:.4f}") + + # C7 + for turn in sorted({c["turn"] for b in sched.bands for c in b["corners"]}): + try: + prof = L.corner_profile_local(turn, p) + except L.LayoutError as exc: + bad.append(f"C7 corner turn {turn:.3f}: {exc}") + continue + if L.signed_area(prof) <= 0.0 or not L.is_simple(prof): + bad.append(f"C7 corner turn {turn:.3f}: profile not simple / CCW") + + # C10 + for b in sched.bands: + if b["height"] > L.MAX_PANEL_H + EPS: + bad.append(f"C10 {b['band_id']}: band height {b['height']} > {L.MAX_PANEL_H}") + lobby = sched.band("lobby") + if abs(lobby["rows"] * lobby["height"] - p.lobby_h) > EPS: + bad.append(f"C10 lobby rows {lobby['rows']} x {lobby['height']} != {p.lobby_h}") + + # C8 + for rec in sched.floors: + f = rec["floor"] + if abs(rec["level"] - (p.lobby_h + (f - 1) * p.floor_h)) > EPS: + bad.append(f"C8 floor {f}: level {rec['level']}") + if rec["tier"] != (f - 1) // p.setback_every: + bad.append(f"C8 floor {f}: tier {rec['tier']}") + if f not in sched.floor_types[rec["floor_type"]]["floors"]: + bad.append(f"C8 floor {f}: not listed under its floor type {rec['floor_type']}") + base = L.edge_frames(sched.tiers[0]) + for k, poly in enumerate(sched.tiers): + for eb, et in zip(base, L.edge_frames(poly)): # each tier edge line sits k * depth inside the base edge line + gap = L._dot(L._sub(eb["v0"], et["v0"]), eb["n"]) + if abs(gap - k * p.setback_depth) > 1.0e-6: + bad.append(f"C8 tier {k}: edge setback {gap:.6f} != {k * p.setback_depth}") + top = max(f for f in range(1, p.n_floors + 1) if (f - 1) // p.setback_every == k) + if abs(sched.levels[f"roof{k}"] - (p.lobby_h + top * p.floor_h)) > EPS: + bad.append(f"C8 roof{k} level {sched.levels[f'roof{k}']}") + + return bad + + +# ---------------------------------------------------------------- C3 guard matrix + + +def _rejects(stage, **overrides) -> bool: + """stage: the narrowest engine entry that owns the guard, so a neighbouring guard cannot mask it.""" + try: + params = L.default_params(**overrides) + stage(params) + except L.LayoutError: + return True + return False + + +def guard_matrix() -> list[str]: + p = L.default_params() + d = 1.0e-6 + # shortest default edge: tier-top chamfer. Setback that shrinks it to exactly 2c + 2 closer_min is the boundary. + chamfer_top = L.edge_frames(L.tier_plans(p)[-1])[2]["length"] + tiers = len(L.tier_plans(p)) + # the chamfer edge shrinks by 2 * tan(22.5 deg) per unit of setback + shrink = 2.0 * math.tan(math.radians(22.5)) * (tiers - 1) + limit_depth = p.setback_depth + (chamfer_top - 2.0 * p.corner_c - 2.0 * p.closer_min) / shrink + full, params_only, tiers_only = L.build_schedule, L.assert_params, L.tier_plans + rows = [ + # (name, entry, overrides past boundary -> must reject, overrides just inside -> must accept) + ("edge too short", full, dict(setback_depth=limit_depth + d), dict(setback_depth=limit_depth - d)), + ("closer_min >= w_std", params_only, dict(closer_min=p.w_std), dict(closer_min=p.w_std - d)), + ("K < 2", params_only, dict(fin_bins=1), dict(fin_bins=2)), + ("P not multiple of 4", params_only, dict(fin_period_floors=6), dict(fin_period_floors=8)), + ("corner_c <= frame_d + joint", params_only, dict(corner_c=p.frame_d + p.joint), + dict(corner_c=p.frame_d + p.joint + d)), + ("setback reverses an edge", tiers_only, # RECT, 2 tiers: the 24000 edge vanishes at depth 12000 + dict(plan_kind=L.PlanKind.RECT, setback_depth=p.plan_d / 2.0 + d, n_floors=24), + dict(plan_kind=L.PlanKind.RECT, setback_depth=p.plan_d / 2.0 - 1.0, n_floors=24)), + ("WWR unreachable", full, dict(wwr_target=(("N", 0.62), ("E", 0.4), ("S", 0.35), ("W", 0.4))), + dict(wwr_target=(("N", 0.52), ("E", 0.4), ("S", 0.35), ("W", 0.4)))), + ("fin_amp > 45", params_only, dict(fin_amp=45.0 + d), dict(fin_amp=45.0)), + ("floor_h > transport height", params_only, dict(floor_h=L.MAX_PANEL_H + d), dict(floor_h=L.MAX_PANEL_H)), + ("glass outside frame", params_only, dict(glass_recess=p.frame_d - p.glass_t), + dict(glass_recess=p.frame_d - p.glass_t - d)), + ] + bad = [] + for name, entry, reject, accept in rows: + r, a = _rejects(entry, **reject), not _rejects(entry, **accept) + print(f" guard {name:28s} past -> {'rejected' if r else 'ACCEPTED'} inside -> {'accepted' if a else 'REJECTED'}") + if not r: + bad.append(f"C3 {name}: accepted past the boundary {reject}") + if not a: + bad.append(f"C3 {name}: rejected inside the boundary {accept}") + return bad + + +def main() -> int: + failures = [] + for kind in L.PlanKind: + sched = L.build_schedule(L.default_params(plan_kind=kind)) + again = json.loads(sched.to_json()) + if again["stats"]["n_instances"] != sched.stats["n_instances"] or len(again["bands"]) != len(sched.bands): + failures.append(f"C9 {kind.value}: JSON round-trip mismatch") + errs = check_s1(sched) + st = sched.stats + print(f"-- {kind.value:8s} types {st['n_types']:3d} {st['types_by_family']} std panels {len(st['standard_panel_types'])} fins {len(st['fin_types'])}" + f" floor types {st['n_floor_types']} instances {st['n_instances']} height {st['height']:.0f}") + print(" WWR design " + " ".join(f"{o} {v:.3f}/{dict(sched.params.wwr_target)[o]}" for o, v in st["design_wwr"].items()) + + f" max fin err {st['max_fin_quant_error']:.3f} deg") + failures += [f"[{kind.value}] {e}" for e in errs] + failures += guard_matrix() + return report("S1 layout", failures) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/pcg_facade_tower/verify/s2_known_bad.py b/examples/pcg_facade_tower/verify/s2_known_bad.py new file mode 100644 index 00000000..2a7b0341 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s2_known_bad.py @@ -0,0 +1,81 @@ +"""S2 known-bad proof: check_body must FAIL on each deliberately wrong solid and PASS on the clean ones. + +Each case builds the body from mutated builder arguments (or a mutated step) but checks it against the +unchanged type spec, the way a drifted builder would be caught. + + KB0 clean vision / solid / fin -> must pass + KB1 vision frame 1 mm shallow -> P2 / P3 + KB2 glass recessed 1 mm deeper -> P4 (plane) + P5 + KB3 glass face never tagged -> P4 + KB4 panel built to the module, no joint -> P2 / P3 + KB5 solid panel carries a glass tag -> P4 + KB6 fin built 0.1 deg off its bin -> P6 (angle) + KB7 fin angle sign flipped -> P6 (angle + lean) + KB8 fin lifted 1 mm off the frame face -> P6 (root) + KB9 fin 1 mm thicker -> P6 (side planes) +""" +import sys +from types import SimpleNamespace + +from _common import report +from s2_verify import check_body + +import layout as L +import panels as P +import simplecadapi as scad + +p = L.default_params() +TYPES = L.build_schedule(p).types +VISION = next(t for t, s in TYPES.items() if s["family"] == "vision" and s["spandrel"] > 0) +SOLID = next(t for t, s in TYPES.items() if s["family"] == "solid") +FIN = next(t for t, s in TYPES.items() if s["family"] == "fin" and s["angle"] > 0) + + +def case(tid, mutate_kwargs=None, post=None): + builder, kwargs = P.builder_args(TYPES[tid], p) + body = builder(**{**kwargs, **(mutate_kwargs or {})}) + if post is not None: + body = post(body) + return tid, SimpleNamespace(part_id=tid, body=body) + + +def _untagged_vision(_body): + kw = P.builder_args(TYPES[VISION], p)[1] + ops = P.openings(kw["w"], kw["h"], kw["spandrel"], kw["frame_sight"], kw["joint"]) + z1 = kw["frame_d"] - kw["glass_recess"] + body = P._frame_ring(kw["w"], kw["h"], ops, kw["frame_d"], kw["joint"]) + body = scad.union_rsolid(body, P._rect_plate("s", *P._grow(ops["spandrel"], 30.0), z1 - kw["glass_t"], + kw["frame_d"] - kw["glass_recess"] / 2.0)) + return scad.union_rsolid(body, P._rect_plate("g", *P._grow(ops["vision"], 30.0), z1 - kw["glass_t"], z1)) + + +def _tag_solid(body): + kw = P.builder_args(TYPES[SOLID], p)[1] + op = P.openings(kw["w"], kw["h"], 0.0, kw["frame_sight"], kw["joint"])["vision"] + return P._tag_front_face(body, op, kw["frame_d"] - kw["glass_recess"] / 2.0, P.GLASS_TAG) + + +cases = { + "KB0 clean vision": (case(VISION), True), + "KB0 clean solid": (case(SOLID), True), + "KB0 clean fin": (case(FIN), True), + "KB1 frame 1 mm shallow": (case(VISION, dict(frame_d=p.frame_d - 1.0)), False), + "KB2 glass 1 mm deeper": (case(VISION, dict(glass_recess=p.glass_recess + 1.0)), False), + "KB3 glass never tagged": (case(VISION, post=_untagged_vision), False), + "KB4 no joint": (case(VISION, dict(joint=0.0)), False), + "KB5 solid tagged as glass": (case(SOLID, post=_tag_solid), False), + "KB6 fin 0.1 deg off bin": (case(FIN, dict(angle=TYPES[FIN]["angle"] + 0.1)), False), + "KB7 fin sign flipped": (case(FIN, dict(angle=-TYPES[FIN]["angle"])), False), + "KB8 fin lifted 1 mm": (case(FIN, post=lambda b: scad.translate_shape(shape=b, vector=(0.0, 0.0, 1.0))), False), + "KB9 fin 1 mm thicker": (case(FIN, dict(fin_t=p.fin_t + 1.0)), False), +} +wrong = [] +for name, ((tid, part), should_pass) in cases.items(): + failures = check_body(tid, TYPES[tid], part, p) + passed = not failures + verdict = "as designed" if passed == should_pass else "GATE BROKEN" + print(f"--- {name:26s} => {'pass' if passed else 'fail'} ({verdict})" + + ("" if passed else f" first: {failures[0]} [+{len(failures) - 1}]")) + if passed != should_pass: + wrong.append(name) +sys.exit(report("S2 known-bad", [f"{name}: gate did not discriminate" for name in wrong])) diff --git a/examples/pcg_facade_tower/verify/s2_verify.py b/examples/pcg_facade_tower/verify/s2_verify.py new file mode 100644 index 00000000..27870663 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s2_verify.py @@ -0,0 +1,182 @@ +"""S2 gate: panel family (every vision / solid / fin type used by RECT, CHAMFER and L_SHAPE), measured geometry. + + P1 body Part id == type id; one solid; BRepCheck valid + P2 envelope panel bbox == [j/2, w - j/2] x [j/2, h - j/2] x [0, frame_d] (1e-6); V3 transport <= 1600 x 4200 x 600 + P3 frame +Z planar area at z = frame_d and -Z planar area at z = 0 both == frame ring area + P4 glass vision: exactly one face tagged facade.vision_glass, planar +Z at frame_d - glass_recess, + area == layout._glass_area (the WWR numerator); solid: no tagged face + P5 volume == ring + recessed plates (analytic, 1e-6 relative) + P6 fin (V5) side-face normals measure the bin angle +/- 0.05 deg; side planes fin_t apart; root flush on z = 0 + with width fin_t / cos(a); tip z <= fin_depth + 1 (building line: frame_d + fin_depth + 1); + y span [j/2, h - j/2]; volume fin_t * fin_depth * (h - j) + + ../../../.venv/bin/python s2_verify.py +""" +from __future__ import annotations + +import math +import sys + +from _common import report, tight_bbox + +import layout as L +import panels as P +from simplecadapi import ql + +TOL = 1.0e-6 +ENVELOPE = (1600.0, L.MAX_PANEL_H, 600.0) + + +def _faces(body, nz: float, z: float): + return ql.faces().where(ql.and_( + ql.prop("geom.type", "==", "PLANE"), + ql.prop("geom.normal.z", ">=", nz - 1.0e-9) if nz > 0 else ql.prop("geom.normal.z", "<=", nz + 1.0e-9), + ql.prop("geom.center.z", ">=", z - 1.0e-6), ql.prop("geom.center.z", "<=", z + 1.0e-6), + )).resolve(body) + + +def _area(faces) -> float: + return sum(f.get_area() for f in faces) + + +def _valid(body) -> bool: + from OCP.BRepCheck import BRepCheck_Analyzer + return BRepCheck_Analyzer(body.wrapped).IsValid() + + +def _rect_area(r) -> float: + return (r[2] - r[0]) * (r[3] - r[1]) + + +def check_panel(tid: str, spec: dict, body, p: L.FacadeParams) -> list[str]: + bad = [] + w, h, j, fd = spec["w"], spec["h"], p.joint, p.frame_d + spandrel = spec.get("spandrel", 0.0) + ops = P.openings(w, h, spandrel if spec["family"] == "vision" else 0.0, p.frame_sight, j) + + x0, y0, z0, x1, y1, z1 = tight_bbox(body) + want = (j / 2.0, j / 2.0, 0.0, w - j / 2.0, h - j / 2.0, fd) + if max(abs(a - b) for a, b in zip((x0, y0, z0, x1, y1, z1), want)) > TOL: + bad.append(f"P2 {tid}: bbox {tuple(round(v, 6) for v in (x0, y0, z0, x1, y1, z1))} != {want}") + size = (x1 - x0, y1 - y0, z1 - z0) + if any(s > e for s, e in zip(size, ENVELOPE)): + bad.append(f"P2 {tid}: size {tuple(round(s) for s in size)} exceeds transport {ENVELOPE}") + + ring = (w - j) * (h - j) - sum(_rect_area(r) for r in ops.values()) + front, back = _area(_faces(body, 1.0, fd)), _area(_faces(body, -1.0, 0.0)) + if abs(front - ring) > TOL * ring or abs(back - ring) > TOL * ring: + bad.append(f"P3 {tid}: frame face areas front {front:.3f} / back {back:.3f} != ring {ring:.3f}") + + tagged = ql.faces().where(ql.tag(P.GLASS_TAG)).resolve(body) + if spec["family"] == "vision": + design = L._glass_area(w, h, spandrel, p) + if len(tagged) != 1: + bad.append(f"P4 {tid}: {len(tagged)} tagged glass faces") + else: + g = tagged[0] + n, c = g.get_normal_at(), g.get_center() + if abs(n.z - 1.0) > 1.0e-9 or abs(c.z - (fd - p.glass_recess)) > TOL: + bad.append(f"P4 {tid}: glass face normal {n.z:.9f} at z {c.z:.6f}") + if abs(g.get_area() - design) > TOL * design or abs(_rect_area(ops["vision"]) - design) > TOL * design: + bad.append(f"P4 {tid}: glass area {g.get_area():.3f} / opening {_rect_area(ops['vision']):.3f} " + f"!= design {design:.3f}") + elif tagged: + bad.append(f"P4 {tid}: solid panel carries {len(tagged)} glass tags") + + plates = 0.0 + if spec["family"] == "vision": + plates += _rect_area(ops["vision"]) * p.glass_t + if "spandrel" in ops: + plates += _rect_area(ops["spandrel"]) * (p.glass_t + p.glass_recess / 2.0) + else: + plates += _rect_area(ops["vision"]) * (p.glass_t + p.glass_recess / 2.0) + vol = ring * fd + plates + if abs(body.get_volume() - vol) > TOL * vol: + bad.append(f"P5 {tid}: volume {body.get_volume():.3f} != {vol:.3f}") + return bad + + +def check_fin(tid: str, spec: dict, body, p: L.FacadeParams) -> list[str]: + bad = [] + a, h, j = spec["angle"], spec["h"], p.joint + th = math.radians(a) + sides = ql.faces().where(ql.and_( + ql.prop("geom.type", "==", "PLANE"), + ql.prop("geom.normal.y", ">=", -1.0e-9), ql.prop("geom.normal.y", "<=", 1.0e-9), + ql.prop("geom.normal.x", ">=", 1.0e-6), + )).resolve(body) + if len(sides) != 1: + bad.append(f"P6 {tid}: {len(sides)} +x side faces") + else: + n = sides[0].get_normal_at() + measured = math.degrees(math.atan2(-n.z, n.x)) # side along (sin a, cos a) -> outward normal (cos a, -sin a) + if abs(measured - a) > 0.05: + bad.append(f"P6 {tid}: side normal measures {measured:.4f} deg != bin {a}") + c_r = sides[0].get_center() + opp = ql.faces().where(ql.and_(ql.prop("geom.type", "==", "PLANE"), + ql.prop("geom.normal.x", "<=", -1.0e-6))).resolve(body) + if len(opp) == 1: + c_l = opp[0].get_center() + gap = (c_r.x - c_l.x) * n.x + (c_r.z - c_l.z) * n.z + if abs(gap - p.fin_t) > TOL: + bad.append(f"P6 {tid}: side planes {gap:.6f} apart != fin_t {p.fin_t}") + else: + bad.append(f"P6 {tid}: {len(opp)} -x side faces") + root = _faces(body, -1.0, 0.0) + want_root = p.fin_t / math.cos(th) * (h - j) + if abs(_area(root) - want_root) > TOL * want_root: + bad.append(f"P6 {tid}: root area {_area(root):.3f} != {want_root:.3f} (not flush on the frame face)") + x0, y0, z0, x1, y1, z1 = tight_bbox(body) + if abs(z0) > TOL or z1 > p.fin_depth + 1.0 or abs(y0 - j / 2.0) > TOL or abs(y1 - (h - j / 2.0)) > TOL: + bad.append(f"P6 {tid}: bbox z [{z0:.6f}, {z1:.3f}] y [{y0:.6f}, {y1:.6f}]") + if a != 0.0 and math.copysign(1.0, (x0 + x1) / 2.0) != math.copysign(1.0, a): + bad.append(f"P6 {tid}: tip leans to x {(x0 + x1) / 2.0:.1f}, angle {a} (sign convention broken)") + if any(s > e for s, e in zip((x1 - x0, y1 - y0, z1 - z0), ENVELOPE)): + bad.append(f"P6 {tid}: fin exceeds transport {ENVELOPE}") + vol = p.fin_t * p.fin_depth * (h - j) + if abs(body.get_volume() - vol) > TOL * vol: + bad.append(f"P6 {tid}: volume {body.get_volume():.3f} != {vol:.3f}") + return bad + + +def all_types() -> dict: + """Union of the vision / solid / fin types of the three default plans (identical ids must mean identical specs).""" + out = {} + for kind in L.PlanKind: + for tid, spec in L.build_schedule(L.default_params(plan_kind=kind)).types.items(): + if spec["family"] == "corner": + continue + if out.setdefault(tid, spec) != spec: + raise AssertionError(f"type id {tid} names two specs: {out[tid]} / {spec}") + return out + + +def check_body(tid: str, spec: dict, part, p: L.FacadeParams) -> list[str]: + bad = [] + if part.part_id != tid: + bad.append(f"P1 {tid}: part id {part.part_id}") + solids = ql.solids().resolve(part.body) + if len(solids) != 1: + return bad + [f"P1 {tid}: {len(solids)} solids"] + if not _valid(part.body): + bad.append(f"P1 {tid}: BRep invalid") + return bad + (check_fin if spec["family"] == "fin" else check_panel)(tid, spec, part.body, p) + + +def main() -> int: + p = L.default_params() + types = all_types() + failures, parts = [], {} + for tid, spec in sorted(types.items()): + result = P.make_panel_part(tid, spec, p) + parts[tid] = result.value + failures += check_body(tid, spec, result.value, p) + fams = {} + for spec in types.values(): + fams[spec["family"]] = fams.get(spec["family"], 0) + 1 + print(f"-- checked {len(types)} types {fams}") + return report("S2 panels", failures) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/pcg_facade_tower/verify/s3_known_bad.py b/examples/pcg_facade_tower/verify/s3_known_bad.py new file mode 100644 index 00000000..9f7449b3 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s3_known_bad.py @@ -0,0 +1,105 @@ +"""S3 known-bad proof: the S3 checks must FAIL on each deliberately wrong solid / placement and PASS on the clean ones. + +Bodies are built from mutated builder arguments but checked against the unchanged schedule (a drifted builder). + + KB0 clean corners (+90 / +45 / -90), slab, canopy -> must pass (L_SHAPE; +45 from CHAMFER) + KB1 corner legs 1 mm long (corner_c + 1) -> K2 (end faces) + K6 (joint) + KB2 corner 1 mm shallow (frame_d - 1) -> K1 (volume) + K2 (outer faces) + KB3 +90 corner built as +45 -> K1 + K2 + KB4 corner built without joint -> K1 (y span) + K6 + KB5 slab on the building line (slab_gap 0) -> K4 + K3 (clearance) + KB6 slab 1 mm thick -> K4 (z span) + KB7 a floor slab placed on top of its level -> K3 (level) + KB8 canopy shifted one module east -> K5 (centre) + KB9 canopy against the panel face (no standoff) -> K7 + KB10 roof slabs floor-style, on top of the roof level -> K3 (roof end + terrace cut by the upper tier) +""" +import sys +from types import SimpleNamespace + +from _common import report +from s3_verify import Parts, check_canopy, check_canopy_fit, check_corner, check_corner_fit, check_slab, check_slab_fit +from s3_verify import fit_bands + +import corners as C +import layout as L +import slabs as S + +p = L.default_params(plan_kind=L.PlanKind.L_SHAPE) +sched = L.build_schedule(p) +chamfer = L.build_schedule(L.default_params(plan_kind=L.PlanKind.CHAMFER)) + + +def corner_case(which: float, **mutate) -> list[str]: + """Tier-0 tower band corner of this turn (L_SHAPE has +/-90, CHAMFER has +45).""" + s = sched if abs(which) == 90.0 else chamfer + band = next(b for b in fit_bands(s) if b["band"] == L.Band.TOWER.value and b["tier"] == 0) + rec = next(r for r in band["corners"] if abs(r["turn"] - which) < 1.0e-9) + tid, spec = rec["type_id"], s.types[rec["type_id"]] + body = C.build_corner(**{**C.builder_args(spec, s.params)[1], **mutate}) + parts = Parts(s) + parts.bodies[tid] = body + return (check_corner(tid, spec, SimpleNamespace(part_id=tid, body=body), s.params) + + check_corner_fit(parts, band, rec)) + + +def slab_case(plan=None, slab_t=None, instances=None) -> list[str]: + kwargs = S.slab_args(sched, 0) + body = S.build_slab(plan=plan or kwargs["plan"], slab_t=slab_t or kwargs["slab_t"]) + bodies = {k: S.make_slab_part(sched, k).value.body for k in range(len(sched.tiers))} + bodies[0] = body + real = L.slab_instances + if instances is not None: + L.slab_instances = lambda _s: instances + try: + return (check_slab(sched, 0, SimpleNamespace(part_id=S.slab_type_id(0), body=body)) + + check_slab_fit(Parts(sched), bodies)) + finally: + L.slab_instances = real + + +def canopy_case(dx=0.0, dy=0.0) -> list[str]: + kwargs = S.canopy_args(sched) + body = S.build_canopy(**{**kwargs, "x0": kwargs["x0"] + dx, "x1": kwargs["x1"] + dx, "y1": kwargs["y1"] + dy}) + return (check_canopy(sched, SimpleNamespace(part_id=S.CANOPY_ID, body=body)) + + check_canopy_fit(Parts(sched), body)) + + +def _raised_floor() -> list[dict]: + out = [dict(i) for i in L.slab_instances(sched)] + out[3]["level"] += p.slab_t + return out + + +def _floor_style_roofs() -> list[dict]: + return [dict(i, level=sched.levels[i["role"]]) if i["role"].startswith("roof") else dict(i) + for i in L.slab_instances(sched)] + + +cases = { + "KB0 clean corner +90": (lambda: corner_case(90.0), True), + "KB0 clean corner +45": (lambda: corner_case(45.0), True), + "KB0 clean corner -90": (lambda: corner_case(-90.0), True), + "KB0 clean slab": (lambda: slab_case(), True), + "KB0 clean canopy": (lambda: canopy_case(), True), + "KB1 corner legs +1 mm": (lambda: corner_case(90.0, corner_c=p.corner_c + 1.0), False), + "KB2 corner 1 mm shallow": (lambda: corner_case(-90.0, frame_d=p.frame_d - 1.0), False), + "KB3 +90 built as +45": (lambda: corner_case(90.0, turn=45.0), False), + "KB4 corner without joint": (lambda: corner_case(90.0, joint=0.0), False), + "KB5 slab on building line": (lambda: slab_case(plan=tuple(map(tuple, sched.tiers[0]))), False), + "KB6 slab 1 mm thick": (lambda: slab_case(slab_t=p.slab_t + 1.0), False), + "KB7 floor slab raised": (lambda: slab_case(instances=_raised_floor()), False), + "KB8 canopy one module east": (lambda: canopy_case(dx=p.w_std), False), + "KB9 canopy on panel face": (lambda: canopy_case(dy=p.joint), False), + "KB10 floor-style roof slabs": (lambda: slab_case(instances=_floor_style_roofs()), False), +} +wrong = [] +for name, (run, should_pass) in cases.items(): + failures = run() + passed = not failures + verdict = "as designed" if passed == should_pass else "GATE BROKEN" + print(f"--- {name:28s} => {'pass' if passed else 'fail'} ({verdict})" + + ("" if passed else f" first: {failures[0]} [+{len(failures) - 1}]")) + if passed != should_pass: + wrong.append(name) +sys.exit(report("S3 known-bad", [f"{name}: gate did not discriminate" for name in wrong])) diff --git a/examples/pcg_facade_tower/verify/s3_verify.py b/examples/pcg_facade_tower/verify/s3_verify.py new file mode 100644 index 00000000..d9ae3d44 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s3_verify.py @@ -0,0 +1,309 @@ +"""S3 gate: corner pieces, floor slabs and the entrance canopy, measured on the built solids. + +Part checks (local frame): + K1 corner Part id == type id; one solid; BRepCheck valid; y span [j/2, h - j/2]; + volume == profile area x (h - j) + K4 slab one solid, valid; z span [0, slab_t]; one side face per tier edge, each parallel to it and + slab_gap inside the building line; volume == |plan area| x slab_t + K5 canopy one solid, valid; bbox == layout canopy box; centred on the south edge; both ends on lobby + module joints; z0 on the lobby row joint +Fit checks (placed in world through the layout frames, per plan, one band of each (tier, kind)): + K2 corner (V7) outer faces coplanar with both panel bands' outer planes (building line + frame_d), + building-line faces on the building line, end faces at corner_c - joint/2 from the vertex + K6 joint min distance corner <-> last panel of the incoming edge and first panel of the outgoing edge == joint + (+/- 5e-4: A7 rounds type widths to 0.001 mm, the joint absorbs it) + K3 slab floor slabs sit on a realized floor band level of their tier (ground at 0), roof slabs end at + their tier's roof (parapet level); a tier's first floor stands on the roof below; min distance + slab <-> same-level panels / corners == slab_gap (roof: the top floor's); terrace: roof k-1 <-> + tier k first-floor panels / corners / fins == joint/2 (the panels stand j/2 above it) + K7 canopy min distance canopy <-> south lobby panels == joint (the standoff) + + ../../../.venv/bin/python s3_verify.py +""" +from __future__ import annotations + +import sys + +from _common import min_distance, place_shape, report, tight_bbox + +import corners as C +import layout as L +import panels as P +import simplecadapi as scad +import slabs as S +from simplecadapi import ql + +TOL = 1.0e-6 +# A7: panel types are built at the width rounded to 0.001 mm and placed at the exact layout start, so the joint +# after a closer absorbs up to 5e-4 mm. +JOINT_TOL = 0.5 * 10.0 ** -L.TYPE_ROUND_DIGITS + TOL + + +def _valid(body) -> bool: + from OCP.BRepCheck import BRepCheck_Analyzer + return BRepCheck_Analyzer(body.wrapped).IsValid() + + +def _one_valid_solid(tag: str, tid: str, part) -> list[str]: + if part.part_id != tid: + return [f"{tag} {tid}: part id {part.part_id}"] + solids = ql.solids().resolve(part.body) + if len(solids) != 1: + return [f"{tag} {tid}: {len(solids)} solids"] + return [] if _valid(part.body) else [f"{tag} {tid}: BRep invalid"] + + +def _side_planes(body) -> list[tuple]: + """(unit plan normal, plan centre, area) of every vertical planar face.""" + out = [] + for f in ql.faces().where(ql.prop("geom.type", "==", "PLANE")).resolve(body): + n, c = f.get_normal_at(), f.get_center() + if abs(n.z) < 1.0e-9: + out.append(((n.x, n.y), (c.x, c.y), f.get_area())) + return out + + +def _has_plane(planes, normal, anchor, offset) -> bool: + """A face with this outward plan normal whose plane lies `offset` along it from `anchor`.""" + return any(abs(n[0] - normal[0]) < 1.0e-9 and abs(n[1] - normal[1]) < 1.0e-9 + and abs(L._dot(normal, L._sub(c, anchor)) - offset) < TOL for n, c, _ in planes) + + +# ---------------------------------------------------------------- part checks + + +def check_corner(tid: str, spec: dict, part, p: L.FacadeParams) -> list[str]: + bad = _one_valid_solid("K1", tid, part) + if bad: + return bad + j, h = p.joint, spec["h"] + _, y0, _, _, y1, _ = tight_bbox(part.body) + if abs(y0 - j / 2.0) > TOL or abs(y1 - (h - j / 2.0)) > TOL: + bad.append(f"K1 {tid}: y span [{y0:.6f}, {y1:.6f}] != [{j / 2.0}, {h - j / 2.0}]") + area = abs(L.signed_area(C.corner_section(spec["turn"], p.corner_c, p.frame_d, j))) + vol = area * (h - j) + if abs(part.body.get_volume() - vol) > TOL * vol: + bad.append(f"K1 {tid}: volume {part.body.get_volume():.3f} != profile x height {vol:.3f}") + return bad + + +def check_slab(sched: L.FacadeSchedule, tier: int, part) -> list[str]: + tid = S.slab_type_id(tier) + bad = _one_valid_solid("K4", tid, part) + if bad: + return bad + p = sched.params + _, _, z0, _, _, z1 = tight_bbox(part.body) + if abs(z0) > TOL or abs(z1 - p.slab_t) > TOL: + bad.append(f"K4 {tid}: z span [{z0:.6f}, {z1:.6f}] != [0, {p.slab_t}]") + planes = _side_planes(part.body) + frames = L.edge_frames([tuple(v) for v in sched.tiers[tier]]) + if len(planes) != len(frames): + bad.append(f"K4 {tid}: {len(planes)} side faces for {len(frames)} tier edges") + for e, fr in enumerate(frames): + if not _has_plane(planes, fr["n"], fr["v0"], -p.slab_gap): + bad.append(f"K4 {tid}: no side face slab_gap {p.slab_gap} inside edge {e}") + vol = abs(L.signed_area(L.slab_plans(sched)[tier])) * p.slab_t + if abs(part.body.get_volume() - vol) > TOL * vol: + bad.append(f"K4 {tid}: volume {part.body.get_volume():.1f} != {vol:.1f}") + return bad + + +def check_canopy(sched: L.FacadeSchedule, part) -> list[str]: + bad = _one_valid_solid("K5", S.CANOPY_ID, part) + if bad: + return bad + box, lobby = L.canopy_box(sched), sched.band("lobby") + want = (box["x0"], box["y0"], box["z0"], box["x1"], box["y1"], box["z1"]) + got = tight_bbox(part.body) + if max(abs(a - b) for a, b in zip(got, want)) > TOL: + bad.append(f"K5 canopy: bbox {tuple(round(v, 6) for v in got)} != {want}") + fr = L.edge_frames([tuple(v) for v in sched.tiers[0]])[0] + mid = fr["v0"][0] + fr["length"] / 2.0 + if abs((got[0] + got[3]) / 2.0 - mid) > TOL: + bad.append(f"K5 canopy: centre x {(got[0] + got[3]) / 2.0:.6f} != south edge midpoint {mid:.6f}") + south = [r for r in lobby["panels"] if r["edge"] == 0] + starts = [fr["v0"][0] + r["s0"] for r in south] + ends = [fr["v0"][0] + r["s0"] + r["w"] for r in south] + if min(abs(got[0] - s) for s in starts) > TOL or min(abs(got[3] - e) for e in ends) > TOL: + bad.append(f"K5 canopy: ends x [{got[0]:.6f}, {got[3]:.6f}] not on south lobby module joints") + if lobby["rows"] > 1 and abs(got[2] - lobby["height"]) > TOL: + bad.append(f"K5 canopy: z0 {got[2]:.6f} not on the lobby row joint {lobby['height']}") + return bad + + +# ---------------------------------------------------------------- fit checks (world) + + +class Parts: + """Part bodies by type id, memoized here to avoid rebuilding within one gate.""" + + def __init__(self, sched: L.FacadeSchedule): + self.sched, self.bodies = sched, {} + + def body(self, tid: str): + if tid not in self.bodies: + spec, p = self.sched.types[tid], self.sched.params + make = C.make_corner_part if spec["family"] == "corner" else P.make_panel_part + self.bodies[tid] = make(tid, spec, p).value.body + return self.bodies[tid] + + def panel(self, band: dict, rec: dict, level: float): + return place_shape(self.body(rec["type_id"]), L.panel_frame(self.sched, band, rec, level)) + + def corner(self, band: dict, rec: dict, level: float): + return place_shape(self.body(rec["type_id"]), L.corner_frame(self.sched, band, rec, level)) + + +def fit_bands(sched: L.FacadeSchedule) -> list[dict]: + """One band per (tier, kind): corners and closers repeat unchanged across a tier's phases.""" + seen, out = set(), [] + for b in sched.bands: + if (b["tier"], b["band"]) not in seen: + seen.add((b["tier"], b["band"])) + out.append(b) + return out + + +def _edge_panel(band: dict, edge: int, first: bool) -> dict: + recs = [r for r in band["panels"] if r["edge"] == edge] + return min(recs, key=lambda r: r["slot"]) if first else max(recs, key=lambda r: r["slot"]) + + +def check_corner_fit(parts: Parts, band: dict, rec: dict) -> list[str]: + sched, p = parts.sched, parts.sched.params + level = L.band_levels(sched, band["band_id"])[0] + poly = [tuple(v) for v in sched.tiers[band["tier"]]] + frames = L.edge_frames(poly) + i = rec["vertex"] + fin, fout, v = frames[i - 1], frames[i], poly[i] + body = parts.corner(band, rec, level) + planes, c = _side_planes(body), p.corner_c - p.joint / 2.0 + where = f"{band['band_id']} v{i} ({rec['turn']:+.1f} deg)" + bad = [] + for name, normal, offset in (("incoming outer", fin["n"], p.frame_d), ("outgoing outer", fout["n"], p.frame_d), + ("incoming building line", L._mul(fin["n"], -1.0), 0.0), + ("outgoing building line", L._mul(fout["n"], -1.0), 0.0), + ("incoming end", L._mul(fin["t"], -1.0), c), ("outgoing end", fout["t"], c)): + if not _has_plane(planes, normal, v, offset): + bad.append(f"K2 {where}: no {name} face at {offset}") + _, _, z0, _, _, z1 = tight_bbox(body) + if abs(z0 - (level + p.joint / 2.0)) > TOL or abs(z1 - (level + band["height"] - p.joint / 2.0)) > TOL: + bad.append(f"K2 {where}: z span [{z0:.6f}, {z1:.6f}] off the band") + for side, prec in (("incoming", _edge_panel(band, (i - 1) % len(poly), first=False)), + ("outgoing", _edge_panel(band, i, first=True))): + gap = min_distance(body, parts.panel(band, prec, level)) + if abs(gap - p.joint) > JOINT_TOL: + bad.append(f"K6 {where}: {side} panel {prec['type_id']} gap {gap:.6f} != joint {p.joint}") + return bad + + +def check_slab_fit(parts: Parts, slab_bodies: dict) -> list[str]: + sched, p = parts.sched, parts.sched.params + bad = [] + floor_levels = {0: {0.0}} + for b in sched.bands: + if b["band"] == L.Band.TOWER.value: + floor_levels.setdefault(b["tier"], set()).update(round(z, 6) for z in L.band_levels(sched, b["band_id"])) + insts = L.slab_instances(sched) + for inst in insts: + if inst["role"].startswith("roof"): + roof = sched.levels[inst["role"]] + if abs(inst["level"] + p.slab_t - roof) > TOL or abs(sched.band(f"parapet{inst['tier']}")["level"] - roof) > TOL: + bad.append(f"K3 slab {inst['role']}: [{inst['level']}, +slab_t] does not end at the roof {roof}") + elif round(inst["level"], 6) not in floor_levels[inst["tier"]]: + bad.append(f"K3 slab {inst['role']}: level {inst['level']} is no floor level of tier {inst['tier']}") + have = {round(i["level"], 6) for i in insts if not i["role"].startswith("roof")} + for rec in sched.floors: + if round(rec["level"], 6) not in have: + below = sched.levels.get(f"roof{rec['tier'] - 1}") + if below is None or abs(below - rec["level"]) > TOL: + bad.append(f"K3 floor{rec['floor']}: no slab and not standing on a roof") + + def placed_slab(inst): + return scad.translate_shape(shape=slab_bodies[inst["tier"]], vector=(0.0, 0.0, inst["level"])) + + def edge_ends(band, level): + near = [parts.panel(band, _edge_panel(band, e, first=True), level) for e in {r["edge"] for r in band["panels"]}] + return near + [parts.corner(band, rec, level) for rec in band["corners"]] + + for band in fit_bands(sched): # floor slabs at their band level, behind the spandrel + level = L.band_levels(sched, band["band_id"])[0] + inst = next((i for i in insts if abs(i["level"] - level) < TOL and not i["role"].startswith("roof")), None) + if inst is None: + continue + gap = min(min_distance(placed_slab(inst), shape) for shape in edge_ends(band, level)) + if abs(gap - p.slab_gap) > TOL: + bad.append(f"K3 {band['band_id']}: slab clearance {gap:.6f} != slab_gap {p.slab_gap}") + for inst in (i for i in insts if i["role"].startswith("roof")): + k = inst["tier"] + top = next(b for b in sched.bands if b["band"] == L.Band.TOWER.value and b["tier"] == k + and max(sched.floor_types[b["band_id"]]["floors"]) == max( + r["floor"] for r in sched.floors if r["tier"] == k)) + gap = min(min_distance(placed_slab(inst), shape) for shape in edge_ends(top, inst["level"] + p.slab_t - p.floor_h)) + if abs(gap - p.slab_gap) > TOL: + bad.append(f"K3 roof{k}: clearance to the top floor {gap:.6f} != slab_gap {p.slab_gap}") + if k + 1 >= len(sched.tiers): + continue + first = sched.band(f"tier{k + 1}-phase0") + level = sched.levels[f"roof{k}"] + near = [parts.panel(first, r, level) for r in first["panels"]] + edge_ends(first, level) + near += [place_shape(parts.body(r["type_id"]), L.fin_frame(sched, first, r, level)) for r in first["fins"]] + gap = min(min_distance(placed_slab(inst), shape) for shape in near) + if abs(gap - p.joint / 2.0) > TOL: + bad.append(f"K3 roof{k} terrace: clearance to tier {k + 1} first floor {gap:.6f} != joint/2 {p.joint / 2.0}") + return bad + + +def check_canopy_fit(parts: Parts, canopy_body) -> list[str]: + sched, p = parts.sched, parts.sched.params + band = sched.band("lobby") + south = [r for r in band["panels"] if r["edge"] == 0] + gaps = [min_distance(canopy_body, parts.panel(band, r, level)) + for level in L.band_levels(sched, "lobby") for r in south] + if abs(min(gaps) - p.joint) > TOL: + return [f"K7 canopy: standoff {min(gaps):.6f} != joint {p.joint}"] + return [] + + +def corner_types() -> dict: + out = {} + for kind in L.PlanKind: + for tid, spec in L.build_schedule(L.default_params(plan_kind=kind)).types.items(): + if spec["family"] == "corner" and out.setdefault(tid, spec) != spec: + raise AssertionError(f"type id {tid} names two specs: {out[tid]} / {spec}") + return out + + +def main() -> int: + failures = [] + p0 = L.default_params() + ctypes = corner_types() + for tid, spec in sorted(ctypes.items()): + failures += check_corner(tid, spec, C.make_corner_part(tid, spec, p0).value, p0) + turns = sorted({s["turn"] for s in ctypes.values()}) + print(f"-- {len(ctypes)} corner types, turns {turns}") + + for kind in L.PlanKind: + sched = L.build_schedule(L.default_params(plan_kind=kind)) + parts = Parts(sched) + slab_bodies = {} + for k in range(len(sched.tiers)): + part = S.make_slab_part(sched, k).value + failures += check_slab(sched, k, part) + slab_bodies[k] = part.body + n_fit = 0 + for band in fit_bands(sched): + for rec in band["corners"]: + failures += check_corner_fit(parts, band, rec) + n_fit += 1 + failures += check_slab_fit(parts, slab_bodies) + canopy = S.make_canopy_part(sched).value + failures += check_canopy(sched, canopy) + check_canopy_fit(parts, canopy.body) + print(f"-- {kind.value}: {len(sched.tiers)} slab types, {len(L.slab_instances(sched))} slab instances, " + f"{n_fit} corner fits, canopy {L.canopy_box(sched)['modules']} modules") + return report("S3 corners / slabs / canopy", failures) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/pcg_facade_tower/verify/s4_known_bad.py b/examples/pcg_facade_tower/verify/s4_known_bad.py new file mode 100644 index 00000000..0acfa60b --- /dev/null +++ b/examples/pcg_facade_tower/verify/s4_known_bad.py @@ -0,0 +1,131 @@ +"""S4 known-bad proof: the S4 checks must FAIL on each deliberately wrong band assembly and PASS on the clean ones. + +Assemblies are rebuilt from the layout component list with one edit, then checked against the unchanged schedule. +Each bad case must fire (at least) the listed gates; the clean ones none. + + KB0 clean tier0-phase0 / lobby -> must pass + KB1 one panel 1 mm along the edge -> A1 + V1 (joints 21 / 19) + KB2 one panel 30 mm along the edge (into neighbour) -> A1 + V1 (overlap) + V2 + KB3 one panel missing -> A0 + V1 + KB4 one panel not grounded -> A0 + KB5 a vision panel swapped for a solid of its width -> A0 + V4 + KB6 a fin root pushed into its panel (frame_d in) -> A1 + V2 + KB7 a panel turned inside out (x = -t) -> A1 + KB8 a corner piece with the wrong turn -> A0 + V1 + KB9 lobby upper row dropped 30 mm (joint is 20) -> A1 + V1 (z span, row joint) + V2 + KB10 a warm rebuild of a different design -> A2 +""" +import sys + +from _common import report +from s4_verify import check_band, check_solve + +import floor as F +import layout as L +import simplecadapi as scad + +sched = L.build_schedule(L.default_params()) +p = sched.params +FLOOR, LOBBY = sched.band("tier0-phase0"), sched.band("lobby") + + +def assembly(band: dict, edit=None, unground=()) -> scad.Assembly: + comps = F.band_components(sched, band) + if edit: + comps = edit([dict(c, frame=dict(c["frame"])) for c in comps]) + parts = F.type_parts(sched, [c["type_id"] for c in comps]) + asm = scad.make_assembly_rassembly(assembly_id=F.band_assembly_id(band["band_id"])) + for c in comps: + f = c["frame"] + asm = scad.add_component_rassembly( + assembly=asm, item=parts[c["type_id"]].value, component_id=c["component_id"], + placement=scad.make_placement_rplacement(origin=f["origin"], x_axis=f["x_axis"], y_axis=f["y_axis"])) + if c["component_id"] not in unground: + asm = scad.ground_component_rassembly(assembly=asm, component_id=c["component_id"]) + return asm + + +def _find(comps, cid): + return next(c for c in comps if c["component_id"] == cid) + + +def shift(cid, d, dz=0.0, axis=None): + def edit(comps): + for c in comps: + if c["component_id"] == cid or (cid.endswith("*") and c["component_id"].startswith(cid[:-1])): + a = axis or c["frame"]["x_axis"] + o = c["frame"]["origin"] + c["frame"]["origin"] = (o[0] + d * a[0], o[1] + d * a[1], o[2] + dz) + return comps + return edit + + +def drop(cid): + return lambda comps: [c for c in comps if c["component_id"] != cid] + + +def retype(cid, type_id): + def edit(comps): + _find(comps, cid)["type_id"] = type_id + return comps + return edit + + +def flip(cid): + def edit(comps): + c = _find(comps, cid) + rec = next(r for r in FLOOR["panels"] if f"r0-e{r['edge']}-s{r['slot']}" == cid) + o, (tx, ty, _) = c["frame"]["origin"], c["frame"]["x_axis"] + c["frame"]["origin"] = (o[0] + rec["w"] * tx, o[1] + rec["w"] * ty, o[2]) + c["frame"]["x_axis"] = (-tx, -ty, 0.0) + return comps + return edit + + +vision = next(r for r in FLOOR["panels"] if r["kind"] == "vision" and r["edge"] == 0 and r["fin"] is not None) +VISION_ID = f"r0-e0-s{vision['slot']}" +SOLID_SAME_W = L.solid_type_id(vision["w"], FLOOR["height"]) +sched.types.setdefault(SOLID_SAME_W, {"family": "solid", "w": L._r(vision["w"]), "h": L._r(FLOOR["height"])}) +n0 = L.edge_frames(sched.tiers[0])[0]["n"] +other_turn = next(r["type_id"] for r in FLOOR["corners"] if r["type_id"] != FLOOR["corners"][0]["type_id"]) + + +def band_case(band, **kw) -> list[str]: + return check_band(sched, band, assembly(band, **kw))[0] + + +def solve_case() -> list[str]: + """A warm rebuild that is not the same design (different band) must not pass A2.""" + cold = F.make_band_assembly(sched, "parapet0") + warm = F.make_band_assembly(sched, "lobby") + return check_solve("parapet0", cold, warm) + + +cases = { + "KB0 clean tier0-phase0": (lambda: band_case(FLOOR), set()), + "KB0 clean lobby": (lambda: band_case(LOBBY), set()), + "KB0 clean solve": (lambda: check_solve("parapet0", F.make_band_assembly(sched, "parapet0"), + F.make_band_assembly(sched, "parapet0")), set()), + "KB1 panel +1 mm": (lambda: band_case(FLOOR, edit=shift("r0-e0-s4", 1.0)), {"A1", "V1"}), + "KB2 panel +30 mm": (lambda: band_case(FLOOR, edit=shift("r0-e0-s4", 30.0)), {"A1", "V1", "V2"}), + "KB3 panel missing": (lambda: band_case(FLOOR, edit=drop("r0-e3-s5")), {"A0", "V1"}), + "KB4 panel ungrounded": (lambda: band_case(FLOOR, unground=("r0-e1-s3",)), {"A0"}), + "KB5 vision -> solid": (lambda: band_case(FLOOR, edit=retype(VISION_ID, SOLID_SAME_W)), {"A0", "V4"}), + "KB6 fin into its panel": (lambda: band_case(FLOOR, edit=shift(VISION_ID + "-fin", -p.frame_d, + axis=(n0[0], n0[1], 0.0))), {"A1", "V2"}), + "KB7 panel inside out": (lambda: band_case(FLOOR, edit=flip("r0-e0-s6")), {"A1"}), + "KB8 corner wrong turn": (lambda: band_case(FLOOR, edit=retype("r0-v0", other_turn)), {"A0", "V1"}), + "KB9 lobby row 1 -30 mm": (lambda: band_case(LOBBY, edit=shift("r1-*", 0.0, dz=-30.0)), {"A1", "V1", "V2"}), + "KB10 warm rebuild, different design": (solve_case, {"A2"}), +} +wrong = [] +for name, (run, gates) in cases.items(): + failures = run() + fired = {f.split()[0] for f in failures} + ok = fired == gates if not gates else gates <= fired + print(f"--- {name:26s} => {'pass' if not failures else 'fail ' + '+'.join(sorted(fired))} " + f"({'as designed' if ok else 'GATE BROKEN, want ' + '+'.join(sorted(gates))})" + + ("" if not failures else f" first: {failures[0]} [+{len(failures) - 1}]")) + if not ok: + wrong.append(name) +sys.exit(report("S4 known-bad", [f"{name}: gate did not discriminate" for name in wrong])) diff --git a/examples/pcg_facade_tower/verify/s4_verify.py b/examples/pcg_facade_tower/verify/s4_verify.py new file mode 100644 index 00000000..b027379c --- /dev/null +++ b/examples/pcg_facade_tower/verify/s4_verify.py @@ -0,0 +1,283 @@ +"""S4 gate: band subassemblies (floor types, lobby, parapets), measured on the solved component placements. + +Every component is placed here from its solved `Placement` (columns x, y, z, origin -> gp_Trsf), independently of +the layout frames, then: + A0 contents component ids / types == schedule (panels + fins + corners per row); definitions == the band's + unique type ids; every component grounded (assembly and definition) + A1 placement placement == layout frame (origin, x = t, y = +Z) and z_axis == outward normal n; the SDK's own + projection (make_compound_from_assembly) has the same volume and bbox as ours + V1 closure per row and edge, corner v_e | panels by slot | corner v_e+1 in that order along t (vertex + projection), consecutive gap == joint (A7 tolerance), no overlap; fins excluded. Every piece spans + z [row + j/2, row + h - j/2]; stacked lobby rows meet at a joint + V2 clash all component pairs whose boxes touch: Common volume == 0 (panels, fins, corners, both rows) + V4 glass band glass == sum of tagged glass area on the definition bodies (tags stay local, SDK scope rule) + == layout design glass; WWR of this band per orientation is reported + A2 solve rebuild is deterministic: a second build of the same band carries the identical definition hash + + ../../../.venv/bin/python s4_verify.py [band_id ...] (default: every band of the default schedule) +""" +from __future__ import annotations + +import sys +import time + +from _common import report, tight_bbox +from s3_verify import JOINT_TOL, TOL + +import floor as F +import layout as L +import panels as P +import simplecadapi as scad +from simplecadapi import ql + +CLASH_TOL = 1.0e-3 # mm^3 of Common volume + + +def trsf(pl): + """gp_Trsf of a solved Placement (columns x, y, z, origin).""" + from OCP.gp import gp_Trsf + + (xx, xy, xz), (yx, yy, yz), (zx, zy, zz), (ox, oy, oz) = pl.x_axis, pl.y_axis, pl.z_axis, pl.origin + out = gp_Trsf() + out.SetValues(xx, yx, zx, ox, xy, yy, zy, oy, xz, yz, zz, oz) + return out + + +def placed(component, outer=None): + """World shape of a band component from its solved placement (diagnostic, not the SDK path); `outer` = the + parent's gp_Trsf when the band is itself placed (tower).""" + from OCP.TopLoc import TopLoc_Location + + t = trsf(component.placement) + if outer is not None: + t = outer.Multiplied(t) + return component.item.body.wrapped.Moved(TopLoc_Location(t)) + + +def _points(shape) -> list[tuple]: + from OCP.BRep import BRep_Tool + from OCP.TopAbs import TopAbs_VERTEX + from OCP.TopExp import TopExp_Explorer + from OCP.TopoDS import TopoDS + + out, it = [], TopExp_Explorer(shape, TopAbs_VERTEX) + while it.More(): + pnt = BRep_Tool.Pnt_s(TopoDS.Vertex_s(it.Current())) + out.append((pnt.X(), pnt.Y(), pnt.Z())) + it.Next() + return out + + +def _volume(shape) -> float: + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + + props = GProp_GProps() + BRepGProp.VolumeProperties_s(shape, props) + return props.Mass() + + +def _common_volume(a, b) -> float: + from OCP.BRepAlgoAPI import BRepAlgoAPI_Common + + op = BRepAlgoAPI_Common(a, b) + if not op.IsDone(): + raise RuntimeError("Common failed") + return _volume(op.Shape()) + + +def expected_components(sched: L.FacadeSchedule, band: dict) -> dict: + return {c["component_id"]: c for c in F.band_components(sched, band)} + + +def check_contents(sched: L.FacadeSchedule, band: dict, asm, definition=None) -> list[str]: + bad, want = [], expected_components(sched, band) + got = {c.component_id: c for c in asm.components} + if set(got) != set(want): + bad.append(f"A0 {band['band_id']}: missing {sorted(set(want) - set(got))[:4]} " + f"extra {sorted(set(got) - set(want))[:4]}") + for cid in sorted(set(got) & set(want)): + if got[cid].item.part_id != want[cid]["type_id"]: + bad.append(f"A0 {band['band_id']} {cid}: type {got[cid].item.part_id} != {want[cid]['type_id']}") + if set(asm.grounded_component_ids) != set(got): + bad.append(f"A0 {band['band_id']}: ungrounded {sorted(set(got) - set(asm.grounded_component_ids))[:4]}") + if definition is not None: + if set(definition.grounded_instance_ids) != set(got): + bad.append(f"A0 {band['band_id']}: definition grounds {len(definition.grounded_instance_ids)}/{len(got)}") + refs = {getattr(r, "definition_id", r) for r in definition.definition_refs} + types = {c["type_id"] for c in want.values()} + if len(refs) != len(types): + bad.append(f"A0 {band['band_id']}: {len(refs)} definitions for {len(types)} types") + return bad + + +def check_placements(sched: L.FacadeSchedule, band: dict, asm, shapes: dict) -> list[str]: + bad, want = [], expected_components(sched, band) + normals = [fr["n"] for fr in L.edge_frames(sched.tiers[band["tier"]])] + for c in asm.components: + if c.component_id not in want: + continue + f, pl = want[c.component_id]["frame"], c.placement + edge = (int(c.component_id.split("-v")[1]) if "-v" in c.component_id + else int(c.component_id.split("-e")[1].split("-")[0])) + n = normals[edge - 1] if "-v" in c.component_id else normals[edge] # corner x = incoming edge + dev = max(max(abs(a - b) for a, b in zip(pl.origin, f["origin"])), + 1.0e3 * max(abs(a - b) for a, b in zip(pl.x_axis, f["x_axis"])), + 1.0e3 * max(abs(a - b) for a, b in zip(pl.y_axis, f["y_axis"])), + 1.0e3 * max(abs(a - b) for a, b in zip(pl.z_axis, (n[0], n[1], 0.0)))) + if dev > TOL: + bad.append(f"A1 {band['band_id']} {c.component_id}: placement off layout frame by {dev:.3g}") + compound = scad.make_compound_from_assembly_rcompound(assembly=asm) + vol = sum(_volume(s) for s in shapes.values()) + sdk_vol = _volume(compound.wrapped) + if abs(sdk_vol - vol) > TOL * vol: + bad.append(f"A1 {band['band_id']}: SDK projection volume {sdk_vol:.6g} != {vol:.6g}") + ours = [tight_bbox(s) for s in shapes.values()] + box = tuple(min(b[i] for b in ours) for i in range(3)) + tuple(max(b[i] for b in ours) for i in range(3, 6)) + if max(abs(a - b) for a, b in zip(tight_bbox(compound), box)) > TOL: + bad.append(f"A1 {band['band_id']}: SDK projection bbox != ours") + return bad + + +def check_closure(sched: L.FacadeSchedule, band: dict, shapes: dict, z_base: float = 0.0, + label: str | None = None) -> list[str]: + """`z_base` = the band's level when the shapes are tower-placed; `label` names the realization in messages.""" + bad, p = [], sched.params + name = label or band["band_id"] + frames = L.edge_frames(sched.tiers[band["tier"]]) + j, h, n = p.joint, band["height"], len(frames) + spans = {} + for cid, shape in shapes.items(): + z = [pt[2] for pt in _points(shape)] + spans[cid] = (min(z), max(z)) + for row in range(band["rows"]): + z0 = z_base + row * h + for cid in (c for c in shapes if c.startswith(f"r{row}-")): + lo, hi = spans[cid] + if abs(lo - (z0 + j / 2.0)) > TOL or abs(hi - (z0 + h - j / 2.0)) > TOL: + bad.append(f"V1 {name} {cid}: z span [{lo:.6f}, {hi:.6f}] != row {row} band") + for e, fr in enumerate(frames): + order = ([f"r{row}-v{e}"] + [f"r{row}-e{e}-s{r['slot']}" for r in + sorted((r for r in band["panels"] if r["edge"] == e), key=lambda r: r["slot"])] + + [f"r{row}-v{(e + 1) % n}"]) + missing = [cid for cid in order if cid not in shapes] + if missing: + bad.append(f"V1 {name} r{row} e{e}: no piece at {missing[:3]}") + order = [cid for cid in order if cid in shapes] + v0, t = fr["v0"], fr["t"] + ext = {} + for cid in order: + s = [(x - v0[0]) * t[0] + (y - v0[1]) * t[1] for x, y, _ in _points(shapes[cid])] + ext[cid] = (min(s), max(s)) + ext[order[0]] = (float("-inf"), ext[order[0]][1]) # corner at the edge start: only its max faces edge e + ext[order[-1]] = (ext[order[-1]][0], float("inf")) # corner at the edge end: only its min + by_pos = sorted(order, key=lambda cid: ext[cid][0]) + if by_pos != order: + bad.append(f"V1 {name} r{row} e{e}: order along t {by_pos[:3]}... != schedule") + continue + for a, b in zip(order, order[1:]): + gap = ext[b][0] - ext[a][1] + if abs(gap - j) > JOINT_TOL: + bad.append(f"V1 {name} {a} | {b}: gap {gap:.6f} != joint {j}") + for row in range(1, band["rows"]): # stacked lobby rows + below = max(spans[c][1] for c in shapes if c.startswith(f"r{row - 1}-")) + above = min(spans[c][0] for c in shapes if c.startswith(f"r{row}-")) + if abs(above - below - j) > TOL: + bad.append(f"V1 {name} rows {row - 1}|{row}: gap {above - below:.6f} != joint {j}") + return bad + + +def check_clash(band: dict, shapes: dict) -> tuple[list[str], int]: + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + + boxes = {} + for cid, shape in shapes.items(): + box = Bnd_Box() + BRepBndLib.Add_s(shape, box) + box.Enlarge(1.0e-3) + boxes[cid] = box + bad, ids, tested = [], sorted(shapes), 0 + for i, a in enumerate(ids): + for b in ids[i + 1:]: + if boxes[a].IsOut(boxes[b]): + continue + tested += 1 + v = _common_volume(shapes[a], shapes[b]) + if v > CLASH_TOL: + bad.append(f"V2 {band['band_id']} {a} x {b}: Common volume {v:.4g} mm^3") + return bad, tested + + +def band_glass(sched: L.FacadeSchedule, band: dict, asm) -> tuple[dict, dict, dict]: + """(measured glass, design glass, gross facade area) per orientation, one realization of the band.""" + p, area_by_type = sched.params, {} + orient = {f"r{row}-e{r['edge']}-s{r['slot']}": r["orientation"] + for row in range(band["rows"]) for r in band["panels"]} + glass, design, gross = {}, {}, {} + for c in asm.components: + if c.component_id not in orient: + continue + tid = c.item.part_id + if tid not in area_by_type: + area_by_type[tid] = sum(f.get_area() for f in ql.faces().where(ql.tag(P.GLASS_TAG)).resolve(c.item.body)) + o = orient[c.component_id] + glass[o] = glass.get(o, 0.0) + area_by_type[tid] + for rec in band["panels"]: + if rec["kind"] == "vision": + spandrel = sched.types[rec["type_id"]]["spandrel"] + design[rec["orientation"]] = (design.get(rec["orientation"], 0.0) + + band["rows"] * L._glass_area(rec["w"], band["height"], spandrel, p)) + for fr in L.edge_frames(sched.tiers[band["tier"]]): + o = L.orientation(fr["n"]) + gross[o] = gross.get(o, 0.0) + (fr["length"] - 2.0 * p.corner_c) * band["height"] * band["rows"] + return glass, design, gross + + +def check_glass(sched: L.FacadeSchedule, band: dict, asm) -> tuple[list[str], dict]: + glass, design, gross = band_glass(sched, band, asm) + bad = [] + for o in sorted(set(glass) | set(design)): + g, d = glass.get(o, 0.0), design.get(o, 0.0) + if abs(g - d) > TOL * max(d, 1.0): + bad.append(f"V4 {band['band_id']} {o}: tagged glass {g:.1f} != design {d:.1f}") + return bad, {o: glass.get(o, 0.0) / gross[o] for o in sorted(gross)} + + +def check_band(sched: L.FacadeSchedule, band: dict, asm, definition=None) -> tuple[list[str], dict]: + bad = check_contents(sched, band, asm, definition) + shapes = {c.component_id: placed(c) for c in asm.components} + bad += check_placements(sched, band, asm, shapes) + bad += check_closure(sched, band, shapes) + clash, tested = check_clash(band, shapes) + glass, wwr = check_glass(sched, band, asm) + return bad + clash + glass, {"components": len(shapes), "clash_pairs": tested, "wwr": wwr} + + +def check_solve(band_id: str, cold, warm) -> list[str]: + bad = [] + if warm.definition.content_hash != cold.definition.content_hash: + bad.append(f"A2 {band_id}: rebuild changed the definition hash") + return bad + + +def main(band_ids: list[str]) -> int: + sched = L.build_schedule(L.default_params()) + failures = [] + for band_id in band_ids or [b["band_id"] for b in sched.bands]: + start = time.perf_counter() + band = sched.band(band_id) + cold = F.make_band_assembly(sched, band_id) + warm = F.make_band_assembly(sched, band_id) + bad, info = check_band(sched, band, cold.value, cold.definition) + bad += check_solve(band_id, cold, warm) + failures += bad + wwr = " ".join(f"{o}={v:.3f}" for o, v in info["wwr"].items()) + print(f" {band_id:14s} {info['components']:4d} comps {len(cold.definition.definition_refs):3d} defs " + f"{info['clash_pairs']:4d} clash pairs wwr {wwr} {'ok' if not bad else f'{len(bad)} FAIL'} " + f"({time.perf_counter() - start:.1f}s)") + return report("S4", failures) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/examples/pcg_facade_tower/verify/s5_known_bad.py b/examples/pcg_facade_tower/verify/s5_known_bad.py new file mode 100644 index 00000000..81d34837 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s5_known_bad.py @@ -0,0 +1,120 @@ +"""S5 known-bad proof: the S5 checks must FAIL on each deliberately wrong tower assembly and PASS on the clean one. + +Towers are rebuilt from the tower component list with one edit (same band / slab / canopy definitions), then checked +against the unchanged schedule. Each bad case must fire (at least) the listed gates; the clean ones none. + + KB0 clean tower -> must pass + KB1 floor2 30 mm high (into floor3, a sampled floor) -> V8 + V1 + V2 + KB2 floor7 missing -> T0 + V4 (T1 is SDK-vs-ours, self-consistent here) + KB3 floor13 (tier 1's first) given a tier 0 band -> T0 + V8 + V2 (into parapet0) + V4 + KB4 parapet1 standing on roof 0 -> V8 + V1 + V2 + KB5 roof slab 0 floor-style (on the roof, not below it) -> V8 + V2 (cuts tier 1's first floor) + KB6 floor5 with the wrong fin phase -> T0 + V4 + KB7 floor20 not grounded -> T0 + KB8 slab under floor2 pushed 40 mm east (slab_gap is 30) -> T1 + V2 + KB9 floor3 turned 90 degrees about its centre -> T1 + V1 + V5 + KB10 a warm rebuild of a different design -> A2 +""" +import math +import sys + +from _common import report +from s4_verify import check_solve +from s5_verify import check_tower + +import layout as L +import simplecadapi as scad +from tower import TOWER_ID, definitions, make_tower, tower_components + +sched = L.build_schedule(L.default_params()) +p = sched.params +COMPS = tower_components(sched) +DEFS = definitions(sched, COMPS) + + +def tower(edit=None, unground=()) -> scad.Assembly: + comps = [dict(c) for c in COMPS] + if edit: + comps = edit(comps) + asm = scad.make_assembly_rassembly(assembly_id=TOWER_ID) + for c in comps: + dx, dy = c.get("xy", (0.0, 0.0)) + a = math.radians(c.get("rot", 0.0)) + cx, cy = c.get("centre", (0.0, 0.0)) + # rotation about (cx, cy): origin = centre - R centre + ox = cx - (math.cos(a) * cx - math.sin(a) * cy) + dx + oy = cy - (math.sin(a) * cx + math.cos(a) * cy) + dy + asm = scad.add_component_rassembly( + assembly=asm, item=DEFS[c["ref"]].value, component_id=c["component_id"], + placement=scad.make_placement_rplacement(origin=(ox, oy, c["level"]), x_axis=(math.cos(a), math.sin(a), 0.0), + y_axis=(-math.sin(a), math.cos(a), 0.0))) + if c["component_id"] not in unground: + asm = scad.ground_component_rassembly(assembly=asm, component_id=c["component_id"]) + return asm + + +def _find(comps, cid): + return next(c for c in comps if c["component_id"] == cid) + + +def change(cid, **kw): + def edit(comps): + c = _find(comps, cid) + for key, value in kw.items(): + c[key] = value(c[key]) if callable(value) else value + return comps + return edit + + +def drop(cid): + return lambda comps: [c for c in comps if c["component_id"] != cid] + + +def tower_case(**kw) -> list[str]: + return check_tower(sched, tower(**kw))[0] + + +def solve_case() -> list[str]: + """A warm rebuild that is not the same design (edited schedule) must not pass A2.""" + cold = make_tower(sched) + warm = make_tower(L.build_schedule(L.default_params(n_floors=35))) + return check_solve("tower", cold, warm) + + +first13 = _find(COMPS, "floor13") +poly0 = sched.tiers[0] +centre = (sum(v[0] for v in poly0) / len(poly0), sum(v[1] for v in poly0) / len(poly0)) +phase5 = _find(COMPS, "floor5")["ref"][1] +other_phase = next(b["band_id"] for b in sched.bands + if b.get("tier") == 0 and b["band"] == L.Band.TOWER.value and b["band_id"] != phase5) +roof0 = sched.levels["roof0"] + +cases = { + "KB0 clean tower": (lambda: tower_case(), set()), + "KB1 floor2 +30 mm": (lambda: tower_case(edit=change("floor2", level=lambda z: z + 30.0)), {"V8", "V1", "V2"}), + "KB2 floor7 missing": (lambda: tower_case(edit=drop("floor7")), {"T0", "V4"}), + "KB3 floor13 tier-0 band": (lambda: tower_case(edit=change("floor13", ref=("band", "tier0-phase0"))), + {"T0", "V8", "V2", "V4"}), + "KB4 parapet1 on roof 0": (lambda: tower_case(edit=change("parapet1", level=roof0)), {"V8", "V1", "V2"}), + "KB5 floor-style roof slab 0": (lambda: tower_case(edit=change("slab-roof0", level=roof0)), {"V8", "V2"}), + "KB6 floor5 wrong phase": (lambda: tower_case(edit=change("floor5", ref=("band", other_phase))), {"T0", "V4"}), + "KB7 floor20 ungrounded": (lambda: tower_case(unground=("floor20",)), {"T0"}), + "KB8 slab-floor2 +40 mm E": (lambda: tower_case(edit=change("slab-floor2", xy=(40.0, 0.0))), {"T1", "V2"}), + "KB9 floor3 turned 90": (lambda: tower_case(edit=change("floor3", rot=90.0, centre=centre)), {"T1", "V1", "V5"}), + "KB10 warm rebuild, different design": (solve_case, {"A2"}), +} +assert first13["ref"][1].startswith("tier1-"), first13 +only = sys.argv[1:] +wrong = [] +for name, (run, gates) in cases.items(): + if only and not any(name.startswith(o + " ") for o in only): + continue + failures = run() + fired = {f.split()[0] for f in failures} + ok = fired == gates if not gates else gates <= fired + print(f"--- {name:28s} => {'pass' if not failures else 'fail ' + '+'.join(sorted(fired))} " + f"({'as designed' if ok else 'GATE BROKEN, want ' + '+'.join(sorted(gates))})" + + ("" if not failures else f" first: {failures[0]} [+{len(failures) - 1}]"), flush=True) + if not ok: + wrong.append(name) +sys.exit(report("S5 known-bad", [f"{name}: gate did not discriminate" for name in wrong])) diff --git a/examples/pcg_facade_tower/verify/s5_verify.py b/examples/pcg_facade_tower/verify/s5_verify.py new file mode 100644 index 00000000..49f7a0c3 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s5_verify.py @@ -0,0 +1,359 @@ +"""S5 gate: the tower assembly (band subassemblies + slabs + canopy), measured on the solved, composed placements. + +Band pieces are placed in world here as tower placement x band placement (gp_Trsf product, not the SDK path); +levels and setbacks are recomputed from the parameters, not read back from the schedule. + + T0 contents tower component ids / items == tower_components; all grounded; one definition per unique ref; + leaf part set == schedule type table; flattened piece count == schedule instance count + T1 placement every tower placement is a pure translation (0, 0, level); the SDK projection of the whole tower + has pieces + slabs + canopy solids, the summed volume and the bbox z [0, height - j/2] + V8 levels floor f at lobby_h + (f - 1) floor_h with a band of tier (f - 1) // setback_every; parapet k and + roof slab k top at tier k's roof; measured on sampled realizations: building-line plane of every edge + at k x setback_depth inside tier 0's + V1 closure sampled realizations (lobby, first floor of each floor type, both floors of every setback, the top + floor, every parapet): S4 closure at the tower level + V2 clash windows around every sampled junction (element below | element | slabs at the junction | parapet on a + setback | canopy on the lobby), plus every component whose measured z range overlaps the window: + Common volume == 0 on every bbox-touching pair + V4 glass whole-building WWR per orientation from the tagged definition glass x realizations == design (1e-6) + and within 0.03 of the target + V5 envelope sampled realizations: panels span exactly [0, frame_d] off their edge's building line, corners + <= frame_d, fin tips <= frame_d + fin_depth + 1 + V6 types standard panel types (w_std x floor_h, vision + solid) == 2; fin types <= fin_bins + A2 solve warm rebuild: no dirty instances, no corrupt entries, identical definition hash + + ../../../.venv/bin/python s5_verify.py +""" +from __future__ import annotations + +import sys +import time + +from _common import report, tight_bbox +from s3_verify import TOL +from s4_verify import _points, _volume, check_clash, check_closure, check_solve, placed, trsf + +import floor as F +import layout as L +import panels as P +import slabs as S +import simplecadapi as scad +from simplecadapi import Part +from tower import make_tower, tower_components + +WWR_TOL = 0.03 + + +def item_id(item) -> str: + return item.part_id if isinstance(item, Part) else item.assembly_id + + +def expected_item_id(ref) -> str: + kind, key = ref + return F.band_assembly_id(key) if kind == "band" else S.slab_type_id(key) if kind == "slab" else S.CANOPY_ID + + +def level_of(p: L.FacadeParams, floor: int) -> float: + return p.lobby_h + (floor - 1) * p.floor_h + + +def roof_of(p: L.FacadeParams, tier: int) -> float: + return level_of(p, min(p.n_floors, (tier + 1) * p.setback_every) + 1) + + +def realize(component) -> dict: + """World shapes of one tower component: band pieces keyed by their band id, a part by ''.""" + outer = trsf(component.placement) + if isinstance(component.item, Part): + from OCP.TopLoc import TopLoc_Location + return {"": component.item.body.wrapped.Moved(TopLoc_Location(outer))} + return {c.component_id: placed(c, outer) for c in component.item.components} + + +def band_of(sched: L.FacadeSchedule, component): + return sched.band(component.item.assembly_id.removeprefix("band-")) + + +# ---------------------------------------------------------------- contents, placements, levels (every component) + + +def check_contents(sched: L.FacadeSchedule, asm, definition=None) -> tuple[list[str], dict]: + bad, want = [], {c["component_id"]: c for c in tower_components(sched)} + got = {c.component_id: c for c in asm.components} + if set(got) != set(want): + bad.append(f"T0 missing {sorted(set(want) - set(got))[:4]} extra {sorted(set(got) - set(want))[:4]}") + for cid in sorted(set(got) & set(want)): + if item_id(got[cid].item) != expected_item_id(want[cid]["ref"]): + bad.append(f"T0 {cid}: item {item_id(got[cid].item)} != {expected_item_id(want[cid]['ref'])}") + if set(asm.grounded_component_ids) != set(got): + bad.append(f"T0 ungrounded {sorted(set(got) - set(asm.grounded_component_ids))[:4]}") + if definition is not None: + refs = {getattr(r, "definition_id", r) for r in definition.definition_refs} + if len(refs) != len({c["ref"] for c in want.values()}): + bad.append(f"T0 {len(refs)} definitions for {len({c['ref'] for c in want.values()})} refs") + if set(definition.grounded_instance_ids) != set(got): + bad.append(f"T0 definition grounds {len(definition.grounded_instance_ids)}/{len(got)}") + leaves, pieces = set(), 0 + for c in asm.components: + if not isinstance(c.item, Part): + leaves |= {sub.item.part_id for sub in c.item.components} + pieces += len(c.item.components) + if leaves != set(sched.types): + bad.append(f"T0 leaf parts: missing {sorted(set(sched.types) - leaves)[:3]} extra {sorted(leaves - set(sched.types))[:3]}") + if pieces != sched.stats["n_instances"]: + bad.append(f"T0 {pieces} facade pieces != schedule {sched.stats['n_instances']}") + return bad, {"components": len(got), "pieces": pieces, "leaf_types": len(leaves)} + + +def check_levels(sched: L.FacadeSchedule, asm) -> list[str]: + """T1 pure translations + V8 levels / tiers (placement values, every component).""" + bad, p = [], sched.params + n_tiers = (p.n_floors - 1) // p.setback_every + 1 + for c in asm.components: + pl = c.placement + axes = max(max(abs(a - b) for a, b in zip(pl.x_axis, (1, 0, 0))), + max(abs(a - b) for a, b in zip(pl.y_axis, (0, 1, 0))), + max(abs(a - b) for a, b in zip(pl.z_axis, (0, 0, 1))), abs(pl.origin[0]), abs(pl.origin[1])) + if axes > TOL: + bad.append(f"T1 {c.component_id}: placement is not a pure z translation ({axes:.3g})") + cid, z = c.component_id, pl.origin[2] + if cid.startswith("floor"): + f = int(cid[5:]) + band = band_of(sched, c) + if abs(z - level_of(p, f)) > TOL: + bad.append(f"V8 {cid}: at z {z:.3f} != {level_of(p, f):.3f}") + if band.get("tier") != (f - 1) // p.setback_every or band["band"] != L.Band.TOWER.value: + bad.append(f"V8 {cid}: band {band['band_id']} is not a tier {(f - 1) // p.setback_every} floor band") + elif cid.startswith("parapet"): + k = int(cid[7:]) + if abs(z - roof_of(p, k)) > TOL or band_of(sched, c).get("tier") != k: + bad.append(f"V8 {cid}: {band_of(sched, c)['band_id']} at z {z:.3f} != tier {k} roof {roof_of(p, k):.3f}") + elif cid.startswith("slab-roof"): + k = int(cid[9:]) + top = z + tight_bbox(c.item.body)[5] + if abs(top - roof_of(p, k)) > TOL: + bad.append(f"V8 {cid}: top {top:.3f} != tier {k} roof {roof_of(p, k):.3f}") + roofs = sorted(c.component_id for c in asm.components if c.component_id.startswith("parapet")) + if roofs != [f"parapet{k}" for k in range(n_tiers)]: + bad.append(f"V8 parapets {roofs} != one per tier ({n_tiers})") + return bad + + +def check_projection(sched: L.FacadeSchedule, asm) -> tuple[list[str], dict]: + """T1: the SDK's flattened tower vs definition volumes x counts.""" + from OCP.TopAbs import TopAbs_SOLID + from OCP.TopExp import TopExp_Explorer + + bad, p = [], sched.params + vol_by_item, want_vol, want_solids = {}, 0.0, 0 + for c in asm.components: + leaves = [c.item] if isinstance(c.item, Part) else [sub.item for sub in c.item.components] + for leaf in leaves: + if leaf.part_id not in vol_by_item: + vol_by_item[leaf.part_id] = _volume(leaf.body.wrapped) + want_vol += vol_by_item[leaf.part_id] + want_solids += 1 + compound = scad.make_compound_from_assembly_rcompound(assembly=asm) + solids, it = 0, TopExp_Explorer(compound.wrapped, TopAbs_SOLID) + while it.More(): + solids += 1 + it.Next() + vol = _volume(compound.wrapped) + box = tight_bbox(compound) + height = roof_of(p, (p.n_floors - 1) // p.setback_every) + p.parapet_h + if solids != want_solids: + bad.append(f"T1 SDK projection has {solids} solids != {want_solids}") + if abs(vol - want_vol) > TOL * want_vol: + bad.append(f"T1 SDK projection volume {vol:.6g} != {want_vol:.6g}") + if abs(box[2]) > TOL or abs(box[5] - (height - p.joint / 2.0)) > TOL: + bad.append(f"T1 SDK projection z [{box[2]:.3f}, {box[5]:.3f}] != [0, {height - p.joint / 2.0:.3f}]") + return bad, {"solids": solids, "bbox": box, "height": height} + + +# ---------------------------------------------------------------- sampled realizations (geometry) + + +def sample_ids(sched: L.FacadeSchedule) -> list[str]: + p = sched.params + floors = {1, p.n_floors} + floors |= {info["floors"][0] for info in sched.floor_types.values()} + for k in range((p.n_floors - 1) // p.setback_every): + top = (k + 1) * p.setback_every + floors |= {top, top + 1} + parapets = [c["component_id"] for c in tower_components(sched) if c["component_id"].startswith("parapet")] + return ["lobby"] + [f"floor{f}" for f in sorted(floors)] + parapets + + +def windows(sched: L.FacadeSchedule, asm, sampled: list[str]) -> list[tuple[str, list[str]]]: + """(name, tower component ids) around each sampled floor's lower junction, plus each tier's roof.""" + p, ids = sched.params, {c.component_id for c in asm.components} + out = [] + for cid in sampled: + if not cid.startswith("floor"): + continue + f = int(cid[5:]) + below = "lobby" if f == 1 else f"floor{f - 1}" + group = [below, cid, f"slab-floor{f}"] + if f == 1: + group += ["canopy", "slab-ground"] + if f > 1 and (f - 1) % p.setback_every == 0: + k = (f - 1) // p.setback_every - 1 + group += [f"parapet{k}", f"slab-roof{k}"] + out.append((f"{below}|{cid}", [g for g in group if g in ids])) + for k in range((p.n_floors - 1) // p.setback_every + 1): + top = min(p.n_floors, (k + 1) * p.setback_every) + out.append((f"floor{top}|parapet{k}", [g for g in (f"floor{top}", f"parapet{k}", f"slab-roof{k}") if g in ids])) + return out + + +def check_realization(sched: L.FacadeSchedule, component, shapes: dict) -> list[str]: + """V1 at the tower level + V8 setback depth + V5 envelope for one band realization.""" + p, band, cid = sched.params, band_of(sched, component), component.component_id + k = band["tier"] + z_base = (0.0 if cid == "lobby" else roof_of(p, int(cid[7:])) if cid.startswith("parapet") + else level_of(p, int(cid[5:]))) + bad = check_closure(sched, band, shapes, z_base=z_base, label=cid) + base = L.edge_frames(L.tier_plans(p)[0]) + own = L.edge_frames(L.tier_plans(p)[k]) + env = p.frame_d + p.fin_depth + 1.0 + inset = {} + for pid, shape in shapes.items(): + pts = _points(shape) + if "-v" in pid: + v = int(pid.split("-v")[1]) + edges = [(v - 1) % len(own), v] + else: + edges = [int(pid.split("-e")[1].split("-")[0])] + for e in edges: + fr = own[e] + d = [(x - fr["v0"][0]) * fr["n"][0] + (y - fr["v0"][1]) * fr["n"][1] for x, y, _ in pts] + if pid.endswith("-fin"): + if max(d) > env: + bad.append(f"V5 {cid} {pid}: fin tip {max(d):.3f} > {env:.3f} off the building line") + elif "-v" in pid: + if max(d) > p.frame_d + TOL: + bad.append(f"V5 {cid} {pid}: corner {max(d):.4f} > frame_d off edge {e}") + elif abs(min(d)) > TOL or abs(max(d) - p.frame_d) > TOL: + bad.append(f"V5 {cid} {pid}: spans [{min(d):.4f}, {max(d):.4f}] != [0, frame_d] off its edge") + if not pid.endswith("-fin") and "-v" not in pid: + b0 = base[e] + s = min((x - b0["v0"][0]) * b0["n"][0] + (y - b0["v0"][1]) * b0["n"][1] for x, y, _ in pts) + inset[e] = min(inset.get(e, 0.0), s) + for e, s in sorted(inset.items()): + if abs(-s - k * p.setback_depth) > TOL: + bad.append(f"V8 {cid} e{e}: building line {-s:.4f} inside tier 0's != {k} x setback {p.setback_depth}") + return bad + + +def z_ranges(asm) -> dict: + """Measured world z range of every tower component (local item bbox through its placement).""" + local, out = {}, {} + for c in asm.components: + key = item_id(c.item) + if key not in local: + body = c.item.body if isinstance(c.item, Part) else scad.make_compound_from_assembly_rcompound(assembly=c.item) + local[key] = tight_bbox(body) + b, t = local[key], trsf(c.placement) + zs = [t.Value(3, 1) * x + t.Value(3, 2) * y + t.Value(3, 3) * z + t.Value(3, 4) + for x in (b[0], b[3]) for y in (b[1], b[4]) for z in (b[2], b[5])] + out[c.component_id] = (min(zs), max(zs)) + return out + + +def check_sampled(sched: L.FacadeSchedule, asm) -> tuple[list[str], dict]: + comps = {c.component_id: c for c in asm.components} + sampled = [cid for cid in sample_ids(sched) if cid in comps] + # each window also takes whatever actually stands in its z range (a misplaced component meets its real neighbours) + zr, groups = z_ranges(asm), [] + for name, group in windows(sched, asm, sampled): + lo, hi = min(zr[g][0] for g in group), max(zr[g][1] for g in group) + groups.append((name, sorted(set(group) | {cid for cid, (a, b) in zr.items() if a < hi - 1.0 and b > lo + 1.0}))) + world, bad = {}, [] + for cid in {g for _, group in groups for g in group} | set(sampled): + world[cid] = realize(comps[cid]) + for cid in sampled: + bad += check_realization(sched, comps[cid], world[cid]) + pairs = 0 + for name, group in groups: + shapes = {f"{g}/{pid}" if pid else g: s for g in group for pid, s in world[g].items()} + clash, tested = check_clash({"band_id": name}, shapes) + bad += clash + pairs += tested + return bad, {"sampled": len(sampled), "windows": len(groups), "clash_pairs": pairs} + + +# ---------------------------------------------------------------- whole-building glass + types + + +def check_glass(sched: L.FacadeSchedule, asm) -> tuple[list[str], dict]: + from s4_verify import band_glass + + bad, p = [], sched.params + per_band, glass, gross = {}, {}, {} + for c in asm.components: + if not c.component_id.startswith("floor"): + continue + band = band_of(sched, c) + if band["band_id"] not in per_band: + per_band[band["band_id"]] = band_glass(sched, band, c.item) + g, _, a = per_band[band["band_id"]] + for o in a: + glass[o] = glass.get(o, 0.0) + g.get(o, 0.0) + gross[o] = gross.get(o, 0.0) + a[o] + wwr = {o: glass.get(o, 0.0) / gross[o] for o in sorted(gross)} + design, target = L.design_wwr(sched), dict(p.wwr_target) + for o, v in wwr.items(): + if abs(v - design[o]) > TOL: + bad.append(f"V4 {o}: measured WWR {v:.6f} != design {design[o]:.6f}") + if abs(v - target[o]) > WWR_TOL: + bad.append(f"V4 {o}: WWR {v:.3f} off target {target[o]:.2f} by more than {WWR_TOL}") + return bad, wwr + + +def check_types(sched: L.FacadeSchedule, asm) -> tuple[list[str], dict]: + p, bad = sched.params, [] + leaves = {sub.item.part_id for c in asm.components if not isinstance(c.item, Part) for sub in c.item.components} + std = sorted(t for t in leaves if sched.types[t]["family"] in ("vision", "solid") + and sched.types[t]["w"] == L._r(p.w_std) and sched.types[t]["h"] == L._r(p.floor_h)) + fins = sorted(t for t in leaves if sched.types[t]["family"] == "fin") + if len(std) != 2: + bad.append(f"V6 {len(std)} standard panel types {std} != 2") + if len(fins) > p.fin_bins: + bad.append(f"V6 {len(fins)} fin types > K = {p.fin_bins}") + return bad, {"standard": std, "fins": len(fins)} + + +def check_tower(sched: L.FacadeSchedule, asm, definition=None) -> tuple[list[str], dict]: + bad, info = check_contents(sched, asm, definition) + bad += check_levels(sched, asm) + proj, info["projection"] = check_projection(sched, asm) + sampled, info["sampled"] = check_sampled(sched, asm) + glass, info["wwr"] = check_glass(sched, asm) + types, info["types"] = check_types(sched, asm) + return bad + proj + sampled + glass + types, info + + +def main() -> int: + sched = L.build_schedule(L.default_params()) + t0 = time.perf_counter() + cold = make_tower(sched) + t1 = time.perf_counter() + warm = make_tower(sched) + t2 = time.perf_counter() + bad, info = check_tower(sched, cold.value, cold.definition) + bad += check_solve("tower", cold, warm) + t3 = time.perf_counter() + pr, sm = info["projection"], info["sampled"] + print(f" tower: {info['components']} components, {len(cold.definition.definition_refs)} definitions, " + f"{info['pieces']} facade pieces of {info['leaf_types']} types; SDK projection {pr['solids']} solids, " + f"height {pr['height']:.0f}") + print(f" sampled {sm['sampled']} realizations, {sm['windows']} junction windows, {sm['clash_pairs']} clash pairs") + print(f" WWR " + " ".join(f"{o}={v:.3f}" for o, v in info["wwr"].items()) + + f" (target {dict(sched.params.wwr_target)})") + print(f" standard types {info['types']['standard']}, {info['types']['fins']} fin types") + print(f" build {t1 - t0:.1f}s, rebuild {t2 - t1:.1f}s, checks {t3 - t2:.1f}s") + return report("S5", bad) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/pcg_facade_tower/verify/s6_verify.py b/examples/pcg_facade_tower/verify/s6_verify.py new file mode 100644 index 00000000..9a4e8cac --- /dev/null +++ b/examples/pcg_facade_tower/verify/s6_verify.py @@ -0,0 +1,159 @@ +"""S6 gate: rationalization / scale / plan sweeps — the same rules at any size. + + R1 rationalization K in {2, 3, 4, 6, 8, 12, 24} (fin bins): fin types <= K, total types non-decreasing in K, + max quantization error <= 90 / (2 (K - 1)) (half a bin), design WWR independent of K + R2 scale n_floors in {8, 20, 36, 48}, setback_every = ceil(n / 3) (3 tiers): the standard panel type + set is the same at every size; each tower is built and passes the S5 gate + R3 plan RECT / CHAMFER (36 floors): each tower is built and passes the S5 gate (V1, V4, ...); + L_SHAPE is layout-verified only — see the README "Known limitations" + +Artifacts: out/rationalization.png (types and error vs K), out/scale_sweep.json (per size / plan: types, pieces, +definitions, build seconds, S5 result). + + ../../../.venv/bin/python s6_verify.py [--no-geometry] +""" +from __future__ import annotations + +import json +import math +import sys +import time +from pathlib import Path + +from _common import PART_DIR, report +from s3_verify import TOL + +import layout as L + +OUT = PART_DIR / "out" +KS = (2, 3, 4, 6, 8, 12, 24) +SIZES = (8, 20, 36, 48) + + +def rationalization() -> tuple[list[str], list[dict]]: + bad, rows = [], [] + base_wwr = None + for k in KS: + s = L.build_schedule(L.default_params(fin_bins=k)) + st = s.stats + bound = 90.0 / (2.0 * (k - 1)) + row = {"K": k, "types": st["n_types"], "fin_types": len(st["fin_types"]), "instances": st["n_instances"], + "max_error": st["max_fin_quant_error"], "bound": bound, "wwr": st["design_wwr"]} + rows.append(row) + if row["fin_types"] > k: + bad.append(f"R1 K={k}: {row['fin_types']} fin types > K") + if row["max_error"] > bound + TOL: + bad.append(f"R1 K={k}: max quantization error {row['max_error']:.4f} > half bin {bound:.4f}") + base_wwr = base_wwr or st["design_wwr"] + if any(abs(st["design_wwr"][o] - base_wwr[o]) > TOL for o in base_wwr): + bad.append(f"R1 K={k}: design WWR {st['design_wwr']} depends on K") + for a, b in zip(rows, rows[1:]): + if b["types"] < a["types"]: + bad.append(f"R1 types fall from {a['types']} (K={a['K']}) to {b['types']} (K={b['K']})") + return bad, rows + + +def rationalization_figure(rows: list[dict], path: Path) -> str: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + ks = [r["K"] for r in rows] + fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10.5, 3.8)) + ink, muted = "#222222", "#8a8a8a" + ax0.plot(ks, [r["fin_types"] for r in rows], color=ink, lw=2, marker="o", ms=5) + for r in rows: + ax0.annotate(str(r["fin_types"]), (r["K"], r["fin_types"]), textcoords="offset points", xytext=(0, 7), + ha="center", fontsize=8, color=ink) + ax0.set_title("fin part types vs K (panels, corners, slabs unchanged)", fontsize=10) + ax0.set_ylabel("fin part types") + ax1.plot(ks, [r["max_error"] for r in rows], color=ink, lw=2, marker="o", ms=5) + ax1.plot(ks, [r["bound"] for r in rows], color=muted, lw=1, ls="--") + ax1.annotate("half bin 90/(2(K-1))", (ks[1], rows[1]["bound"]), textcoords="offset points", xytext=(8, 4), + fontsize=8, color=muted) + ax1.set_title("max fin angle quantization error vs K", fontsize=10) + ax1.set_ylabel("degrees") + for ax in (ax0, ax1): + ax.set_xscale("log", base=2) + ax.set_xticks(ks, [str(k) for k in ks]) + ax.set_xlabel("fin bins K") + ax.grid(True, color="#e6e6e6", lw=0.6) + ax.set_axisbelow(True) + for side in ("top", "right"): + ax.spines[side].set_visible(False) + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + return str(path) + + +def scale_schedules() -> tuple[list[str], dict]: + bad, out = [], {} + std = None + for n in SIZES: + s = L.build_schedule(L.default_params(n_floors=n, setback_every=math.ceil(n / 3))) + out[n] = s + if std is None: + std = s.stats["standard_panel_types"] + if s.stats["standard_panel_types"] != std: + bad.append(f"R2 {n} floors: standard types {s.stats['standard_panel_types']} != {std}") + if s.stats["n_tiers"] != 3: + bad.append(f"R2 {n} floors: {s.stats['n_tiers']} tiers != 3") + return bad, out + + +def build_and_check(tag: str, sched: L.FacadeSchedule) -> tuple[list[str], dict]: + from s5_verify import check_tower + from tower import make_tower + + start = time.perf_counter() + r = make_tower(sched) + built = time.perf_counter() - start + bad, info = check_tower(sched, r.value, r.definition) + st = sched.stats + row = {"plan": sched.params.plan_kind.value, "n_floors": sched.params.n_floors, + "setback_every": sched.params.setback_every, "height_m": st["height"] / 1000.0, "types": st["n_types"], + "standard_types": st["standard_panel_types"], "floor_types": st["n_floor_types"], + "pieces": info["pieces"], "tower_components": info["components"], + "definitions": len(r.definition.definition_refs), "build_s": round(built, 1), + "wwr": {o: round(v, 4) for o, v in info["wwr"].items()}, "s5_failures": len(bad)} + print(f" {tag:18s} {row['height_m']:6.1f} m {row['types']:3d} types {row['pieces']:5d} pieces " + f"{row['definitions']:3d} defs build {built:5.1f}s S5 {'ok' if not bad else f'{len(bad)} FAIL'}", flush=True) + return [f"{tag}: {b}" for b in bad], row + + +def main(geometry: bool) -> int: + OUT.mkdir(exist_ok=True) + failures, sweep = [], {"rationalization": [], "scale": [], "plan": []} + bad, rows = rationalization() + failures += bad + sweep["rationalization"] = rows + print(" K " + " ".join(f"{r['K']:6d}" for r in rows)) + print(" types " + " ".join(f"{r['types']:6d}" for r in rows)) + print(" fins " + " ".join(f"{r['fin_types']:6d}" for r in rows)) + print(" err " + " ".join(f"{r['max_error']:6.2f}" for r in rows)) + print(" " + rationalization_figure(rows, OUT / "rationalization.png")) + bad, scheds = scale_schedules() + failures += bad + for n, s in scheds.items(): + print(f" {n:2d} floors: {s.stats['n_types']} types, {s.stats['n_instances']} pieces, " + f"{s.stats['n_floor_types']} floor types, std {s.stats['standard_panel_types']}") + if geometry: + for n, s in scheds.items(): + bad, row = build_and_check(f"chamfer {n}F", s) + failures += bad + sweep["scale"].append(row) + # L_SHAPE is layout-verified only (see the example README "Known limitations"): + # its reflex-corner profile extends past frame_d and fails S5 V5 at tower fit. + bad, row = build_and_check(f"{L.PlanKind.RECT.value} 36F", L.build_schedule(L.default_params(plan_kind=L.PlanKind.RECT))) + failures += bad + sweep["plan"].append(row) + sweep["plan"].append(next(r for r in sweep["scale"] if r["n_floors"] == 36)) + path = OUT / "scale_sweep.json" + path.write_text(json.dumps(sweep, indent=1, default=str)) + print(f" {path}") + return report("S6", failures) + + +if __name__ == "__main__": + sys.exit(main(geometry="--no-geometry" not in sys.argv)) diff --git a/examples/pcg_facade_tower/verify/s7_verify.py b/examples/pcg_facade_tower/verify/s7_verify.py new file mode 100644 index 00000000..45914921 --- /dev/null +++ b/examples/pcg_facade_tower/verify/s7_verify.py @@ -0,0 +1,69 @@ +"""S7 report (V9): what a parameter edit costs — full rebuild timings per edit step. + +The part cache this gate used to verify was removed from the SDK (dev now rebuilds through the +notebook runtime), so there are no hit / dirty reports to assert anymore. What survives is the +question the gate always answered, now measured honestly as full rebuilds: how long each edit +step takes, and how the piece count moves. Every build is a complete, deterministic rebuild of +the same design policy. + +Steps: prime (default) -> warm (default again) -> fin_depth 300 -> 350 -> revert -> +n_floors 36 -> 48, setback_every 16 -> revert. Artifacts: out/incremental.json (per step: +build seconds, pieces, types). + + ../../../.venv/bin/python s7_verify.py +""" +from __future__ import annotations + +import json +import sys +import time + +from _common import PART_DIR, report + +import layout as L +import tower as T + + +class Run: + def __init__(self): + self.rows: list = [] + + def step(self, name: str, params: L.FacadeParams) -> list[str]: + start = time.perf_counter() + sched = L.build_schedule(params) + tower = T.make_tower(sched) + seconds = time.perf_counter() - start + st = sched.stats + row = { + "step": name, + "build_s": round(seconds, 1), + "pieces": st["n_instances"], + "types": st["n_types"], + "fin_types": len(st["fin_types"]), + "height_m": round(st["height"] / 1000.0, 2), + } + self.rows.append(row) + print(f" {name:22s} {seconds:6.1f}s pieces {row['pieces']:5d} types {row['types']:3d} " + f"fins {row['fin_types']:3d} {row['height_m']:6.2f} m", flush=True) + return [] + + +def main() -> int: + base = L.default_params() + run = Run() + bad = [] + bad += run.step("prime (default)", base) + bad += run.step("warm (default)", base) + bad += run.step("fin_depth 350", L.default_params(fin_depth=350.0)) + bad += run.step("revert fin_depth", base) + bad += run.step("n_floors 48 (every 16)", L.default_params(n_floors=48, setback_every=16)) + bad += run.step("revert n_floors", base) + out = PART_DIR / "out" + out.mkdir(exist_ok=True) + (out / "incremental.json").write_text(json.dumps(run.rows, indent=1)) + print(f" {out / 'incremental.json'}") + return report("S7 rebuild timings", bad) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/pcg_facade_tower/verify/s8_verify.py b/examples/pcg_facade_tower/verify/s8_verify.py new file mode 100644 index 00000000..9048ea8d --- /dev/null +++ b/examples/pcg_facade_tower/verify/s8_verify.py @@ -0,0 +1,238 @@ +"""S8 gate (V10): the exported tower reopens, in a fresh process, as the same product. + +Expected values come from the schedule and the per-definition part volumes of this process' build (not from the +package): + P1 package fresh process: read + materialize the .scadpkg; root id; distinct definitions = tower + bands + piece + types + tier slabs + canopy; leaf occurrences per part id = schedule counts (facade pieces) / slab + instances / 1 canopy; volume sum (part volume x multiplicity) within 1e-9 relative; the SDK world + compound of the reopened tower has that many solids and z extent [0, height - joint/2] + P2 STEP report: AP242, definition ids = the package's, occurrence count = Σ components over the assembly + definitions (each definition written once, occurrences are located references); fresh process: read + the STEP back -> leaf solids = pieces + slabs + canopy, volume within 1e-6 relative + P3 CSV one row per piece type, counts = schedule, Σ count = pieces +Known-bad (run after the gate): a truncated package copy must be rejected by the fresh process (KB1); a CSV with one +count off by one must fail P3 (KB2). + + ../../../.venv/bin/python s8_verify.py +""" +from __future__ import annotations + +import csv +import json +import subprocess +import sys +import time +from collections import Counter +from pathlib import Path + +from _common import PART_DIR, report + +import export as E +import layout as L +import slabs as S +import tower as T + +CHILD = r""" +import json, sys, time +import simplecadapi as scad +from OCP.BRepGProp import BRepGProp +from OCP.GProp import GProp_GProps +from OCP.IFSelect import IFSelect_RetDone +from OCP.STEPControl import STEPControl_Reader +from OCP.TopAbs import TopAbs_SOLID +from OCP.TopExp import TopExp_Explorer +from OCP.Bnd import Bnd_Box +from OCP.BRepBndLib import BRepBndLib + +def volume(shape): + g = GProp_GProps(); BRepGProp.VolumeProperties_s(shape, g); return g.Mass() + +pkg_path, step_path = sys.argv[1], sys.argv[2] +out = {} +start = time.perf_counter() +package = scad.read_product_package(pkg_path) +root = scad.materialize_definition(package.root_definition) +out["read_s"] = time.perf_counter() - start +out["root_id"] = package.root_id +leaves, defs, part_volume = {}, set(), {} + +def walk(item): # every occurrence path to a part is one leaf + if isinstance(item, scad.Assembly): + defs.add(("assembly", item.assembly_id)) + for c in item.components: + walk(c.item) + else: + defs.add(("part", item.part_id)) + leaves[item.part_id] = leaves.get(item.part_id, 0) + 1 + if item.part_id not in part_volume: + part_volume[item.part_id] = item.body.get_volume() + +walk(root) +out["definitions"] = sorted(f"{k}:{i}" for k, i in defs) +out["leaves"] = leaves +out["volume"] = sum(part_volume[p] * n for p, n in leaves.items()) +whole = scad.make_compound_from_assembly_rcompound(assembly=root).wrapped +n = 0 +ex = TopExp_Explorer(whole, TopAbs_SOLID) +while ex.More(): + n += 1; ex.Next() +box = Bnd_Box(); box.SetGap(0.0); BRepBndLib.AddOptimal_s(whole, box, useTriangulation=False) +out["world_solids"] = n +out["world_bbox"] = list(box.Get()) +start = time.perf_counter() +reader = STEPControl_Reader() +if reader.ReadFile(step_path) != IFSelect_RetDone: + raise SystemExit("STEP read failed") +reader.TransferRoots() +shape = reader.OneShape() +n, vol = 0, 0.0 +ex = TopExp_Explorer(shape, TopAbs_SOLID) +while ex.More(): + n += 1; vol += volume(ex.Current()); ex.Next() +out["step_solids"], out["step_volume"], out["step_read_s"] = n, vol, time.perf_counter() - start +print("RESULT " + json.dumps(out)) +""" + + +def fresh_process(package: Path, step: Path) -> tuple[int, dict | None, str]: + proc = subprocess.run([sys.executable, "-c", CHILD, str(package), str(step)], capture_output=True, text=True, + cwd=PART_DIR) + line = next((ln for ln in proc.stdout.splitlines() if ln.startswith("RESULT ")), None) + return proc.returncode, (json.loads(line[7:]) if line else None), proc.stderr[-600:] + + +def expected(sched: L.FacadeSchedule, result) -> dict: + comps = T.tower_components(sched) + leaves = Counter(sched.stats["instances_by_type"]) + for c in comps: + kind, key = c["ref"] + if kind == "slab": + leaves[S.slab_type_id(key)] += 1 + elif kind == "canopy": + leaves[S.CANOPY_ID] += 1 + bands = {c.item.assembly_id: c.item for c in result.value.components if hasattr(c.item, "assembly_id")} + parts = {c.item.part_id: c.item for b in bands.values() for c in b.components} + parts |= {c.item.part_id: c.item for c in result.value.components if hasattr(c.item, "part_id")} + defs = sorted([f"assembly:{T.TOWER_ID}"] + [f"assembly:{b}" for b in bands] + [f"part:{p}" for p in parts]) + return {"leaves": dict(leaves), "definitions": defs, "pieces": sum(leaves.values()), + "volume": sum(parts[p].body.get_volume() * n for p, n in leaves.items()), + "occurrences": len(result.value.components) + sum(len(b.components) for b in bands.values()), + "height": sched.stats["height"] - sched.params.joint / 2.0} + + +def check_package(want: dict, got: dict | None, code: int, err: str) -> list[str]: + if got is None: + return [f"P1 fresh process failed (exit {code}): {err.strip().splitlines()[-1] if err.strip() else '?'}"] + bad = [] + if got["root_id"] != T.TOWER_ID: + bad.append(f"P1 root {got['root_id']} != {T.TOWER_ID}") + if got["definitions"] != want["definitions"]: + missing = sorted(set(want["definitions"]) - set(got["definitions"])) + extra = sorted(set(got["definitions"]) - set(want["definitions"])) + bad.append(f"P1 definitions: missing {missing[:4]} extra {extra[:4]}") + if got["leaves"] != want["leaves"]: + diff = sorted(k for k in set(got["leaves"]) | set(want["leaves"]) if got["leaves"].get(k) != want["leaves"].get(k)) + bad.append(f"P1 leaf occurrences differ for {diff[:4]} (+{len(diff)})") + if abs(got["volume"] - want["volume"]) > 1e-9 * want["volume"]: + bad.append(f"P1 volume {got['volume']:.6e} != {want['volume']:.6e}") + if got["world_solids"] != want["pieces"]: + bad.append(f"P1 world compound {got['world_solids']} solids != {want['pieces']}") + z0, z1 = got["world_bbox"][2], got["world_bbox"][5] + if abs(z0) > 1e-6 or abs(z1 - want["height"]) > 1e-6: + bad.append(f"P1 world z [{z0:.4f}, {z1:.4f}] != [0, {want['height']:.4f}]") + return bad + + +def check_step(want: dict, rep, got: dict | None) -> list[str]: + bad = [] + if "AP242" not in rep.schema.upper(): + bad.append(f"P2 schema {rep.schema}") + ids = sorted(d.split(":", 1)[1] for d in want["definitions"]) + if sorted(rep.definition_ids) != ids: + bad.append(f"P2 {len(rep.definition_ids)} STEP definitions != {len(ids)}") + if rep.occurrence_count != want["occurrences"]: + bad.append(f"P2 {rep.occurrence_count} STEP occurrences != {want['occurrences']} (Σ assembly components)") + if got is not None: + if got["step_solids"] != want["pieces"]: + bad.append(f"P2 STEP read back {got['step_solids']} solids != {want['pieces']}") + if abs(got["step_volume"] - want["volume"]) > 1e-6 * want["volume"]: + bad.append(f"P2 STEP volume {got['step_volume']:.6e} != {want['volume']:.6e}") + return bad + + +def check_csv(sched: L.FacadeSchedule, path: Path) -> list[str]: + with path.open(newline="") as fh: + rows = list(csv.DictReader(fh)) + bad = [] + counts = sched.stats["instances_by_type"] + got = {r["type_id"]: int(r["count"]) for r in rows} + if len(rows) != len(sched.types) or set(got) != set(sched.types): + bad.append(f"P3 CSV {len(rows)} rows / types != {len(sched.types)} schedule types") + wrong = sorted(t for t in got if got[t] != counts.get(t)) + if wrong: + bad.append(f"P3 CSV counts differ for {wrong[:4]}") + if sum(got.values()) != sched.stats["n_instances"]: + bad.append(f"P3 CSV Σ count {sum(got.values())} != {sched.stats['n_instances']} pieces") + for r in rows: + spec = sched.types.get(r["type_id"], {}) + if r["family"] != spec.get("family") or float(r["h_mm"]) != spec.get("h"): + bad.append(f"P3 CSV row {r['type_id']}: family / h do not match the type table") + break + return bad + + +def known_bad(sched: L.FacadeSchedule, paths: dict) -> list[str]: + from _common import OUT as RENDERS # scratch space next to the renders, not in out/ + + wrong = [] + RENDERS.mkdir(exist_ok=True) + cut = RENDERS / "_kb_truncated.scadpkg" + data = Path(paths["scadpkg"]).read_bytes() + cut.write_bytes(data[: len(data) // 2]) + code, got, err = fresh_process(cut, Path(paths["step"])) + fired = got is None and code != 0 + print(f"--- KB1 truncated package => {'rejected' if fired else 'ACCEPTED'} " + f"({'as designed' if fired else 'GATE BROKEN'}) {err.strip().splitlines()[-1][:110] if err.strip() else ''}") + wrong += [] if fired else ["KB1"] + cut.unlink() + rows = E.panel_rows(sched) + rows[3]["count"] += 1 + off = RENDERS / "_kb_panels.csv" + with off.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=E.CSV_FIELDS) + writer.writeheader() + writer.writerows(rows) + bad = check_csv(sched, off) + print(f"--- KB2 CSV count off by one => {'fail ' + bad[0] if bad else 'pass'} " + f"({'as designed' if bad else 'GATE BROKEN'})") + wrong += [] if bad else ["KB2"] + off.unlink() + return [f"{k}: gate did not discriminate" for k in wrong] + + +def main() -> int: + sched = L.build_schedule(L.default_params()) + start = time.perf_counter() + out = E.export(sched) + total = time.perf_counter() - start + paths = out["paths"] + for name, path in paths.items(): + print(f" {name:8s} {path} ({Path(path).stat().st_size / 1e6:.2f} MB)") + print(" export " + ", ".join(f"{k} {v:.1f}s" for k, v in out["timings"].items()) + f" (total {total:.1f}s)") + want = expected(sched, out["result"]) + code, got, err = fresh_process(paths["scadpkg"], paths["step"]) + bad = check_package(want, got, code, err) + bad += check_step(want, out["step_report"], got) + bad += check_csv(sched, paths["csv"]) + if got: + print(f" fresh process: {len(got['definitions'])} definitions, {sum(got['leaves'].values())} leaves, " + f"{got['world_solids']} world solids, read {got['read_s']:.1f}s; STEP {got['step_solids']} solids, " + f"read {got['step_read_s']:.1f}s") + print(f" STEP {out['step_report'].schema}: {out['step_report'].occurrence_count} occurrences " + f"(expanded {want['pieces']}), {len(out['step_report'].definition_ids)} definitions") + status = report("S8", bad) + return status or report("S8 known-bad", known_bad(sched, paths)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/img/capability/editor.png b/img/capability/editor.png new file mode 100644 index 00000000..e8fde3af Binary files /dev/null and b/img/capability/editor.png differ diff --git a/img/capability/reverse_studio_mvp.gif b/img/capability/reverse_studio_mvp.gif deleted file mode 100644 index 9ed3cafd..00000000 Binary files a/img/capability/reverse_studio_mvp.gif and /dev/null differ diff --git a/img/capability/reverse_studio_mvp.mp4 b/img/capability/reverse_studio_mvp.mp4 deleted file mode 100644 index c9b57a74..00000000 Binary files a/img/capability/reverse_studio_mvp.mp4 and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index 11420f9f..91a9e5e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simplecadapi" -version = "2.2.0b1" +version = "2.2.1" description = "A simplified OCP-native CAD modeling Python API" readme = "README.md" license = { file = "LICENSE" } @@ -33,7 +33,6 @@ dependencies = [ "py-slvs==1.0.6", "ocp-gordon>=0.2,<0.3", "ezdxf>=1.4.4", - "pymupdf>=1.28.2", "marimo>=0.25.0", "plotly>=7.1.0", ] @@ -45,8 +44,16 @@ dev = [ "mypy>=1.0", "black>=23.0", "isort>=5.0", + "pymupdf>=1.28.2", +] +drawing = [ + "pymupdf>=1.28.2", +] +editor = [ + "starlette>=1.7.0", + "uvicorn>=0.54.0", + "websockets>=16.1.1", ] -editor = [] inspect = [ "matplotlib>=3.7.0", ] @@ -78,13 +85,24 @@ core-metadata-version = "2.4" # Ship harness-neutral source; sca skill compiles it on the user's machine. packages = ["src/simplecadapi"] include = ["src/**/*"] +# The built webui (`cd editor-web && npm run build`) is git-ignored; ship it +# with `sca edit` when it has been built. +artifacts = ["src/simplecadapi/editor/webui/**"] [tool.hatch.build.targets.wheel.force-include] "docs/skill" = "simplecadapi/skill/resources/docs/skill" "skillproj.toml" = "simplecadapi/skill/resources/skillproj.toml" +# The notebooks `sca example u_link` copies out. +"examples/u_link_motor_mount/u_link.py" = "simplecadapi/example/resources/u_link_motor_mount/u_link.py" +"examples/u_link_motor_mount/shell.py" = "simplecadapi/example/resources/u_link_motor_mount/shell.py" +"examples/u_link_motor_mount/u_link_motor_mount.py" = "simplecadapi/example/resources/u_link_motor_mount/u_link_motor_mount.py" +"examples/u_link_motor_mount/dimensions.py" = "simplecadapi/example/resources/u_link_motor_mount/dimensions.py" +"examples/u_link_motor_mount/common.py" = "simplecadapi/example/resources/u_link_motor_mount/common.py" [tool.hatch.build.targets.sdist] core-metadata-version = "2.4" +# The release builds its wheel from this sdist, so the built webui has to be in it. +artifacts = ["src/simplecadapi/editor/webui/**"] include = [ "/README.md", "/LICENSE", @@ -103,10 +121,24 @@ exclude = [ "/examples", ] +# The sdist leaves out /examples, but `sca example u_link` builds its wheel +# from these five notebooks (the wheel is built from the sdist on release). +[tool.hatch.build.targets.sdist.force-include] +"examples/u_link_motor_mount/u_link.py" = "examples/u_link_motor_mount/u_link.py" +"examples/u_link_motor_mount/shell.py" = "examples/u_link_motor_mount/shell.py" +"examples/u_link_motor_mount/u_link_motor_mount.py" = "examples/u_link_motor_mount/u_link_motor_mount.py" +"examples/u_link_motor_mount/dimensions.py" = "examples/u_link_motor_mount/dimensions.py" +"examples/u_link_motor_mount/common.py" = "examples/u_link_motor_mount/common.py" + [dependency-groups] dev = [ "twine>=6.1.0", "pytest", + "pymupdf>=1.28.2", + # the editor tests (test/test_editor.py) need the editor extra's packages + "starlette>=1.7.0", + "uvicorn>=0.54.0", + "websockets>=16.1.1", "jupyterlab>=4.5.5", "ipykernel>=6.29.5", "matplotlib>=3.10.8", diff --git a/src/simplecadapi/__init__.py b/src/simplecadapi/__init__.py index 839f9a33..16769cd4 100644 --- a/src/simplecadapi/__init__.py +++ b/src/simplecadapi/__init__.py @@ -69,6 +69,10 @@ get_current_cs, ) from .errors import SimpleCADError +from .operators.deformation import scale_rsolid, offset_rsolid, transform_rsolid, boolean_rsolidlist +from .operators.section_features import extrude_taper_rsolid, sweep_multisection_rsolid +from .operators.limited_extrusion import extrude_until_rsolid +from .operators.draft import draft_rsolid from .params.expr import ( Const, DimensionTolerance, @@ -433,6 +437,14 @@ def notebook_id() -> str: __all__ = [ + "scale_rsolid", + "boolean_rsolidlist", + "transform_rsolid", + "offset_rsolid", + "extrude_taper_rsolid", + "sweep_multisection_rsolid", + "extrude_until_rsolid", + "draft_rsolid", "exporter", # 核心类 "CoordinateSystem", diff --git a/src/simplecadapi/_internal/mesh.py b/src/simplecadapi/_internal/mesh.py index 04a930f9..23ff899b 100644 --- a/src/simplecadapi/_internal/mesh.py +++ b/src/simplecadapi/_internal/mesh.py @@ -1,8 +1,9 @@ """Internal mesh companion data for SimpleCAD geometry. -This module intentionally stays out of the public API. Users should not need to -extract or manage meshes directly; future structural checks will consume this -cache internally. +This module intentionally stays out of the public API. A solid's mesh is built +on first use by :func:`cached_mesh` and kept on the solid per tolerance; it is +derived data (never pickled, never copied) and nothing builds one while a model +is being made. """ from __future__ import annotations @@ -144,19 +145,6 @@ def build_solid_trimesh( ) -def attach_default_mesh(solid: Any) -> None: - """Attach the default internal mesh cache to a Solid wrapper if possible.""" - - try: - mesh = build_solid_trimesh(solid) - except Exception as exc: - _set_runtime(solid, MESH_ERROR_RUNTIME_KEY, str(exc)) - return - - _set_runtime(solid, MESH_RUNTIME_KEY, mesh) - _set_runtime(solid, MESH_ERROR_RUNTIME_KEY, None) - - def cached_mesh( solid: Any, *, diff --git a/src/simplecadapi/cli.py b/src/simplecadapi/cli.py index e83b5c60..900b6d8a 100644 --- a/src/simplecadapi/cli.py +++ b/src/simplecadapi/cli.py @@ -5,9 +5,13 @@ sca init # machine setup: addon home + shell wiring sca addon add|update|remove|list|use - sca skill targets|install + sca skill targets|where|install sca export [--format ...] sca run [--set NAME=VALUE] [--out ...] + sca ls [DIR] | sca params + sca edit [WORKSPACE] # the web editor (needs the 'editor' extra) + sca example [NAME] [DIR] # copy a bundled example workspace out + sca snapshot [WORKSPACE] # what the editor user has selected / sees Shared contract: group handlers return ``(report, exit_code)`` and are followed by the group's own printer (human-readable for init/addon, JSON @@ -29,6 +33,8 @@ def build_parser() -> argparse.ArgumentParser: from .addon import cli as addon_cli + from .editor import cli as editor_cli + from .example import cli as example_cli from .exporter import cli as exporter_cli from .runtime import cli as runtime_cli from .skill import cli as skill_cli @@ -43,6 +49,8 @@ def build_parser() -> argparse.ArgumentParser: ) subparsers = parser.add_subparsers(dest="group", required=True) addon_cli.configure(subparsers) + editor_cli.configure(subparsers) + example_cli.configure(subparsers) exporter_cli.configure(subparsers) runtime_cli.configure(subparsers) skill_cli.configure(subparsers) diff --git a/src/simplecadapi/core.py b/src/simplecadapi/core.py index 83bbfc49..363f0541 100644 --- a/src/simplecadapi/core.py +++ b/src/simplecadapi/core.py @@ -1441,12 +1441,9 @@ def __init__(self, solid: Any, cache: Optional[_TopologyEntityCache] = None): TopoMixein.__init__(self, level=4, self_shape_ref=self) for face in faces_of(self.wrapped): self.add_child(Face(face, cache=self._topology_cache)) - try: - from ._internal.mesh import attach_default_mesh - - attach_default_mesh(self) - except Exception as mesh_error: - self._set_runtime("mesh.error", str(mesh_error)) + # No mesh is built here: every intermediate solid of a feature chain + # would pay for one nobody reads. ``_internal.mesh.cached_mesh`` + # builds it on first use (a preview, a package) and keeps it. except Exception as e: raise ValueError(f"初始化实体失败: {e}. 请检查输入的实体对象是否有效。") diff --git a/src/simplecadapi/editor/__init__.py b/src/simplecadapi/editor/__init__.py new file mode 100644 index 00000000..86d45958 --- /dev/null +++ b/src/simplecadapi/editor/__init__.py @@ -0,0 +1,7 @@ +"""The SimpleCAD editor: ``sca edit ``. + +A code-first CAD editor over a workspace of SimpleCAD notebooks. It runs +the notebooks in a marimo kernel with the SDK's own cell executor, so a +model gives the same product in the editor as under ``sca run``. Needs the +``editor`` extra (``pip install "simplecadapi[editor]"``). +""" diff --git a/src/simplecadapi/editor/cli.py b/src/simplecadapi/editor/cli.py new file mode 100644 index 00000000..b9baa450 --- /dev/null +++ b/src/simplecadapi/editor/cli.py @@ -0,0 +1,89 @@ +"""The ``sca edit`` command (see :mod:`simplecadapi.cli`). + +:: + + sca edit [WORKSPACE] # open the editor on a directory of notebooks + sca edit path/to/model.py # ... starting at one notebook + sca edit --port 7180 --no-browser + sca snapshot [WORKSPACE] # what the person has selected / is looking at +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +DEFAULT_PORT = 7180 + + +def configure(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser( + "edit", + help="open the web editor on a workspace of SimpleCAD notebooks", + description="Serve the code-first CAD editor for WORKSPACE (default: the " + "current directory): a directory of notebooks with [tool.simplecadapi] " + "headers, or one such notebook. Needs the 'editor' extra.", + ) + parser.add_argument("workspace", type=Path, nargs="?", default=Path.cwd()) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=DEFAULT_PORT) + parser.add_argument("--no-browser", action="store_true", help="do not open a browser tab") + parser.set_defaults(handler=_execute, printer=lambda report: None) + + snapshot = subparsers.add_parser( + "snapshot", + help="print what the editor user has selected and is looking at", + description="Print the editor's snapshot for WORKSPACE (default: the current " + "directory) as JSON: the selection (the ql code is in the notebook's selection " + "cell, saved in the file), the cells' state and errors, the parameter values, and " + "the path of the viewport image (a PNG an agent can look at). The editor keeps " + "it in WORKSPACE/.sca-editor/ while it runs.", + ) + snapshot.add_argument("workspace", type=Path, nargs="?", default=Path.cwd()) + snapshot.set_defaults(handler=_snapshot, printer=_print_snapshot) + + +def _execute(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + try: + from .server import serve_workspace + except ModuleNotFoundError as error: + raise ValueError( + f"the editor needs the 'editor' extra ({error.name} is missing): " + 'pip install "simplecadapi[editor]"' + ) from error + target = args.workspace.expanduser().resolve() + notebook = None + if target.is_file(): + notebook, target = target, target.parent + elif not target.is_dir(): + raise ValueError(f"{args.workspace}: no such file or directory") + code = serve_workspace( + target, + host=args.host, + port=args.port, + open_browser=not args.no_browser, + notebook=notebook, + ) + return {}, code + + +def _snapshot(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + import json + + root = args.workspace.expanduser().resolve() + if root.is_file(): + root = root.parent + path = root / ".sca-editor" / "snapshot.json" + if not path.is_file(): + raise ValueError(f"{root}: no editor snapshot; is `sca edit` running on this workspace?") + return json.loads(path.read_text(encoding="utf-8")), 0 + + +def _print_snapshot(report: dict[str, Any]) -> None: + import json + + print(json.dumps(report, indent=2, ensure_ascii=False)) + + +__all__ = ["configure"] diff --git a/src/simplecadapi/editor/kernel.py b/src/simplecadapi/editor/kernel.py new file mode 100644 index 00000000..1c60f4c0 --- /dev/null +++ b/src/simplecadapi/editor/kernel.py @@ -0,0 +1,671 @@ +"""Host the workspace's marimo kernel as a subprocess and drive it. + +The bridge owns exactly one headless ``marimo edit`` server per workspace. +It creates the kernel session by holding one WebSocket connection (the same +thing a browser tab does) and drives everything else through marimo's HTTP +API: + +- ``POST /api/kernel/execute`` runs driver snippets in the kernel's scratch + cell (deliberately auth-exempt for agents); +- ``marimo._code_mode`` edits, runs and deletes notebook cells; +- the kernel runs the notebook with the SimpleCAD cell executor + (:mod:`simplecadapi.runtime.executor`), exactly as ``sca run`` does; the + bridge asks it for the product with + :func:`simplecadapi.runtime.live.publish_live` once the cells settle. + +The held WebSocket also carries marimo's own messages; ``completed-run`` ends +every batch of cell runs, whoever started it (this bridge, or a person +typing in the embedded marimo editor), and is what the server reacts to. +""" + +from __future__ import annotations + +import json +import os +import re +import socket +import subprocess +import sys +import threading +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Callable + +import websockets + +SESSION_ID = "sca-runtime" +_TRACE = bool(os.environ.get("SCA_EDITOR_TRACE")) + + +class KernelError(RuntimeError): + pass + + +class MarimoKernel: + def __init__( + self, + notebook: Path, + *, + server_port: int, + on_run_complete: Callable[[], None] | None = None, + on_cell_status: Callable[[str, str], None] | None = None, + ) -> None: + self.notebook = notebook.expanduser().resolve() + self.on_run_complete = on_run_complete + self.on_cell_status = on_cell_status + self._interrupted = threading.Event() + self.setup_errors: list[str] = [] + self._ran = threading.Event() # a run of cells completed (see wait_instantiated) + self._own_calls = 0 + self._own_ended = 0.0 + self.workspace = self.notebook.parent + self.marimo_port = _free_port(server_port + 1) + self.marimo_url = f"http://127.0.0.1:{self.marimo_port}" + self._process: subprocess.Popen[bytes] | None = None + self._token: str | None = None + self._ws_thread: threading.Thread | None = None + self._stop = threading.Event() + self._lock = threading.Lock() + + # -- lifecycle --------------------------------------------------------------- + + def _ensure_lazy_config(self) -> None: + """Provision marimo's lazy reactive mode for the workspace. + + The editor's execution semantics (cell rollback, differential + re-runs, stale suffixes) are marimo's lazy mode: + runtime.on_cell_change="lazy". marimo discovers config by walking up + from its cwd (the workspace), so when NO .marimo.toml exists on that + path this writes one at the workspace root — workspaces may live + anywhere on disk. An existing config anywhere up the tree always + wins (the user's choice). + """ + probe = self.workspace + while True: + if (probe / ".marimo.toml").is_file(): + return + parent = probe.parent + if parent == probe: + break + probe = parent + try: + (self.workspace / ".marimo.toml").write_text( + "# Provisioned by sca edit: rollback / differential rerun\n" + "# semantics need marimo's lazy reactive mode.\n" + '[runtime]\non_cell_change = "lazy"\n', + encoding="utf-8", + ) + except OSError as exc: + print(f"[sca edit] could not write lazy-mode config: {exc}", file=sys.stderr) + + def start(self, timeout: float = 60.0) -> None: + """Spawn marimo, wait for readiness, create the kernel session and + instantiate the notebook (runs all cells top-to-bottom).""" + self._ensure_lazy_config() + env = dict(os.environ) + log_path = self.workspace / ".sca-editor" / "marimo-kernel.log" + log_path.parent.mkdir(parents=True, exist_ok=True) + self._log_file = log_path.open("ab") + self._process = subprocess.Popen( + [ + sys.executable, "-m", "marimo", "edit", str(self.notebook), + "--headless", "--no-token", "--no-sandbox", + "--port", str(self.marimo_port), + ], + cwd=str(self.workspace), + env=env, + # A notebook with inline dependencies makes marimo ask whether to + # sandbox it; nobody is there to answer. + stdin=subprocess.DEVNULL, + stdout=self._log_file, + stderr=subprocess.STDOUT, + ) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if self._process.poll() is not None: + raise KernelError(f"marimo exited with code {self._process.returncode}") + try: + with urllib.request.urlopen(f"{self.marimo_url}/api/sessions", timeout=2) as resp: + resp.read() + break + except (urllib.error.URLError, OSError): + time.sleep(0.3) + else: + self.stop() + raise KernelError(f"marimo not ready after {timeout:.0f}s") + + self._token = self._fetch_token() + self._hold_session() + self.instantiate() + + @property + def alive(self) -> bool: + return self._process is not None and self._process.poll() is None + + def stop(self) -> None: + self._stop.set() + if self._process is not None and self._process.poll() is None: + self._process.terminate() + try: + self._process.wait(timeout=5) + except subprocess.TimeoutExpired: + self._process.kill() + if getattr(self, "_log_file", None) is not None: + try: + self._log_file.close() + except OSError: + pass + + # -- session plumbing ---------------------------------------------------------- + + def _fetch_token(self) -> str: + """The skew-protection token ships in the marimo homepage HTML.""" + with urllib.request.urlopen(f"{self.marimo_url}/", timeout=5) as resp: + html = resp.read().decode("utf-8", "replace") + match = re.search(r'"serverToken":\s*"([^"]+)"', html) + if match is None: + raise KernelError("could not read marimo server token from homepage") + return match.group(1) + + def _hold_session(self) -> None: + """Hold one WS connection to marimo — this creates and keeps the + kernel session alive (a headless browser tab, effectively). + + marimo restarts its websocket service whenever the notebook file + changes on disk (code.edit persists) — the connection dies with + 1012/service-restart. The holder must reconnect, or the session + silently dies and every later edit/run becomes a no-op. + """ + + def run() -> None: + import asyncio + + async def main() -> None: + uri = ( + f"ws://127.0.0.1:{self.marimo_port}/ws" + f"?session_id={SESSION_ID}&file_key={self.notebook}" + ) + while not self._stop.is_set(): + try: + async with websockets.connect(uri, max_size=None) as ws: + async for message in ws: + self._on_message(message) + except Exception: + if self._stop.is_set(): + return + # marimo restarted; re-hold the same session id. + await asyncio.sleep(1.0) + + asyncio.run(main()) + + self._ws_thread = threading.Thread(target=run, name="marimo-bridge-ws", daemon=True) + self._ws_thread.start() + time.sleep(1.0) + + def session_exists(self) -> bool: + """Whether marimo still has this bridge's session. + + Unlike a scratch snippet this never touches the kernel, so it can be + asked while a cell runs. + """ + try: + with urllib.request.urlopen(f"{self.marimo_url}/api/sessions", timeout=5) as response: + return SESSION_ID in json.loads(response.read()) + except (urllib.error.URLError, OSError, ValueError): + return False + + def _on_message(self, message: str | bytes) -> None: + try: + data = json.loads(message) + except (TypeError, ValueError): + return + if isinstance(data, dict) and data.get("op") == "cell-op": + # A cell started, finished, was queued...: the live state of its run button. + payload = data.get("data") or {} + cell_id, status = payload.get("cell_id"), payload.get("status") + # (the setup cell and the bridge's scratch snippets are not cells of the notebook) + if cell_id and cell_id != "setup" and not str(cell_id).startswith("__") and isinstance(status, str) and self.on_cell_status is not None: + self.on_cell_status(str(cell_id), status) + return + if not isinstance(data, dict) or data.get("op") != "completed-run": + return + self._ran.set() + # Runs this bridge asked for (scratch snippets, publishing) end in a + # completed-run too; only the cell runs of someone else count. + if self._own_calls or time.monotonic() - self._own_ended < 0.5: + return + if self.on_run_complete is not None: + self.on_run_complete() + + def wait_instantiated(self, timeout: float = 120.0) -> bool: + """Wait until marimo reports the run that :meth:`instantiate` asked for as completed. + + The kernel takes a moment to begin that run, and a probe sent in that + moment (see :meth:`wait_idle`) finds it idle: the cells would be read, + and the product published, before they ran. + """ + return self._ran.wait(timeout) + + def instantiate(self) -> None: + """Run all notebook cells (what the marimo frontend does on load).""" + self._ran.clear() + self._post("/api/kernel/instantiate", {"objectIds": [], "values": []}) + + def list_cells(self) -> list[dict]: + """Notebook cells as [{id, name, code, status, output, console}]. + + Mirrors the marimo reading experience: each cell carries its last + execution status, main output text, and buffered console output so + the studio can render code → output interleaved. Infrastructure + cells (the setup cell) are hidden; every other cell, anonymous ones + included, is one block of the notebook. Cells are addressed by id. + """ + hidden = {"setup"} + result = self.execute( + "import json as _json\n" + "import marimo._code_mode as _cm\n" + "def _text(_o):\n" + " if _o is None:\n" + " return None\n" + " # CellOutput.data is str | list[Error] | dict[str, Any] — render\n" + " # each shape as plain text instead of its repr.\n" + " _d = getattr(_o, 'data', None)\n" + " if isinstance(_d, str):\n" + " _v = _d\n" + " elif isinstance(_d, dict):\n" + " _v = _d.get('text/plain') or str(_d)\n" + " elif isinstance(_d, (list, tuple)):\n" + " _parts = []\n" + " for _item in _d:\n" + " _m = getattr(_item, 'message', None)\n" + " _parts.append(_m if isinstance(_m, str) else str(_item))\n" + " _v = '\\n'.join(_parts)\n" + " else:\n" + " _v = str(_d) if _d is not None else None\n" + " if isinstance(_v, str):\n" + " if _v.lstrip().startswith('['):\n" + " # marimo serialises mimebundle outputs as a JSON array of\n" + " # ':' strings — prefer the text/plain entry.\n" + " try:\n" + " import json as _json\n" + " _arr = _json.loads(_v)\n" + " except Exception:\n" + " _arr = None\n" + " if isinstance(_arr, list):\n" + " _plain = [str(_x)[len('text/plain:'):] for _x in _arr if isinstance(_x, str) and _x.startswith('text/plain:')]\n" + " _v = _plain[0] if _plain else '\\n'.join(str(_x) for _x in _arr)\n" + " if _v.startswith('text/plain:'):\n" + " _v = _v[len('text/plain:'):]\n" + " if _v.lstrip().startswith('<'):\n" + " import html as _html, re as _re\n" + " _v = _html.unescape(_re.sub(r'<[^>]+>', '', _v)).strip()\n" + " return _v\n" + " return str(_v) if _v is not None else None\n" + "def _failures(_c):\n" + " # An error is the cell's output: a list of marimo error objects.\n" + " _d = getattr(getattr(_c, 'output', None), 'data', None)\n" + " _own, _blocked = [], False\n" + " if isinstance(_d, (list, tuple)):\n" + " for _e in _d:\n" + " _kind = str(getattr(_e, 'exception_type', '') or '')\n" + " _msg = str(getattr(_e, 'msg', _e))\n" + " if _kind == 'Ancestor raised':\n" + " _blocked = True\n" + " else:\n" + " _own.append((_kind + ': ' if _kind else '') + _msg)\n" + " return _own, _blocked\n" + "_ctx = _cm.get_context()\n" + "_rows = []\n" + "for _c in _ctx.cells:\n" + " _st = getattr(_c, 'status', None)\n" + " _own, _blocked = _failures(_c)\n" + " _failed = bool(_own) or _blocked\n" + " _rows.append({\n" + " 'id': _c.id, 'name': _c.name, 'code': _c.code,\n" + " 'status': getattr(_st, 'value', None) if _st is not None else None,\n" + " 'errors': ([str(getattr(_e, 'msg', _e))[:300] for _e in (getattr(_c, 'errors', None) or [])][:4] + [_m[:400] for _m in _own])[:4],\n" + " 'blocked': _blocked,\n" + " 'output': None if _failed else ((_text(_c.output) or '')[:400] if getattr(_c, 'output', None) is not None else None),\n" + " 'console': [t for t in ((_text(_o) or '')[-4000:] for _o in (getattr(_c, 'console_outputs', None) or [])[-4:]) if t],\n" + " })\n" + "print(_json.dumps(_rows))\n" + ) + for line in (result.get("log") or "").splitlines(): + line = line.strip() + if line.startswith("["): + try: + cells = json.loads(line) + if isinstance(cells, list): + # What the setup cell raised (a module it imports does not compile) + # blocks every cell: that is the error to show. + self.setup_errors = [ + str(message) + for c in cells + if isinstance(c, dict) and c.get("name") == "setup" + for message in c.get("errors") or [] + ] + return [c for c in cells if isinstance(c, dict) and str(c.get("name", "")) not in hidden] + except json.JSONDecodeError: + continue + return [] + + # -- cell commands ---------------------------------------------------------- + + def run_cells(self, cell_ids: list[str]) -> dict: + """Queue cells to run (lazy mode: their dependents turn stale).""" + return self.execute(_driver("\n".join(f" ctx.run_cell({cell_id!r})" for cell_id in cell_ids))) + + def edit_cell(self, cell_id: str, code: str) -> dict: + """Replace a cell's code (persisted to the file) and run it.""" + return self.execute( + "import json as _json\n" + f"__code = _json.loads({json.dumps(json.dumps(code))})\n" + + _driver(f" ctx.edit_cell({cell_id!r}, code=__code)\n ctx.run_cell({cell_id!r})") + ) + + def save_cell(self, cell_id: str, code: str) -> dict: + """Replace a cell's code (persisted to the file) without running it.""" + return self.execute( + "import json as _json\n" + f"__code = _json.loads({json.dumps(json.dumps(code))})\n" + + _driver(f" ctx.edit_cell({cell_id!r}, code=__code)") + ) + + def put_cell(self, code: str, *, cell_id: str | None, run: bool = True) -> str | None: + """Write *code* into cell *cell_id*, or append a new cell holding it. + + Returns the id of the cell, or ``None`` if the kernel did not answer. + The cell is run unless *run* is false. + """ + run_line = " ctx.run_cell(__id)\n" if run else "" + if cell_id is not None: + body = f" __id = {cell_id!r}\n ctx.edit_cell(__id, code=__code)\n" + run_line + else: + body = ( + " __last = [c.id for c in ctx.cells]\n" + " if __last:\n" + " __id = ctx.create_cell(__code, after=__last[-1])\n" + " else:\n" + " __id = ctx.create_cell(__code)\n" + run_line + ) + result = self.execute( + "import json as _json\n" + f"__code = _json.loads({json.dumps(json.dumps(code))})\n" + + _driver(body) + + "print('SCA-CELL', __id)\n" + ) + for line in (result.get("log") or "").splitlines(): + if line.startswith("SCA-CELL "): + return line[len("SCA-CELL "):].strip() + return None + + def delete_cell(self, cell_id: str) -> dict: + return self.execute(_driver(f" ctx.delete_cell({cell_id!r})")) + + def insert_cell(self, after: str | None) -> dict: + """Insert one empty, named cell after *after* (at the top if None). + + The cell carries a comment placeholder — marimo's create_cell drops + trailing cells when saving an EMPTY-code insertion — and is not + executed. + """ + if after: + create = f" ctx.create_cell('# new cell', after={after!r}, name=__name)\n" + else: + create = ( + " if __names:\n" + " ctx.create_cell('# new cell', before=__names[0], name=__name)\n" + " else:\n" + " ctx.create_cell('# new cell', name=__name)\n" + ) + return self.execute( + _driver( + " __names = [c.name for c in ctx.cells]\n" + " __name = 'new_cell'\n" + " __i = 1\n" + " while __name in __names:\n" + " __i += 1\n" + " __name = f'new_cell_{__i}'\n" + create + ) + ) + + def reload_modules(self, names: list[str]) -> dict: + """Forget the local modules *names* and run every cell again. + + The notebook's setup cell imported them once; a changed file is seen + only by a fresh import, so the modules (and their bytecode, which + Python checks by size and whole-second mtime only) are dropped and + every cell, the setup cell included, runs against the new files. + """ + self.execute( + "import importlib, os, sys\n" + f"__names = {names!r}\n" + "for __n in [m for m in sys.modules if m in __names or any(m.startswith(x + '.') for x in __names)]:\n" + " __cached = getattr(sys.modules.pop(__n), '__cached__', None)\n" + " if isinstance(__cached, str):\n" + " try:\n" + " os.unlink(__cached)\n" + " except OSError:\n" + " pass\n" + "importlib.invalidate_caches()\n" + ) + return self.execute( + _driver(" for __c in ctx.cells:\n ctx.run_cell(__c.id)\n ctx.run_cell('setup')"), + ) + + def stale_cells(self) -> list[str]: + """Ids of the cells that need a run (inputs changed, code edited).""" + result = self.execute( + "import json as _json\n" + "import marimo._code_mode as _cm\n" + "_ids = []\n" + "for _c in _cm.get_context().cells:\n" + " _st = getattr(_c, 'status', None)\n" + " if _c.name != 'setup' and getattr(_st, 'value', _st) == 'stale':\n" + " _ids.append(_c.id)\n" + "print('SCA-STALE', _json.dumps(_ids))\n" + ) + for line in (result.get("log") or "").splitlines(): + if line.startswith("SCA-STALE "): + try: + return [str(item) for item in json.loads(line[len("SCA-STALE "):])] + except json.JSONDecodeError: + break + return [] + + def wait_idle(self, timeout: float = 120.0) -> bool: + """Wait until no cell is queued or running; False on timeout. + + ``run_cell`` only awaits submission; the cells' state is final once + the kernel goes idle. Poll with short scratch executes (never sleep + inside the kernel itself). + """ + probe = ( + "import marimo._code_mode as _cm\n" + "_busy = [c.name for c in _cm.get_context().cells" + " if str(getattr(getattr(c, 'status', ''), 'value', getattr(c, 'status', ''))) in ('queued', 'running')]\n" + "print('SCA-BUSY' if _busy else 'SCA-IDLE')\n" + ) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if "SCA-IDLE" in (self.execute(probe).get("log") or ""): + return True + time.sleep(0.2) + return False + + def interrupt(self) -> None: + """Stop what the kernel is running (the same as marimo's interrupt button). + + Work this bridge has queued behind the running cell (the cells a parameter + change made stale) is dropped too. + """ + self._interrupted.set() + self._post("/api/kernel/interrupt", {}) + + def run_until_settled(self, cell_ids: list[str], *, rounds: int = 8) -> None: + """Run *cell_ids*, then every cell that turned stale, until none is. + + Lazy mode marks a run cell's dependents stale instead of running + them; a parameter change wants the whole chain, so follow up. + """ + self._interrupted.clear() + if cell_ids: + self.run_cells(cell_ids) + self.wait_idle() + for _ in range(rounds): + if self._interrupted.is_set(): + return + stale = self.stale_cells() + if not stale: + return + self.run_cells(stale) + self.wait_idle() + + def publish_preview(self, url: str, step: str = "") -> dict: + """Have the kernel preview its product and POST the bytes to *url*. + + Returns ``{"ok": True, ...}`` or ``{"ok": False, "error"}``; a notebook that + has not produced its product yet is an error value, not an exception. + """ + result = self.execute( + "from simplecadapi.runtime.live import publish_preview as _preview\n" + "try:\n" + f" _p = _preview({url!r}, step={step!r})\n" + " print('SCA-PREVIEWED', _p['nodes'], _p['parts'], _p['bytes'])\n" + "except Exception as _e:\n" + " print('SCA-PREVIEW-ERROR', type(_e).__name__ + ':', str(_e).splitlines()[0] if str(_e) else '')\n", + ) + for line in (result.get("log") or "").splitlines(): + if line.startswith("SCA-PREVIEWED "): + _, nodes, parts, size = line.split(" ") + return {"ok": True, "nodes": int(nodes), "parts": int(parts), "bytes": int(size)} + if line.startswith("SCA-PREVIEW-ERROR "): + return {"ok": False, "error": line[len("SCA-PREVIEW-ERROR "):]} + return {"ok": False, "error": (result.get("log") or "preview failed").strip()[-300:]} + + def entity_info(self, entity_id: str) -> dict | None: + """The properties of one entity of the scene last previewed (``None`` if unknown).""" + result = self.execute( + "import json as _json\n" + "from simplecadapi.runtime.live import preview_entity_info as _info\n" + f"_i = _info({entity_id!r})\n" + "print('SCA-ENTITY', _json.dumps(_i))\n" + ) + for line in (result.get("log") or "").splitlines(): + if line.startswith("SCA-ENTITY "): + try: + return json.loads(line[len("SCA-ENTITY "):]) + except json.JSONDecodeError: + return None + return None + + def publish(self, url: str, step: str = "") -> dict: + """Have the kernel project its product and POST the package to *url*. + + Returns ``{"ok": True, "id", "kind"}`` or ``{"ok": False, "error"}``; + a notebook that has not produced its product yet is an error value, + not an exception. + """ + result = self.execute( + "from simplecadapi.runtime.live import publish_live as _publish\n" + "try:\n" + f" _p = _publish({url!r}, step={step!r})\n" + " print('SCA-PUBLISHED', _p.definition_id, _p.definition_kind)\n" + "except Exception as _e:\n" + " print('SCA-PUBLISH-ERROR', type(_e).__name__ + ':', str(_e).splitlines()[0] if str(_e) else '')\n", + ) + for line in (result.get("log") or "").splitlines(): + if line.startswith("SCA-PUBLISHED "): + _, definition_id, kind = line.split(" ", 2) + return {"ok": True, "id": definition_id, "kind": kind} + if line.startswith("SCA-PUBLISH-ERROR "): + return {"ok": False, "error": line[len("SCA-PUBLISH-ERROR "):]} + return {"ok": False, "error": (result.get("log") or "publishing failed").strip()[-300:]} + + # -- driving -------------------------------------------------------------------- + + def execute(self, code: str, timeout: float | None = None) -> dict: + """Run a scratch snippet in the kernel; returns {success, output, log}. + + The snippet queues behind whatever the kernel is running, and the call + waits for it. There is deliberately no client-side timeout by default: + marimo interrupts the KERNEL when the client of ``/api/kernel/execute`` + disconnects, whatever the kernel is running at that moment, so a call + that gave up after 10 s killed any cell that ran longer (a ``use`` of a + child notebook, a big rebuild). + """ + self._own_calls += 1 + started = time.monotonic() + try: + return self._execute(code, timeout) + finally: + self._own_ended = time.monotonic() + self._own_calls -= 1 + if _TRACE: + first = next((line for line in code.splitlines() if line.strip()), "")[:70] + print(f"[sca edit] {time.strftime('%H:%M:%S')}.{int(time.time() * 1000) % 1000:03d} kernel {self._own_ended - started:6.2f}s {first}", file=sys.stderr, flush=True) + + def _execute(self, code: str, timeout: float | None) -> dict: + body = json.dumps({"code": code}).encode("utf-8") + request = urllib.request.Request( + f"{self.marimo_url}/api/kernel/execute", + data=body, + headers={"Content-Type": "application/json", "Marimo-Session-Id": SESSION_ID}, + method="POST", + ) + result: dict = {"success": None, "output": "", "log": ""} + with urllib.request.urlopen(request, timeout=timeout) as response: + event = "" + for raw in response: + line = raw.decode("utf-8", "replace").rstrip("\r\n") + if line.startswith("event:"): + event = line[len("event:"):].strip() + continue + if not line.startswith("data:"): + continue + try: + payload = json.loads(line[len("data:"):].strip()) + except json.JSONDecodeError: + continue + if event == "stdout" and isinstance(payload.get("data"), str): + result["log"] += payload["data"] + elif event == "stderr" and isinstance(payload.get("data"), str): + result["log"] += payload["data"] + elif event == "done": + result["success"] = bool(payload.get("success", False)) + output = (payload.get("output") or {}).get("data") + if isinstance(output, str): + result["output"] = output + return result + + # -- helpers ---------------------------------------------------------------------- + + def _post(self, path: str, payload: dict, timeout: float = 30.0) -> dict: + body = json.dumps(payload).encode("utf-8") + headers = {"Content-Type": "application/json", "Marimo-Session-Id": SESSION_ID} + if self._token is not None: + headers["Marimo-Server-Token"] = self._token + request = urllib.request.Request( + f"{self.marimo_url}{path}", data=body, headers=headers, method="POST" + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + data = response.read() + return json.loads(data) if data else {} + + +def _driver(inner: str) -> str: + """A scratch snippet running *inner* (indented lines) in a code-mode batch.""" + return "import marimo._code_mode as cm\nasync with cm.get_context() as ctx:\n" + inner + "\n" + + +def _free_port(start: int) -> int: + for port in range(start, start + 32): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + sock.bind(("127.0.0.1", port)) + except OSError: + continue + return port + raise KernelError(f"no free port at or above {start}") diff --git a/src/simplecadapi/editor/selection.py b/src/simplecadapi/editor/selection.py new file mode 100644 index 00000000..08898884 --- /dev/null +++ b/src/simplecadapi/editor/selection.py @@ -0,0 +1,142 @@ +"""The viewport selection as notebook code. + +Picking in the viewport writes a cell: every picked entity becomes a ``ql`` +selector built from its geometry (type, centre, size) and is assigned to +``selected_`` (``selected__`` when several of a kind are +picked), and ``selected_item`` lists them all. The cell is autosaved with +the notebook, so a coding agent reads what the person selected from the file +on disk and builds on it:: + + # ---- selection ---- + selected_edge_1 = scad.ql.edges().where(scad.ql.and_(...)).exactly(1) + selected_edge_2 = scad.ql.edges().where(scad.ql.and_(...)).exactly(1) + selected_item = [selected_edge_1, selected_edge_2] + # next: scad.chamfer_rsolid(solid=body, edges=selected_item[0], ...) + +The selectors say where the geometry is, not which id it had, so they keep +selecting it while the model is edited, as long as it stays put. +""" + +from __future__ import annotations + +from collections import Counter +from collections.abc import Mapping, Sequence +from typing import Any + +MARKER = "# ---- selection ----" +"""First line of the selection cell; how the editor finds it again.""" + +_QL_TYPES = { + "plane": "PLANE", + "cylinder": "CYLINDER", + "cone": "CONE", + "sphere": "SPHERE", + "torus": "TORUS", + "line": "LINE", + "circle": "CIRCLE", + "bspline_surface": "BSPLINE", + "bspline_curve": "BSPLINE", + "bezier_surface": "BEZIER", + "bezier_curve": "BEZIER", +} +_FACTORY = {"face": "faces", "edge": "edges", "solid": "solids", "vertex": "vertices"} +_POSITION_TOLERANCE = 0.1 +_RELATIVE_TOLERANCE = 0.01 + + +def selector_expression(kind: str, properties: Mapping[str, Any], geometry: Mapping[str, Any]) -> str | None: + """A ``scad.ql`` expression selecting the one entity, or ``None``. + + *properties* and *geometry* are the scene sidecar's entity fields. + """ + factory = _FACTORY.get(kind) + if factory is None: + return None + conditions: list[str] = [] + ql_type = _QL_TYPES.get(str(geometry.get("type", ""))) + if ql_type is not None: + conditions.append(f'scad.ql.prop("geom.type", "==", "{ql_type}")') + # A solid has no ql centre; it is told apart by its volume. + centre = None if kind == "solid" else _vector( + properties.get("position") if kind == "vertex" else properties.get("centroid") + ) + if centre is not None: + for axis, value in zip("xyz", centre): + conditions.append(f'scad.ql.prop("geom.center.{axis}", ">=", {_number(value - _POSITION_TOLERANCE)})') + conditions.append(f'scad.ql.prop("geom.center.{axis}", "<=", {_number(value + _POSITION_TOLERANCE)})') + for name, key in (("geom.area", "area"), ("geom.length", "length"), ("geom.volume", "volume")): + size = properties.get(key) + if isinstance(size, (int, float)) and not isinstance(size, bool) and size > 0: + low, high = size * (1 - _RELATIVE_TOLERANCE), size * (1 + _RELATIVE_TOLERANCE) + conditions.append(f'scad.ql.prop("{name}", ">=", {_number(low)})') + conditions.append(f'scad.ql.prop("{name}", "<=", {_number(high)})') + if not conditions: + return None + body = ",\n ".join(conditions) + return f"scad.ql.{factory}().where(scad.ql.and_(\n {body},\n)).exactly(1)" + + +def selection_cell_code(items: Sequence[Mapping[str, Any]]) -> str: + """The source of the selection cell for the picked *items*. + + Each item has ``kind`` (``solid|face|edge|vertex|component``) and, for + the first four, the entity's ``properties`` and ``geometry``; every item + may carry ``definition_id`` and ``node_id`` for the comment above it. + """ + lines = [MARKER] + names: list[str] = [] + totals = Counter(str(item.get("kind", "")) for item in items) + seen: Counter[str] = Counter() + for item in items: + kind = str(item.get("kind", "")) + seen[kind] += 1 + name = f"selected_{kind}" if totals[kind] == 1 else f"selected_{kind}_{seen[kind]}" + where = _where(item) + if kind == "component": + expression = repr(str(item.get("node_id", ""))) + else: + expression = selector_expression(kind, item.get("properties") or {}, item.get("geometry") or {}) + if expression is None: + lines.append(f"# {name}: nothing to select by (no geometry on this entity){where}") + continue + lines.append(f"# {kind}{where}") + lines.append(f"{name} = {expression}") + names.append(name) + lines.append(f"selected_item = [{', '.join(names)}]") + return "\n".join(lines) + + +def variable_names(items: Sequence[Mapping[str, Any]]) -> list[str]: + """The ``selected_*`` variable of each item, in order (as in the code).""" + totals = Counter(str(item.get("kind", "")) for item in items) + seen: Counter[str] = Counter() + names = [] + for item in items: + kind = str(item.get("kind", "")) + seen[kind] += 1 + names.append(f"selected_{kind}" if totals[kind] == 1 else f"selected_{kind}_{seen[kind]}") + return names + + +def _where(item: Mapping[str, Any]) -> str: + parts = [] + if item.get("definition_id"): + parts.append(f"of {item['definition_id']}") + if item.get("node_id"): + parts.append(f"node {item['node_id']}") + return f" ({', '.join(parts)})" if parts else "" + + +def _vector(value: Any) -> tuple[float, float, float] | None: + if ( + isinstance(value, (list, tuple)) + and len(value) == 3 + and all(isinstance(part, (int, float)) and not isinstance(part, bool) for part in value) + ): + return float(value[0]), float(value[1]), float(value[2]) + return None + + +def _number(value: float) -> str: + text = f"{value:.4f}".rstrip("0").rstrip(".") + return "0" if text in ("-0", "") else text diff --git a/src/simplecadapi/editor/server.py b/src/simplecadapi/editor/server.py new file mode 100644 index 00000000..35d48ce1 --- /dev/null +++ b/src/simplecadapi/editor/server.py @@ -0,0 +1,1565 @@ +"""The editor server: webui static files + WS hub + the workspace's marimo kernel. + +This is the ``sca edit `` application. The workspace is a plain +directory of SimpleCAD notebooks (:mod:`simplecadapi.runtime.workspace`). +The server hosts a marimo kernel for one notebook at a time +(:mod:`.kernel`); the kernel runs the notebook with the same executor +``sca run`` uses. When the cells settle — whoever ran them — the server +asks the kernel for the product's package (``publish_live``), keeps the +bytes, and tells the webuis. Commands from the webui are translated into +kernel cell operations; parameter values the webui sets are saved where the +kernel executor applies them (:mod:`simplecadapi.runtime.params`), so +marimo's own dataflow reruns exactly the cells that read them. +""" + +from __future__ import annotations + +import ast +import asyncio +import hashlib +import json +import re +import sys +import threading +import time +import webbrowser +from collections import OrderedDict +from pathlib import Path +from typing import Any + +import uvicorn +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import FileResponse, JSONResponse, Response +from starlette.routing import Route, WebSocketRoute +from starlette.staticfiles import StaticFiles +from starlette.websockets import WebSocketDisconnect + +from ..runtime.cells import cell_keys +from ..runtime.config import NotebookConfig, read_notebook_config +from ..runtime.params import ( + ParamSpec, + commit_field_value, + commit_value, + list_module_parameters, + parameters_in_cell, + read_saved_params, + write_saved_params, +) +from ..runtime.deps import file_digest, input_files +from ..runtime.workspace import features_in_cell, notebook_modules, scan_modules, scan_workspace +from .kernel import MarimoKernel +from .selection import MARKER, selection_cell_code, variable_names +from .templates import create_notebook +from .ws import WsHub + + +def _webui_dir() -> Path: + """The built webui: ``editor-web`` builds into the package directory + (``npm run build``), and the wheel ships it from there.""" + return Path(__file__).resolve().parent / "webui" + + +DIST_DIR = _webui_dir() + + +def pick_notebook(workspace: Path, prefer: Path | None = None) -> Path | None: + """The notebook to open first: the one asked for, else the product. + + Without a request that is the first assembly nothing else uses (the + top of the product), else the first notebook. + """ + if prefer is not None and prefer.is_file(): + return prefer.resolve() + entries = scan_workspace(workspace) + if not entries: + return None + used = {use for entry in entries for use in entry.uses} + tops = [e for e in entries if e.kind == "assembly" and e.path not in used] + chosen = (tops or entries)[0] + return (workspace / chosen.path).resolve() + + +class WorkspaceState: + """Shared server state: hub + kernel + workspace info + latest envelopes. + + A webui that connects (or refreshes) later must still see the current + model, so the latest envelope per type is cached and replayed. + + Parameter bursts collapse: while one rebuild is in flight, newer values + accumulate in ``_pending_params`` and exactly one follow-up rebuild runs + with the merged latest values ("latest wins"). + """ + + # Envelope types worth replaying, in replay order. "preview" is a light + # revision event; the bytes themselves live on /api/package/latest. + _REPLAYED = ( + "module", + "notebook.cells", + "notebook.features", + "params.schema", + "params.values", + "preview", + "selection", + ) + + def __init__(self, workspace: Path, notebook: Path | None = None) -> None: + self.workspace = workspace.expanduser().resolve() + self.hub = WsHub() + self.kernel: MarimoKernel | None = None + self.notebook: Path | None = pick_notebook(self.workspace, notebook) + self.port = 0 + self.lock = threading.Lock() + self.latest: dict[str, dict] = {} + self.latest_package_bytes: bytes | None = None # a full package, made on demand (export) + self.latest_preview_bytes: bytes | None = None + self.preview_revision = 0 + self._package_event = threading.Event() + self.selection: dict = {} + self.selection_items: list[dict] = [] + self.viewport_png: bytes | None = None + self.cells: list[dict] = [] + self.specs: dict[str, ParamSpec] = {} + self._pending_params: dict[str, float | int | bool] = {} + self._rebuild_inflight = False + self._rebuild_cond = threading.Condition() + # One rebuild per interval; later slider batches keep merging. + self._rebuild_min_interval = 0.35 + self._last_rebuild_at = 0.0 + self._watched: dict[Path, int] = {} + self.module_path: Path | None = None + self._pending_modules: dict[tuple[str, str], float | int | bool] = {} + self._module_dirty: set[Path] = set() + self._module_busy = False + self._module_lock = threading.Lock() + self._packages: OrderedDict[str, tuple[str, bytes]] = OrderedDict() + self._publish_signature: str | None = None + self._stopped = False + self._selection_code: str | None = None + self._selection_busy = False + self._selection_lock = threading.Lock() + self._settle_timer: threading.Timer | None = None + self._settle_lock = threading.Lock() + self._last_publish_error = "" + self._generation = 0 + self._switch_lock = threading.Lock() + self._booting: MarimoKernel | None = None + + # -- notebook / kernel lifecycle --------------------------------------------- + + @property + def config(self) -> NotebookConfig | None: + return read_notebook_config(self.notebook) if self.notebook else None + + def open_notebook(self, rel_path: str, generation: int | None = None) -> None: + """Switch the active notebook: restart the kernel on it. + + Runs synchronously (the caller wraps it in a thread): marimo boot + takes seconds. If nothing the notebook depends on changed since its + model was last published, that model and the notebook's cells are shown + at once, while the kernel boots. + """ + target = self.resolve_notebook(rel_path) + if generation is None: + generation = self.reserve_switch() + self.announce_opening(target) + if not self._begin_switch(target, generation): + return # a later open was asked for meanwhile + restored = self._restore(target, generation) + self.start_kernel(target, restored=restored, generation=generation) + + def resolve_notebook(self, rel_path: str) -> Path: + """The file of the workspace notebook *rel_path*; ``ValueError`` if it is none.""" + target = (self.workspace / rel_path).resolve() + if not target.is_file() or self.workspace not in target.parents: + raise ValueError(f"notebook outside workspace: {rel_path}") + if read_notebook_config(target) is None: + raise ValueError(f"not a SimpleCAD notebook (no [tool.simplecadapi]): {rel_path}") + return target + + def announce_opening(self, target: Path) -> None: + """Tell the webuis a notebook is being opened (they show it at once).""" + self.hub.broadcast( + {"type": "opening", "notebook": target.name, "path": target.relative_to(self.workspace).as_posix()} + ) + + def reserve_switch(self) -> int: + """Take the next place in the order of opens. + + Taken where a request arrives, so that a later click wins however the + threads that carry the opens out happen to be scheduled. + """ + with self._switch_lock: + self._generation += 1 + return self._generation + + def _begin_switch(self, target: Path, generation: int) -> bool: + """Make *target* the notebook being opened, superseding any open in flight. + + The list of notebooks shows it as the current one from now on, not + once its kernel is up; whatever an older open is still doing (booting a + kernel, publishing) is cancelled: it notices it was superseded. + """ + with self._switch_lock: + if generation != self._generation: + return False + self.notebook = target + # Let the webuis drop the old model now; commands have no kernel until the new one is up. + self.latest.clear() + self.latest_preview_bytes = None + self._drop_kernel() + return True + + def _superseded(self, generation: int) -> bool: + return generation != self._generation + + def _drop_kernel(self) -> None: + with self.lock: + old, self.kernel = self.kernel, None + booting, self._booting = self._booting, None + for kernel in (old, booting): + if kernel is not None: + kernel.stop() + + def start_kernel(self, notebook: Path, *, restored: bool = False, generation: int | None = None) -> None: + """Boot a marimo kernel for *notebook*, adopt it, and publish its product. + + Shared by server boot, open_notebook, and the auto-heal watch. With + *restored* the notebook's last model and cells are already on screen. + An open that is superseded while it boots (the person opened another + notebook meanwhile) ends quietly: its kernel is stopped, nothing is shown. + """ + if generation is None: + generation = self.reserve_switch() + if not self._begin_switch(notebook, generation): + return + restored = self._restore(notebook, generation) + kernel = MarimoKernel( + notebook, + server_port=self.port, + on_run_complete=lambda: self._superseded(generation) or self.schedule_settle(), + on_cell_status=lambda cell_id, status: self._superseded(generation) or self.cell_status(cell_id, status), + ) + with self.lock: + if self._superseded(generation): + return + self._booting = kernel + try: + kernel.start() + with self.lock: + if self._superseded(generation): + kernel.stop() + return + self._booting = None + self.kernel = kernel + # Envelopes from the previous model must not replay. + self.selection = {} + self.selection_items = [] + self.viewport_png = None + self._last_publish_error = "" + if not restored: + self.latest.clear() + self.latest_preview_bytes = None + self.cells = [] + self.specs = {} + self._watched = self._watched_files()[0] + self.hub.broadcast(self.hello()) + kernel.wait_instantiated() + kernel.wait_idle() + if self._superseded(generation): + return + self.settle() + except Exception: + kernel.stop() + if self._superseded(generation): + return # its kernel was stopped under it; not an error + raise + threading.Thread( + target=self._watch_files, args=(kernel,), name="file-watch", daemon=True + ).start() + + # -- remembering what was published ----------------------------------------------- + + def _signature(self, notebook: Path) -> str: + """A digest of everything the notebook's model depends on. + + The notebook, the local modules it imports, the notebooks it runs and what + they depend on, the data files they list, the values an editor saved, and the + software versions: if it is the same as when a model was published, that + model is the model. + """ + import marimo + + from ..build.keys import generator_profile + + files, _own, _inputs = self._dependency_files(notebook) + files.add(notebook) + config = read_notebook_config(notebook) + payload = { + "files": {path.as_posix(): file_digest(path) for path in sorted(files)}, + "saved": read_saved_params(config) if config is not None else {}, + "generator": dict(generator_profile()), + "marimo": marimo.__version__, + } + return hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest() + + def _model_file(self, notebook: Path) -> Path: + name = notebook.relative_to(self.workspace).as_posix().replace("/", "__") + return self.snapshot_dir / "models" / f"{name}.scapreview" + + def remember_model(self, notebook: Path, signature: str, data: bytes) -> None: + """Keep *data* (a preview) as the model of *notebook* for *signature*, in memory and on disk.""" + key = notebook.relative_to(self.workspace).as_posix() + self._packages[key] = (signature, data) + self._packages.move_to_end(key) + while len(self._packages) > 6: + self._packages.popitem(last=False) + try: + target = self._model_file(notebook) + target.parent.mkdir(parents=True, exist_ok=True) + target.with_suffix(".sig").write_text(signature, encoding="utf-8") + temporary = target.with_suffix(".tmp") + temporary.write_bytes(data) + temporary.replace(target) + except OSError as error: + print(f"[sca edit] could not keep the model: {error}", file=sys.stderr) + + def _recall_model(self, notebook: Path, signature: str) -> bytes | None: + key = notebook.relative_to(self.workspace).as_posix() + known = self._packages.get(key) + if known is not None and known[0] == signature: + return known[1] + target = self._model_file(notebook) + try: + if target.with_suffix(".sig").read_text(encoding="utf-8") == signature: + return target.read_bytes() + except OSError: + pass + return None + + def _restore(self, notebook: Path, generation: int | None = None) -> bool: + """Show *notebook* from its last published model, if nothing it depends on changed. + + Its cells appear too, read from the file (not editable until the kernel is + up); the kernel's own cells replace them, and its model replaces this one + only if it differs. + """ + try: + data = self._recall_model(notebook, self._signature(notebook)) + except (OSError, ValueError): + data = None + if data is None or (generation is not None and self._superseded(generation)): + return False + self.notebook = notebook + self._publish_cells(_static_cells(notebook)) + self.latest_preview_bytes = data + self.preview_revision += 1 + self.emit( + { + "type": "preview", + "revision": self.preview_revision, + "step": notebook.stem, + "name": notebook.stem, + "size": len(data), + "cached": True, + } + ) + self.hub.broadcast({"type": "status", "info": "restored from the last run; the kernel is starting…"}) + return True + + def watch_kernel(self) -> None: + """Auto-heal: the marimo python session can die on crashes or + external file edits while the marimo SERVER process stays alive + (`kernel.alive` is blind to that). Ask marimo whether it still has our + session; two consecutive misses mean it is gone — restart. + """ + failures = 0 + while True: + time.sleep(5) + kernel = self.kernel + # Between kernels (a notebook is being opened) there is nothing to watch. + if kernel is None or kernel._stop.is_set(): + failures = 0 + continue + if not kernel.alive: + self.hub.broadcast({"type": "status", "error": "marimo server exited — restarting kernel"}) + else: + # Not a scratch snippet: one queued behind a long cell and given + # up on interrupts the kernel (see MarimoKernel.execute). + failures = 0 if kernel.session_exists() else failures + 1 + if failures < 2: + continue + self.hub.broadcast({"type": "status", "error": "kernel session lost — restarting"}) + failures = 0 + try: + assert self.notebook is not None + self.start_kernel(self.notebook) + self.hub.broadcast({"type": "status", "recovered": self.notebook.name}) + except Exception as exc: + self.hub.broadcast({"type": "status", "error": f"kernel restart failed: {exc}"}) + + # -- child notebooks --------------------------------------------------------- + + def _watched_files(self) -> tuple[dict[Path, int], set[Path], set[Path]]: + """The files whose edit changes the open notebook, with their mtimes. + + Returns ``(mtimes, own_modules, own_inputs)``. *own_modules* are the + local modules the notebook itself imports: the kernel has them loaded, + so a change needs a reload and a rerun of every cell. *own_inputs* are + the project files its header lists (``inputs``, data the cells read): + nothing to reload, but any cell may read them, so every cell reruns. + The rest are the notebooks it runs with ``use`` (and the modules and data + those use) and the module open in the module view: ``use`` runs a child + headless, outside the kernel's dataflow, so marimo never sees an edit to + it; the editor does and reruns the cells that call ``use`` (the children's + own cell cache, keyed by their files, decides what is rebuilt). + """ + files: set[Path] = set() + own: set[Path] = set() + inputs: set[Path] = set() + if self.notebook is not None: + files, own, inputs = self._dependency_files(self.notebook) + if self.module_path is not None: + files.add(self.module_path) + mtimes: dict[Path, int] = {} + for path in files: + try: + mtimes[path] = path.stat().st_mtime_ns + except OSError: + mtimes[path] = 0 + return mtimes, own, inputs + + def _dependency_files(self, notebook: Path) -> tuple[set[Path], set[Path], set[Path]]: + """What *notebook* depends on: ``(files, own_modules, own_inputs)``.""" + entries = {entry.path: entry for entry in scan_workspace(self.workspace)} + start = notebook.relative_to(self.workspace).as_posix() + own = set(notebook_modules(notebook).values()) + config = read_notebook_config(notebook) + inputs = set(input_files(config)) if config is not None else set() + files = own | inputs + seen: set[str] = set() + pending = list(entries[start].uses) if start in entries else [] + while pending: + path = pending.pop() + if path in seen: + continue + seen.add(path) + if path in entries: + pending.extend(entries[path].uses) + files.update(self.workspace / module for module in entries[path].modules) + child = read_notebook_config(self.workspace / path) + if child is not None: + files |= set(input_files(child)) + files |= {self.workspace / path for path in seen} + return files, own, inputs + + def _watch_files(self, kernel: MarimoKernel) -> None: + while not kernel._stop.is_set(): + time.sleep(1.0) + if self.kernel is not kernel: + return + try: + current, _own, _inputs = self._watched_files() + changed = {path for path, mtime in current.items() if self._watched.get(path) != mtime} + changed |= {path for path in self._watched if path not in current} + self._watched = current + if changed: + self._files_changed(changed) + except Exception as exc: + self.hub.broadcast({"type": "status", "error": f"refresh failed: {str(exc)[-200:]}"}) + + def _files_changed(self, paths: set[Path], *, announce: bool = True) -> None: + """Files the notebook depends on changed: reload / rerun what reads them.""" + if announce and self.module_path in paths: + self.emit_module("external") + with self._module_lock: + self._module_dirty |= paths + if self._module_busy: + return + self._module_busy = True + threading.Thread(target=self._propagate, name="propagate", daemon=True).start() + + def _propagate(self) -> None: + """Serialized, coalescing: one reload covers every change that arrived meanwhile.""" + try: + while True: + with self._module_lock: + paths, self._module_dirty = self._module_dirty, set() + if not paths: + self._module_busy = False + return + kernel = self.kernel + if kernel is None: + continue + own_names = self._own_module_names() + _mtimes, _own, own_inputs = self._watched_files() + if any(file in paths for file in own_names.values()): + shown = ", ".join(Path(path).name for path in sorted(paths)) + self.hub.broadcast({"type": "status", "working": f"reloading {shown}…"}) + kernel.reload_modules(list(own_names)) + kernel.wait_idle(300) + elif any(file in paths for file in own_inputs): + shown = ", ".join(Path(path).name for path in sorted(paths & own_inputs)) + self.hub.broadcast({"type": "status", "working": f"{shown} changed: rerunning…"}) + kernel.run_until_settled([cell["id"] for cell in self.cells]) + else: + uses = [cell["id"] for cell in self.cells if "use(" in cell["code"]] + if not uses: + continue + kernel.run_until_settled(uses) + self.schedule_settle() + except Exception as exc: + with self._module_lock: + self._module_busy = False + self.hub.broadcast({"type": "status", "error": f"reload failed: {str(exc)[-300:]}"}) + + def _own_module_names(self) -> dict[str, Path]: + return notebook_modules(self.notebook) if self.notebook else {} + + # -- the module view ----------------------------------------------------------- + + def modules(self) -> list[dict]: + return [module.to_dict() for module in scan_modules(self.workspace)] + + def emit_module(self, origin: str) -> None: + """Tell the webuis about the module open in the module view.""" + path = self.module_path + if path is None or not path.is_file(): + self.emit({"type": "module", "path": None}) + return + try: + code = path.read_text(encoding="utf-8") + except OSError: + return + relative = path.relative_to(self.workspace).as_posix() + used_by = next((m["used_by"] for m in self.modules() if m["path"] == relative), []) + params = [] + for spec in list_module_parameters(path): + row = spec.to_dict() + row["id"] = spec.name + row["value"] = spec.default + row["modified"] = False + params.append(row) + self.emit( + { + "type": "module", + "origin": origin, + "path": relative, + "name": path.name, + "code": code, + "used_by": used_by, + "params": params, + } + ) + + def open_module(self, rel_path: str) -> None: + target = (self.workspace / rel_path).resolve() + if self.workspace not in target.parents or not target.is_file() or target.suffix != ".py": + raise ValueError(f"not a module of this workspace: {rel_path}") + self.module_path = target + self._watched = self._watched_files()[0] + self.emit_module("open") + + def save_module(self, rel_path: str, code: str) -> None: + """Autosave from the module view; the dependents follow.""" + target = self._module_file(rel_path) + if target.read_text(encoding="utf-8") == code: + return + target.write_text(code, encoding="utf-8") + self._after_module_write(target, "save") + + def set_module_params(self, rel_path: str, values: dict[str, float | int | bool]) -> None: + """Write parameter values into the dataclass defaults of the module.""" + target = self._module_file(rel_path) + code = target.read_text(encoding="utf-8") + for name, value in values.items(): + class_name, dot, field = name.partition(".") + changed = ( + commit_field_value(code, class_name, field, value) if dot else commit_value(code, name, value) + ) + if changed is None: + self.hub.broadcast({"type": "status", "error": f"cannot write {name} into {target.name}"}) + continue + code = changed + if code != target.read_text(encoding="utf-8"): + target.write_text(code, encoding="utf-8") + self._after_module_write(target, "param") + + def _module_file(self, rel_path: str) -> Path: + target = (self.workspace / rel_path).resolve() + if self.workspace not in target.parents or not target.is_file() or target.suffix != ".py": + raise ValueError(f"not a module of this workspace: {rel_path}") + return target + + def _after_module_write(self, target: Path, origin: str) -> None: + # Our own write: the watcher must not fire a second reload for it. + try: + self._watched[target] = target.stat().st_mtime_ns + except OSError: + pass + self.emit_module(origin) + self._files_changed({target}, announce=False) + + def cell_status(self, cell_id: str, status: str) -> None: + """A cell's state changed in the kernel: tell the webuis (its run button shows it).""" + title = None + for cell in self.cells: + if cell["id"] == cell_id: + cell["status"] = status + title = cell.get("title") + self.hub.broadcast({"type": "cell.status", "id": cell_id, "status": status, "title": title}) + + def remember(self, envelope: dict) -> None: + if envelope.get("type") in self._REPLAYED: + self.latest[envelope["type"]] = envelope + + def emit(self, envelope: dict) -> None: + self.remember(envelope) + self.hub.broadcast(envelope) + + def replay(self) -> list[dict]: + return [self.latest[key] for key in self._REPLAYED if key in self.latest] + + def hello(self) -> dict: + return { + "type": "hello", + "workspace": str(self.workspace), + "notebook": self.notebook.name if self.notebook else None, + "path": self.notebook.relative_to(self.workspace).as_posix() if self.notebook else None, + "kernel": self.kernel is not None and self.kernel.alive, + "marimo_url": self.kernel.marimo_url if self.kernel else None, + } + + # -- settling: the cells ran; publish what they made ----------------------- + + def schedule_settle(self) -> None: + """A batch of cell runs ended: settle once the activity calms down.""" + with self._settle_lock: + if self._settle_timer is not None: + self._settle_timer.cancel() + self._settle_timer = threading.Timer(0.4, self._settle_in_thread) + self._settle_timer.daemon = True + self._settle_timer.start() + + def _settle_in_thread(self) -> None: + try: + self.settle() + except Exception as exc: # never let a timer thread die silently + self.hub.broadcast({"type": "status", "error": str(exc)[-300:]}) + + def settle(self) -> None: + """Push the cells, features and parameters, then the product package.""" + kernel = self.kernel + if kernel is None: + return + with self._settle_lock: + self.push_cells() + # The signature of what is published is taken before it is built. + try: + self._publish_signature = self._signature(self.notebook) if self.notebook else None + except (OSError, ValueError): + self._publish_signature = None + result = kernel.publish_preview(self.preview_url, step=self.notebook.stem if self.notebook else "") + if result.get("ok"): + self._last_publish_error = "" + self._stopped = False + return + message = str(result.get("error", "")) + if kernel.setup_errors: + # No product because the notebook's setup failed (a module it imports + # does not compile): that is the cause, not the missing product. + message = f"setup failed — {kernel.setup_errors[0]}" + if self._stopped: + # The person stopped the run: its cells have no product, and that is no fault. + self._stopped = False + self._last_publish_error = "" + self.hub.broadcast({"type": "status", "info": "stopped"}) + return + # A notebook mid-edit has no product yet; say so once, not per run. + if message != self._last_publish_error: + self._last_publish_error = message + self.hub.broadcast({"type": "status", "error": message[-300:]}) + + @property + def preview_url(self) -> str: + return f"http://127.0.0.1:{self.port}/api/runtime/preview" + + @property + def package_url(self) -> str: + return f"http://127.0.0.1:{self.port}/api/runtime/package" + + def push_cells(self) -> None: + """Broadcast the cells, their feature headers and the parameters.""" + kernel = self.kernel + if kernel is None: + return + self._publish_cells(kernel.list_cells()) + + def _publish_cells(self, cells: list[dict]) -> None: + keys = cell_keys(cell["code"] for cell in cells) + features: list[dict] = [] + specs: list[ParamSpec] = [] + for cell, key in zip(cells, keys): + cell["key"] = key + found = features_in_cell(cell["code"], key) + cell["feature"] = found[0].slug if found else None + cell["title"] = cell_title(cell["code"], cell["feature"]) + # code mode would turn an @app.function into a plain cell + cell["readonly"] = cell.get("readonly", False) or _is_function_cell(cell["code"]) + features.extend( + {"cell": cell["id"], "label": feature.slug, "kind": feature.role or "feature"} + for feature in found + ) + specs.extend( + ParamSpec(**{**spec.__dict__, "cell": cell["id"]}) + for spec in parameters_in_cell(cell["code"], key) + ) + try: + text = self.notebook.read_text(encoding="utf-8") if self.notebook else "" + except OSError: + text = "" + for number, (cell, lines) in enumerate(zip(cells, file_line_ranges(text, [c["code"] for c in cells])), start=1): + where = f"{self.notebook}:L{lines[0]}-L{lines[1]}" if lines and self.notebook else str(self.notebook) + cell["lines"] = list(lines) if lines else None + cell["ref"] = f"{where} (cell {number:02d} `{cell['title']}`)" + self.cells = cells + self.specs = {spec.name: spec for spec in specs} + self.emit({"type": "notebook.cells", "cells": cells}) + self.emit({"type": "notebook.features", "features": features}) + self.emit_params() + self.write_snapshot() + + def emit_params(self) -> None: + config = self.config + saved = read_saved_params(config) if config is not None else {} + variables = [] + for spec in self.specs.values(): + row = spec.to_dict() + row["id"] = spec.name + row["value"] = saved.get(spec.name, spec.default) + row["modified"] = spec.name in saved and saved[spec.name] != spec.default + variables.append(row) + self.emit({"type": "params.schema", "vars": variables}) + self.emit({"type": "params.values", "values": {row["id"]: row["value"] for row in variables}}) + + # -- webui command -> kernel operation --------------------------------------- + + def dispatch_command(self, message: dict) -> dict: + """Translate one ui command into kernel operations. Returns an ack.""" + kind = message.get("type") + if kind == "module.open": + try: + self.open_module(str(message.get("path", ""))) + except ValueError as error: + return {"type": "error", "message": str(error)} + return {"type": "ack", "command": kind} + if kind == "module.close": + self.module_path = None + self.emit_module("close") + return {"type": "ack", "command": kind} + if kind == "module.save": + self._in_thread(lambda: self.save_module(str(message.get("path", "")), str(message.get("code", "")))) + return {"type": "ack", "command": kind} + if kind == "module.param": + path = str(message.get("path", "")) + values = {str(k): v for k, v in dict(message.get("values", {})).items() if isinstance(v, (int, float, bool))} + self._in_thread(lambda: self.set_module_params(path, values)) + return {"type": "ack", "command": kind} + kernel = self.kernel + if kernel is None: + return {"type": "error", "message": "no kernel"} + if kind == "entity.info": + entity_id = str(message.get("entity_id", "")) + node_id = message.get("node_id") + + def lookup() -> None: + info = kernel.entity_info(entity_id) + self.hub.broadcast( + {"type": "entity.info", "entity_id": entity_id, "node_id": node_id, "entity": info} + ) + + self._in_thread(lookup) + return {"type": "ack", "command": kind} + if kind == "interrupt.request": + self._stopped = True + self._in_thread(kernel.interrupt) + return {"type": "ack", "command": kind} + if kind == "param.set": + self._enqueue_params(kernel, dict(message.get("values", {}))) + return {"type": "ack", "command": kind} + if kind == "export.request": + self._export() + return {"type": "ack", "command": kind} + if kind in ("param.commit", "param.revert"): + names = [str(name) for name in message.get("names", [])] or list(self.specs) + self._in_thread(lambda: self._commit(kernel, names, revert=kind == "param.revert")) + return {"type": "ack", "command": kind} + + if kind == "selection.set": + items = [item for item in message.get("items", []) if isinstance(item, dict)] + self._in_thread(lambda: self._set_selection(kernel, items)) + return {"type": "ack", "command": kind} + + cell = str(message.get("cell") or "") + if kind == "cell.save": + # Autosave: write the edit to the notebook file, do not run it. + code = _module_code(str(message.get("code", ""))) + current = next((c["code"] for c in self.cells if c["id"] == cell), None) + if current is None or code.strip() == _module_code(current).strip(): + return {"type": "ack", "command": kind} + if _is_function_cell(current): + return { + "type": "error", + "message": "this cell is an @app.function; edit it in the file or in the marimo editor", + } + + def save() -> None: + kernel.save_cell(cell, code) + self.push_cells() + + self._in_thread(save) + return {"type": "ack", "command": kind} + if kind == "rerun.request": + # Header rerun (no cell) refreshes the whole notebook; a per-cell + # rerun runs that cell and leaves its dependents stale. + ids = [cell] if cell else [c["id"] for c in self.cells] + if message.get("above"): + # Every cell above this one, in notebook order (not itself). + order = [c["id"] for c in self.cells] + ids = order[: order.index(cell)] if cell in order else [] + if not ids: + return {"type": "ack", "command": kind} + action = lambda: kernel.run_cells(ids) # noqa: E731 + elif kind == "insert.request": + after = message.get("after") + action = lambda: kernel.insert_cell(str(after) if after else None) # noqa: E731 + elif kind == "rollback.request": + # Delete the cell (persisted to the file); marimo marks what read + # its variables stale. + action = lambda: kernel.delete_cell(cell) # noqa: E731 + elif kind == "code.edit": + code = _module_code(str(message.get("code", ""))) + current = next((c["code"] for c in self.cells if c["id"] == cell), None) + if current is not None and code.strip() != _module_code(current).strip() and _is_function_cell(current): + # marimo's code mode would turn an @app.function into a plain + # cell; edit those in the file or in marimo's own editor. + return { + "type": "error", + "message": "this cell is an @app.function; edit it in the file or in the marimo editor", + } + if current is not None and code.strip() == _module_code(current).strip(): + # Nothing to change: run the cell, never rewrite the notebook file. + action = lambda: kernel.run_cells([cell]) # noqa: E731 + else: + action = lambda: kernel.edit_cell(cell, code) # noqa: E731 + else: + return {"type": "error", "message": f"unknown command: {kind}"} + + def run() -> None: + result = action() + if result.get("success") is False: + self.hub.broadcast( + {"type": "status", "error": (result.get("log") or "kernel command failed")[-400:]} + ) + kernel.wait_idle() + # The kernel reports a completed run for these too, but settle + # even when it reported failure: the edit may have applied and + # the error state is what the notebook UI must show. + self.schedule_settle() + + self._in_thread(run) + return {"type": "ack", "command": kind} + + def _in_thread(self, work: Any) -> None: + def run() -> None: + try: + work() + except Exception as exc: # surface, never crash the WS handler + self.hub.broadcast({"type": "status", "error": str(exc)[-400:]}) + + threading.Thread(target=run, name="kernel-command", daemon=True).start() + + # -- parameters -------------------------------------------------------------- + + def _clean_params(self, values: dict) -> dict[str, float | int | bool]: + """Keep the values that name a parameter, clamped and typed to it.""" + clean: dict[str, float | int | bool] = {} + for name, raw in values.items(): + spec = self.specs.get(str(name)) + if spec is None or isinstance(raw, str): + continue + if spec.kind == "bool": + clean[spec.name] = bool(raw) + continue + if isinstance(raw, bool): + continue + number = float(raw) + if spec.min is not None: + number = max(spec.min, number) + if spec.max is not None: + number = min(spec.max, number) + clean[spec.name] = int(round(number)) if spec.kind == "int" else number + return clean + + def _enqueue_params(self, kernel: MarimoKernel, values: dict) -> None: + """Coalesce slider bursts: at most one rebuild in flight; newer values + always win; one follow-up rebuild per settled gesture.""" + clean = self._clean_params(values) + if not clean: + return + with self._rebuild_cond: + self._pending_params.update(clean) + threading.Thread(target=self._rebuild_loop, args=(kernel,), name="param-rebuild", daemon=True).start() + + def _rebuild_loop(self, kernel: MarimoKernel) -> None: + with self._rebuild_cond: + if self._rebuild_inflight: + return # the running loop will pick up our pending values + self._rebuild_inflight = True + try: + while True: + with self._rebuild_cond: + if not self._pending_params: + return + partial = dict(self._pending_params) + self._pending_params.clear() + since = time.monotonic() - self._last_rebuild_at + if since < self._rebuild_min_interval: + time.sleep(self._rebuild_min_interval - since) + self._last_rebuild_at = time.monotonic() + try: + self._apply_params(kernel, partial) + except Exception as exc: + self.hub.broadcast({"type": "status", "error": str(exc)[-400:]}) + finally: + with self._rebuild_cond: + self._rebuild_inflight = False + + def _apply_params(self, kernel: MarimoKernel, values: dict[str, float | int | bool]) -> None: + """Save the values, then run the cells that declare them. + + The kernel executor puts the saved values in place as those cells + finish; marimo's dataflow marks what reads them stale, and + ``run_until_settled`` runs exactly that. + """ + config = self.config + if config is None: + raise RuntimeError("no SimpleCAD notebook is open") + saved = read_saved_params(config) + cells: list[str] = [] + for name, value in values.items(): + spec = self.specs[name] + if value == spec.default: + saved.pop(name, None) # back at the source value + else: + saved[name] = value + if spec.cell not in cells: + cells.append(spec.cell) + write_saved_params(config, saved) + self.emit_params() + kernel.run_until_settled(cells) + self.schedule_settle() + + def _commit(self, kernel: MarimoKernel, names: list[str], *, revert: bool) -> None: + """Write saved values into the notebook source, or drop them.""" + config = self.config + if config is None: + return + saved = read_saved_params(config) + names = [name for name in names if name in saved] + if not names: + return + if not revert: + codes = {cell["id"]: cell["code"] for cell in self.cells} + changed: dict[str, str] = {} + for name in names: + spec = self.specs.get(name) + if spec is None: + continue + code = commit_value(changed.get(spec.cell, codes[spec.cell]), name, saved[name]) + if code is None: + self.hub.broadcast({"type": "status", "error": f"cannot write {name} into the source"}) + return + changed[spec.cell] = code + for cell_id, code in changed.items(): + kernel.edit_cell(cell_id, code) + kernel.wait_idle() + for name in names: + saved.pop(name, None) + write_saved_params(config, saved) + if revert: + kernel.run_until_settled([self.specs[name].cell for name in names if name in self.specs]) + self.schedule_settle() + + # -- selection and the agent snapshot ---------------------------------------- + + def _set_selection(self, kernel: MarimoKernel, items: list[dict]) -> None: + """Write the viewport selection into the notebook's selection cell. + + The webuis are told at once, from here: the kernel runs scratch code + one piece at a time, so a write that queues behind a long run (a + publish of a big assembly takes seconds) must not hold the picture + back. The write itself is coalesced: while one is in flight only the + latest selection waits, and it is the one written. + """ + if kernel is not self.kernel: + return # the notebook was switched under this pick + keep = ("kind", "entity_id", "node_id", "definition_id", "properties", "geometry") + self.selection_items = [{key: item.get(key) for key in keep if key in item} for item in items] + cell = next((c for c in self.cells if c["code"].lstrip().startswith(MARKER)), None) + if cell is None and not items: + self.write_snapshot() + return + code = selection_cell_code(self.selection_items) + if cell is not None and code.strip() == cell["code"].strip(): + self.write_snapshot() + return + if cell is not None: + cell["code"] = code + else: + self.cells.append( + { + "id": PENDING_SELECTION_ID, + "name": "_", + "code": code, + "status": "idle", + "errors": [], + "output": None, + "console": [], + "key": "", + "feature": None, + "title": "selection", + "readonly": False, + "lines": None, + "ref": f"{self.notebook} (cell {len(self.cells) + 1:02d} `selection`)", + } + ) + self.emit({"type": "notebook.cells", "cells": self.cells}) + self.emit({"type": "selection", "items": self._selection_summary(), "variables": variable_names(self.selection_items)}) + self.write_snapshot() + with self._selection_lock: + self._selection_code = code + if self._selection_busy: + return + self._selection_busy = True + self._write_selection(kernel) + + def _write_selection(self, kernel: MarimoKernel) -> None: + """Write the newest selection code into the kernel's cell; repeat while a newer one arrived.""" + try: + while True: + with self._selection_lock: + code, self._selection_code = self._selection_code, None + if code is None or kernel is not self.kernel: + # nothing (more) to write, or the notebook was switched meanwhile: + # a stale write must not land in the new notebook's cells + self._selection_code = None + self._selection_busy = False + return + cell = next( + (c for c in self.cells if c["code"].lstrip().startswith(MARKER) and c["id"] != PENDING_SELECTION_ID), + None, + ) + kernel.put_cell(code, cell_id=cell["id"] if cell else None) + kernel.wait_idle(10) + if kernel is not self.kernel: + continue + self.push_cells() + except Exception: + with self._selection_lock: + self._selection_busy = False + raise + + def _selection_summary(self) -> list[dict]: + names = variable_names(self.selection_items) + return [ + { + "variable": name, + "kind": item.get("kind"), + "entity_id": item.get("entity_id"), + "node_id": item.get("node_id"), + "definition_id": item.get("definition_id"), + } + for name, item in zip(names, self.selection_items) + ] + + @property + def snapshot_dir(self) -> Path: + return self.workspace / ".sca-editor" + + def snapshot(self) -> dict: + """What the person is looking at, as plain data for a coding agent.""" + selection_cell = next((c for c in self.cells if c["code"].lstrip().startswith(MARKER)), None) + config = self.config + saved = read_saved_params(config) if config is not None else {} + png = self.snapshot_dir / "viewport.png" + return { + "workspace": str(self.workspace), + "notebook": str(self.notebook) if self.notebook else None, + "updated": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "selection": { + "items": self._selection_summary(), + "cell": {"id": selection_cell["id"], "code": selection_cell["code"]} if selection_cell else None, + "note": "the selection cell is saved in the notebook file; read it there", + }, + "viewport_png": str(png) if png.is_file() else None, + "cells": [ + { + "id": c["id"], + "title": c.get("title"), + "status": c.get("status"), + "errors": c.get("errors") or [], + } + for c in self.cells + ], + "parameters": { + name: saved.get(name, spec.default) for name, spec in self.specs.items() + }, + "modified_parameters": sorted(saved), + "model_revision": self.preview_revision, + } + + def write_snapshot(self) -> None: + """Keep ``.sca-editor/snapshot.json`` current; ``sca snapshot`` reads it.""" + try: + self.snapshot_dir.mkdir(parents=True, exist_ok=True) + (self.snapshot_dir / "snapshot.json").write_text( + json.dumps(self.snapshot(), indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + except OSError as error: + print(f"[sca edit] could not write the snapshot: {error}", file=sys.stderr) + + def save_viewport(self, png: bytes) -> None: + self.viewport_png = png + try: + self.snapshot_dir.mkdir(parents=True, exist_ok=True) + (self.snapshot_dir / "viewport.png").write_bytes(png) + except OSError as error: + print(f"[sca edit] could not write the viewport image: {error}", file=sys.stderr) + self.write_snapshot() + + # -- export ------------------------------------------------------------------ + + def _export(self) -> None: + """Write a product package next to the notebook: ``out/.scadpkg``. + + The editor shows previews; a package (the durable form, with the definition + and the feature graph) takes seconds to make, so it is made when asked for. + """ + kernel = self.kernel + if kernel is None or self.notebook is None: + self.hub.broadcast({"type": "status", "error": "nothing to export yet"}) + return + self.hub.broadcast({"type": "status", "working": "building the package"}) + self._package_event.clear() + result = kernel.publish(self.package_url, step=self.notebook.stem) + if not result.get("ok") or not self._package_event.wait(30) or self.latest_package_bytes is None: + self.hub.broadcast({"type": "status", "error": str(result.get("error", "the package was not made"))[-300:]}) + return + target = self.notebook.parent / "out" / f"{self.notebook.stem}.scadpkg" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(self.latest_package_bytes) + self.hub.broadcast({"type": "status", "exported": str(target)}) + + +def _module_code(code: str) -> str: + """marimo's code_mode compiles cell code as a MODULE: a ``return`` in the + cell's own scope (normal in marimo's file format) is a SyntaxError there. + Strip those — the returned expression stays in place and becomes the cell + output — and nothing else: a ``return`` inside a function or class the + cell defines (an ``@app.function`` cell is one) is code, not syntax. + """ + try: + tree = ast.parse(code) + except (SyntaxError, ValueError): + return code + returns: list[ast.Return] = [] + + def visit(node: ast.AST) -> None: + for child in ast.iter_child_nodes(node): + if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)): + continue # their returns are their own + if isinstance(child, ast.Return): + returns.append(child) + visit(child) + + visit(tree) + lines = code.splitlines() + for node in sorted(returns, key=lambda r: (r.lineno, r.col_offset), reverse=True): + line = lines[node.lineno - 1] + before, after = line[: node.col_offset], line[node.col_offset + len("return"):] + lines[node.lineno - 1] = before + ("pass" if node.value is None else after.lstrip()) + return "\n".join(lines) + + +def _static_cells(notebook: Path) -> list[dict]: + """The notebook's cells as the file has them, for showing before a kernel runs them.""" + from ..runtime._marimo import parse_notebook + + try: + parsed = parse_notebook(notebook.read_text(encoding="utf-8")) + except (OSError, UnicodeError, SyntaxError): + return [] + if parsed is None: + return [] + cells = [] + for index, cell in enumerate(parsed.cells): + if getattr(cell, "name", None) == "setup": + continue + cells.append( + { + "id": f"static-{index}", + "name": getattr(cell, "name", None) or "_", + "code": cell.code, + "status": "idle", + "errors": [], + "output": None, + "console": [], + "readonly": True, # not editable until the kernel has the cell + } + ) + return cells + + +PENDING_SELECTION_ID = "selection-pending" +"""Id of the selection cell the webuis see before the kernel has created it.""" + + +def file_line_ranges(text: str, codes: list[str]) -> list[tuple[int, int] | None]: + """Where each cell's code is in the notebook file (1-based, inclusive). + + *codes* are the cells' code in notebook order; they are matched to the + cells of the file in order, so a cell whose edit is not saved yet (or a + function cell the file spells differently) gets ``None``. + """ + from ..runtime._marimo import parse_notebook + + try: + notebook = parse_notebook(text) + except SyntaxError: + return [None] * len(codes) + if notebook is None: + return [None] * len(codes) + saved = [(cell.code.strip(), cell.lineno + 1, cell.lineno + max(1, len(cell.code.splitlines()))) for cell in notebook.cells] + ranges: list[tuple[int, int] | None] = [] + cursor = 0 + for code in codes: + found = None + for index in range(cursor, len(saved)): + if saved[index][0] == code.strip(): + found = (saved[index][1], saved[index][2]) + cursor = index + 1 + break + ranges.append(found) + return ranges + + +_PARAMS_HEADER = re.compile(r"^#\s*-{2,}\s*params(?::\s*(.*?))?\s*-*\s*$", re.MULTILINE) + + +def cell_title(code: str, feature: str | None) -> str: + """The step's name: its feature slug, its params header, else what it defines.""" + if feature: + return feature + if code.lstrip().startswith(MARKER): + return "selection" + header = _PARAMS_HEADER.search(code) + if header: + return f"params: {header.group(1)}" if header.group(1) else "params" + try: + body = ast.parse(code).body + except (SyntaxError, ValueError): + body = [] + for statement in body: + if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + return statement.name + targets = ( + statement.targets if isinstance(statement, ast.Assign) + else [statement.target] if isinstance(statement, ast.AnnAssign) else [] + ) + for target in targets: + if isinstance(target, ast.Name) and not target.id.startswith("_"): + return target.id + for line in code.splitlines(): + if line.strip().startswith("#"): + text = line.strip().lstrip("#").strip(" -") + if text: + return text[:40] + return "cell" + + +def _is_function_cell(code: str) -> bool: + """Whether the cell is one ``def``/``class`` (an ``@app.function`` cell).""" + try: + body = ast.parse(code).body + except (SyntaxError, ValueError): + return False + return len(body) == 1 and isinstance(body[0], (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + + +def create_app(state: WorkspaceState) -> Starlette: + hub = state.hub + + async def health(request: Request) -> JSONResponse: + return JSONResponse( + { + "ok": True, + "workspace": str(state.workspace), + "notebook": state.notebook.name if state.notebook else None, + "kernel": state.kernel is not None and state.kernel.alive, + } + ) + + async def push_preview(request: Request) -> JSONResponse: + """The kernel hands over the preview of the model it just built. + + The bytes are cached for late webuis; webuis only get a lightweight + revision event and fetch the bytes on demand. + """ + body = await request.body() + if not body: + return JSONResponse({"ok": False, "error": "empty preview"}, status_code=400) + unchanged = state.latest_preview_bytes == body + state.latest_preview_bytes = bytes(body) + state.preview_revision += 1 + state.emit( + { + "type": "preview", + "revision": state.preview_revision, + "step": request.headers.get("X-Live-Step", ""), + "name": state.notebook.stem if state.notebook else None, + "size": len(body), + # The model on screen is this model: nothing to reload. + "unchanged": unchanged, + } + ) + if state.notebook is not None and state._publish_signature is not None: + await asyncio.to_thread(state.remember_model, state.notebook, state._publish_signature, bytes(body)) + # The model is fine again: clear a stale "no product yet" message. + hub.broadcast({"type": "status", "error": ""}) + await asyncio.to_thread(state.write_snapshot) + return JSONResponse({"ok": True, "revision": state.preview_revision}) + + async def latest_preview(request: Request) -> Response: + """The freshest preview bytes.""" + if state.latest_preview_bytes: + return Response(content=state.latest_preview_bytes, media_type="application/vnd.simplecad.preview") + return JSONResponse({"error": "no preview published yet"}, status_code=404) + + async def push_package(request: Request) -> JSONResponse: + """The kernel hands over a full package, made because an export asked for it.""" + body = await request.body() + if not body: + return JSONResponse({"ok": False, "error": "empty package"}, status_code=400) + state.latest_package_bytes = bytes(body) + state._package_event.set() + return JSONResponse({"ok": True}) + + async def latest_package(request: Request) -> Response: + """The package last made for an export.""" + if state.latest_package_bytes: + return Response( + content=state.latest_package_bytes, + media_type="application/vnd.simplecad.scadpkg", + headers={"Content-Disposition": 'inline; filename="latest.scadpkg"'}, + ) + return JSONResponse({"error": "no package made yet"}, status_code=404) + + async def workspace_list(request: Request) -> JSONResponse: + """The SimpleCAD notebooks of the workspace.""" + current = state.notebook.relative_to(state.workspace).as_posix() if state.notebook else None + rows = [ + { + "name": Path(entry.path).stem, + "path": entry.path, + "entry": True, + "kind": entry.kind, + "id": entry.id, + "uses": list(entry.uses), + "current": entry.path == current, + "error": entry.error, + } + for entry in scan_workspace(state.workspace) + ] + return JSONResponse({"notebooks": rows, "modules": state.modules()}) + + async def workspace_open(request: Request) -> JSONResponse: + """Switch the active notebook (restarts the kernel).""" + try: + message = json.loads(await request.body()) + rel_path = str(message["path"]) + except Exception: + return JSONResponse({"ok": False, "error": "body must be {path}"}, status_code=400) + + try: + target = state.resolve_notebook(rel_path) + except ValueError as exc: + return JSONResponse({"ok": False, "error": str(exc)}, status_code=400) + generation = state.reserve_switch() + state.announce_opening(target) + + def run() -> None: + try: + state.open_notebook(rel_path, generation) + except Exception as exc: + hub.broadcast({"type": "status", "error": f"open failed: {exc}"}) + + threading.Thread(target=run, name="notebook-open", daemon=True).start() + return JSONResponse({"ok": True, "opening": rel_path}) + + async def workspace_new(request: Request) -> JSONResponse: + """Write a new notebook from a template, then open it.""" + try: + message = json.loads(await request.body()) + path = create_notebook( + state.workspace, str(message["name"]), str(message.get("kind", "part")) + ) + except (FileExistsError, ValueError) as exc: + return JSONResponse({"ok": False, "error": str(exc)}, status_code=400) + rel_path = path.relative_to(state.workspace).as_posix() + + generation = state.reserve_switch() + state.announce_opening(path) + + def run() -> None: + try: + state.open_notebook(rel_path, generation) + hub.broadcast({"type": "status", "created": rel_path}) + except Exception as exc: + hub.broadcast({"type": "status", "error": f"open failed: {exc}"}) + + threading.Thread(target=run, name="notebook-new", daemon=True).start() + return JSONResponse({"ok": True, "created": rel_path}) + + async def studio_select(request: Request) -> JSONResponse: + """Webui reports the viewport selection; agents read it back.""" + try: + message = json.loads(await request.body()) + except (json.JSONDecodeError, UnicodeDecodeError): + return JSONResponse({"ok": False, "error": "invalid json"}, status_code=400) + state.selection = message if isinstance(message, dict) else {"value": message} + state.emit({"type": "selection", **state.selection}) + return JSONResponse({"ok": True}) + + async def viewport_image(request: Request) -> JSONResponse: + """The webui posts what the viewport shows (PNG) when the selection changes.""" + body = await request.body() + if not body.startswith(b"\x89PNG"): + return JSONResponse({"ok": False, "error": "body must be a PNG"}, status_code=400) + await asyncio.to_thread(state.save_viewport, bytes(body)) + return JSONResponse({"ok": True}) + + async def snapshot(request: Request) -> JSONResponse: + """Agent-facing: the selection (as ql code in the notebook), the cells' state, the viewport image path.""" + return JSONResponse(state.snapshot()) + + async def studio_selected(request: Request) -> JSONResponse: + """Agent-facing read: what does the human have selected right now?""" + return JSONResponse({"selected": state.selection, "notebook": state.notebook.name if state.notebook else None}) + + async def ws_endpoint(websocket) -> None: + await hub.connect(websocket) + await websocket.send_json(state.hello()) + for envelope in state.replay(): + await websocket.send_json(envelope) + try: + while True: + message = await websocket.receive_json() + try: + if isinstance(message, dict) and str(message.get("type", "")).endswith( + (".set", ".request", ".edit", ".commit", ".revert", ".save", ".open", ".close", ".param", ".info") + ): + ack = await asyncio.to_thread(state.dispatch_command, message) + await websocket.send_json(ack) + else: + await websocket.send_json({"type": "echo", "message": message}) + except Exception as exc: # one bad command must not kill the socket + await websocket.send_json({"type": "error", "message": str(exc)[-300:]}) + except WebSocketDisconnect: + hub.disconnect(websocket) + + routes = [ + Route("/api/health", health), + Route("/api/runtime/preview", push_preview, methods=["POST"]), + Route("/api/preview/latest", latest_preview, methods=["GET"]), + Route("/api/runtime/package", push_package, methods=["POST"]), + Route("/api/package/latest", latest_package, methods=["GET"]), + Route("/api/workspace", workspace_list, methods=["GET"]), + Route("/api/workspace/open", workspace_open, methods=["POST"]), + Route("/api/workspace/new", workspace_new, methods=["POST"]), + Route("/api/studio/select", studio_select, methods=["POST"]), + Route("/api/studio/selected", studio_selected, methods=["GET"]), + Route("/api/studio/viewport", viewport_image, methods=["POST"]), + Route("/api/snapshot", snapshot, methods=["GET"]), + WebSocketRoute("/ws", ws_endpoint), + ] + app = Starlette(routes=routes) + if DIST_DIR.is_dir(): + app.mount("/", StaticFiles(directory=DIST_DIR, html=True), name="webui") + return app + + +def serve_workspace( + workspace: Path, + *, + host: str, + port: int, + open_browser: bool, + notebook: Path | None = None, +) -> int: + workspace = workspace.expanduser().resolve() + if not workspace.is_dir(): + print(f"[sca edit] workspace directory does not exist: {workspace}", file=sys.stderr) + return 2 + + state = WorkspaceState(workspace, notebook) + state.port = port + app = create_app(state) + config = uvicorn.Config(app, host=host, port=port, log_level="warning") + server = uvicorn.Server(config) + + original_startup = server.startup + + async def startup(sockets=None): + await original_startup(sockets) + hub = state.hub + hub.attach_loop(asyncio.get_running_loop()) + print(f"[sca edit] workspace {state.workspace}") + + if state.notebook is None: + print("[sca edit] no SimpleCAD notebook found in the workspace", file=sys.stderr) + hub.broadcast({"type": "status", "error": "no SimpleCAD notebook in the workspace; create one"}) + else: + def boot() -> None: + try: + assert state.notebook is not None + state.start_kernel(state.notebook) + except Exception as exc: + print(f"[sca edit] kernel boot failed: {exc}", file=sys.stderr) + hub.broadcast({"type": "status", "error": f"kernel boot failed: {exc}"}) + return + kernel = state.kernel + if kernel is not None: # None: a notebook was opened meanwhile + print(f"[sca edit] marimo kernel ready on {kernel.marimo_url}") + state.watch_kernel() + + threading.Thread(target=boot, name="kernel-boot", daemon=True).start() + + print(f"[sca edit] serving http://{host}:{port}/") + if open_browser: + threading.Thread( + target=webbrowser.open, args=(f"http://127.0.0.1:{port}/",), daemon=True + ).start() + + server.startup = startup + + original_shutdown = server.shutdown + + async def shutdown(sockets=None): + if state.kernel is not None: + state.kernel.stop() + await original_shutdown(sockets=sockets) + + server.shutdown = shutdown + server.run() + return 0 diff --git a/src/simplecadapi/editor/templates.py b/src/simplecadapi/editor/templates.py new file mode 100644 index 00000000..b03aa09d --- /dev/null +++ b/src/simplecadapi/editor/templates.py @@ -0,0 +1,135 @@ +"""New notebooks: a part or an assembly, as the editor's "new" action writes them. + +Both follow the feature tree convention (``docs/skill/.../feature-tree-convention.md``): +a ``# ---- params ----`` cell, one cell per feature, the product last. +The parameters carry ``# ui:`` comments so the editor offers sliders. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +from ..runtime.workspace import scan_workspace + +_HEADER = '''# /// script +# requires-python = ">=3.10" +# dependencies = ["simplecadapi"] +# +# [tool.simplecadapi] +# id = "{id}" +# revision = "1.0.0" +# /// +import marimo + +app = marimo.App(width="medium") + +with app.setup: + import simplecadapi as scad +''' + +_PART = ''' + +@app.cell +def _(): + # ---- params ---- + RADIUS = scad.var("radius", 25.0, comment="Radius", unit="mm") # ui: slider(5, 60, step=1, label="Radius") + HEIGHT = scad.var("height", 10.0, comment="Height", unit="mm") # ui: slider(2, 40, step=1, label="Height") + return HEIGHT, RADIUS + + +@app.cell +def _(HEIGHT, RADIUS): + # ---- feature: base (build) ---- + base = scad.make_cylinder_rsolid( + radius=RADIUS, + height=HEIGHT, + bottom_face_center=(0.0, 0.0, 0.0), + axis=(0.0, 0.0, 1.0), + ) + return (base,) + + +@app.cell +def _(base): + {variable} = scad.Part(part_id=scad.notebook_id(), body=base) + return ({variable},) +''' + +_ASSEMBLY = ''' + +@app.cell +def _(): + # ---- feature: parts (add) ---- +{uses} + return {names}, + + +@app.cell +def _({names}): + # ---- feature: layout (add) ---- + _assembly = scad.make_assembly_rassembly( + assembly_id=scad.notebook_id(), name="{title}" + ) +{components} + {variable} = _assembly + return ({variable},) +''' + +_RUNNER = ''' + +if __name__ == "__main__": + app.run() +''' + +_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*") + + +def render_notebook(name: str, kind: str = "part", parts: list[tuple[str, str]] | None = None) -> str: + """The source of a new notebook called *name*. + + An assembly lists the part notebooks it uses as ``(path, variable)``. + """ + if not _NAME.fullmatch(name): + raise ValueError(f"invalid notebook name: {name!r}") + variable = name.replace("-", "_") + header = _HEADER.format(id=name) + if kind == "part": + return header + _PART.format(variable=variable) + _RUNNER + if kind != "assembly": + raise ValueError(f"unknown notebook kind: {kind}") + if not parts: + raise ValueError("an assembly uses part notebooks; create a part first") + names = ", ".join(variable_name for _, variable_name in parts) + uses = "\n".join(f' {variable_name} = scad.use("{path}")' for path, variable_name in parts) + components = "\n".join( + " _assembly = scad.add_component_rassembly(\n" + f' assembly=_assembly, item={variable_name}, component_id="{variable_name}",\n' + " placement=scad.identity_placement_rplacement(),\n" + " )" + for _, variable_name in parts + ) + body = _ASSEMBLY.format( + uses=uses, names=names, title=name, components=components, variable=variable + ) + return header + body + _RUNNER + + +def create_notebook(workspace: Path, name: str, kind: str = "part") -> Path: + """Write a new notebook into *workspace*; refuses to clobber a file.""" + if not _NAME.fullmatch(name): + raise ValueError(f"invalid notebook name: {name!r}") + root = workspace.resolve() + path = root / f"{name}.py" + if path.exists(): + raise FileExistsError(f"notebook already exists: {path}") + parts: list[tuple[str, str]] = [] + if kind == "assembly": + for entry in scan_workspace(root): + if entry.kind == "part": + # Relative to the new notebook, which sits at the workspace root. + parts.append((entry.path, re.sub(r"\W", "_", Path(entry.path).stem))) + if len(parts) == 2: + break + path.write_text(render_notebook(name, kind, parts), encoding="utf-8") + return path diff --git a/src/simplecadapi/editor/ws.py b/src/simplecadapi/editor/ws.py new file mode 100644 index 00000000..fb4b50fb --- /dev/null +++ b/src/simplecadapi/editor/ws.py @@ -0,0 +1,53 @@ +"""WebSocket hub: connection registry and JSON broadcast. + +The runtime (server-side, possibly from a worker thread) pushes messages; +every connected webui receives them. Delivery is fire-and-forget — the +webui is a viewport, not a durable consumer. +""" + +from __future__ import annotations + +import asyncio +import json +from typing import Any + +from starlette.websockets import WebSocket + + +class WsHub: + def __init__(self) -> None: + self._clients: set[WebSocket] = set() + self._lock = asyncio.Lock() + self._loop: asyncio.AbstractEventLoop | None = None + + def attach_loop(self, loop: asyncio.AbstractEventLoop) -> None: + """Bind the running server loop so worker threads can push.""" + self._loop = loop + + async def connect(self, websocket: WebSocket) -> None: + await websocket.accept() + async with self._lock: + self._clients.add(websocket) + + def disconnect(self, websocket: WebSocket) -> None: + self._clients.discard(websocket) + + def broadcast(self, message: dict[str, Any]) -> None: + """Push one JSON message to every connected client (thread-safe).""" + self.broadcast_text(json.dumps(message, ensure_ascii=False)) + + def broadcast_text(self, text: str) -> None: + """Push one pre-serialized JSON frame to every client (thread-safe).""" + loop = self._loop + if loop is None or loop.is_closed(): + return + asyncio.run_coroutine_threadsafe(self._send_all(text), loop) + + async def _send_all(self, text: str) -> None: + async with self._lock: + clients = tuple(self._clients) + for client in clients: + try: + await client.send_text(text) + except Exception: + self._clients.discard(client) diff --git a/src/simplecadapi/example/__init__.py b/src/simplecadapi/example/__init__.py new file mode 100644 index 00000000..7fee03af --- /dev/null +++ b/src/simplecadapi/example/__init__.py @@ -0,0 +1,6 @@ +"""Bundled example workspaces, copied out for a person to open in ``sca edit``.""" + +from .cli import configure +from .runtime import EXAMPLES, copy_example, example_names + +__all__ = ["EXAMPLES", "configure", "copy_example", "example_names"] diff --git a/src/simplecadapi/example/cli.py b/src/simplecadapi/example/cli.py new file mode 100644 index 00000000..d164f250 --- /dev/null +++ b/src/simplecadapi/example/cli.py @@ -0,0 +1,52 @@ +"""The ``sca example`` command (see :mod:`simplecadapi.cli`). + +:: + + sca example # list the examples + sca example u_link [DIR] # copy one out, ready for ``sca edit`` +""" + +from __future__ import annotations + +import argparse +import shlex +from pathlib import Path +from typing import Any + +from .runtime import EXAMPLES, copy_example, example_names + + +def configure(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser( + "example", + help="copy a bundled example workspace out to open in the editor", + description="Copy a bundled example (a directory of SimpleCAD notebooks) into DIR " + "(default: ./), then open it with `sca edit`. Without a name, " + "list the examples. An existing directory is never overwritten.", + ) + parser.add_argument("name", nargs="?", choices=example_names(), help="the example to copy") + parser.add_argument("directory", nargs="?", type=Path, help="where to copy it to") + parser.set_defaults(handler=_run, printer=_print) + + +def _run(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + if args.name is None: + return {"examples": {name: EXAMPLES[name].summary for name in example_names()}}, 0 + target = copy_example(args.name, args.directory) + example = EXAMPLES[args.name] + opened = (target / example.open_with).resolve() + return { + "name": args.name, + "path": str(target), + "files": sorted(path.name for path in target.iterdir()), + "edit": f"sca edit {shlex.quote(str(opened))}", + }, 0 + + +def _print(report: dict[str, Any]) -> None: + if "examples" in report: + for name, summary in report["examples"].items(): + print(f"{name}\t{summary}") + return + print(f"copied {report['name']} to {report['path']} ({len(report['files'])} files)") + print(f"open it: {report['edit']}") diff --git a/src/simplecadapi/example/runtime.py b/src/simplecadapi/example/runtime.py new file mode 100644 index 00000000..990c6015 --- /dev/null +++ b/src/simplecadapi/example/runtime.py @@ -0,0 +1,83 @@ +"""Find and copy the bundled example workspaces. + +An example is a directory of notebooks. The wheel carries the sources under +``resources/`` (see ``pyproject.toml``); a checkout (editable install) reads +them from ``examples/`` at the repository root. +""" + +from __future__ import annotations + +import shutil +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class Example: + name: str + directory: str + """Name of the directory it is copied to by default.""" + open_with: str + """What to hand ``sca edit``, relative to the copy.""" + summary: str + + +EXAMPLES: dict[str, Example] = { + "u_link": Example( + name="u_link", + directory="u_link_motor_mount", + open_with=".", + summary="a motor mount: two parts and the assembly that fixes them together, " + "driven by the shared dimensions in dimensions.py", + ), +} + +_SOURCES = { + "u_link": ( + "u_link.py", + "shell.py", + "u_link_motor_mount.py", + "dimensions.py", + "common.py", + ), +} + + +def example_names() -> list[str]: + return sorted(EXAMPLES) + + +def _source_root(example: Example) -> Path: + bundled = Path(__file__).parent / "resources" / example.directory + if bundled.is_dir(): + return bundled + # An editable install keeps the examples where the repository has them. + return Path(__file__).resolve().parents[3] / "examples" / example.directory + + +def copy_example(name: str, destination: Path | None = None) -> Path: + """Copy example *name* to *destination* (default: its own name in the current directory). + + Refuses to write into an existing directory, so nothing of the person's is + overwritten. Returns the directory made. + """ + if name not in EXAMPLES: + raise ValueError(f"unknown example {name!r}; choose from: {', '.join(example_names())}") + example = EXAMPLES[name] + target = (destination if destination is not None else Path.cwd() / example.directory).expanduser() + target = target.absolute() + if target.exists(): + raise ValueError(f"{target} already exists; pick another directory (nothing was changed)") + source = _source_root(example) + files = [source / relative for relative in _SOURCES[name]] + missing = [file.name for file in files if not file.is_file()] + if missing: + raise ValueError(f"the bundled example is incomplete (missing {', '.join(missing)}); reinstall simplecadapi") + target.mkdir(parents=True) + try: + for file in files: + shutil.copy2(file, target / file.name) + except OSError: + shutil.rmtree(target, ignore_errors=True) + raise + return target diff --git a/src/simplecadapi/inspect/drawing/_pdf.py b/src/simplecadapi/inspect/drawing/_pdf.py index 64475116..254bc3a8 100644 --- a/src/simplecadapi/inspect/drawing/_pdf.py +++ b/src/simplecadapi/inspect/drawing/_pdf.py @@ -14,9 +14,12 @@ from __future__ import annotations from pathlib import Path -from typing import Sequence +from typing import TYPE_CHECKING, Sequence -import pymupdf +if TYPE_CHECKING: + import pymupdf +else: + from ._pymupdf import pymupdf _ITEM_KINDS = {"l": "line", "c": "bezier", "qu": "quad", "re": "rect"} diff --git a/src/simplecadapi/inspect/drawing/_pymupdf.py b/src/simplecadapi/inspect/drawing/_pymupdf.py new file mode 100644 index 00000000..f4a9d49d --- /dev/null +++ b/src/simplecadapi/inspect/drawing/_pymupdf.py @@ -0,0 +1,40 @@ +"""Lazy access to pymupdf for the drawing inspection namespace. + +pymupdf (AGPL-3.0 / Artifex commercial dual license) is an optional dependency +of the ``drawing`` extra. It is needed only by the engineering-drawing family, +never by modeling, export or rendering, so ``import simplecadapi`` must work +without it. Modules bind the proxy exported here under the name ``pymupdf`` +(real module under ``TYPE_CHECKING``); the first attribute access performs the +actual import, and a missing pymupdf surfaces as an ImportError naming the +extra to install. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import pymupdf # noqa: F401 + +_MISSING_PYMUPDF_MESSAGE = ( + "simplecadapi.inspect.drawing requires the optional 'drawing' extra; " + "install it with: pip install 'simplecadapi[drawing]' (provides pymupdf)" +) + + +class _LazyPymupdf: + """Stand-in for the ``pymupdf`` module that imports it on first use.""" + + __slots__ = () + + def __getattr__(self, name: str) -> Any: + try: + import pymupdf + except ImportError as exc: + raise ImportError(_MISSING_PYMUPDF_MESSAGE) from exc + return getattr(pymupdf, name) + + +pymupdf = _LazyPymupdf() + +__all__ = ["pymupdf"] diff --git a/src/simplecadapi/inspect/drawing/_section_layout.py b/src/simplecadapi/inspect/drawing/_section_layout.py index f5c91710..2502e571 100644 --- a/src/simplecadapi/inspect/drawing/_section_layout.py +++ b/src/simplecadapi/inspect/drawing/_section_layout.py @@ -2,7 +2,7 @@ import math -import pymupdf +from ._pymupdf import pymupdf from ._provenance import digest COLOR = (0.0, 0.0, 0.75) diff --git a/src/simplecadapi/inspect/drawing/geometry.py b/src/simplecadapi/inspect/drawing/geometry.py index f47a43ee..d3909e61 100644 --- a/src/simplecadapi/inspect/drawing/geometry.py +++ b/src/simplecadapi/inspect/drawing/geometry.py @@ -5,9 +5,12 @@ import json from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any, Sequence +from typing import TYPE_CHECKING, Any, Sequence -import pymupdf +if TYPE_CHECKING: + import pymupdf +else: + from ._pymupdf import pymupdf from ._pdf import color_to_hex, item_kind, open_page, parse_color, point_to_display from ._provenance import page_metadata diff --git a/src/simplecadapi/inspect/drawing/render.py b/src/simplecadapi/inspect/drawing/render.py index 44cabf95..a6ba1ff0 100644 --- a/src/simplecadapi/inspect/drawing/render.py +++ b/src/simplecadapi/inspect/drawing/render.py @@ -4,9 +4,12 @@ import json from pathlib import Path -from typing import Any, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Mapping, Sequence -import pymupdf +if TYPE_CHECKING: + import pymupdf +else: + from ._pymupdf import pymupdf from ._pdf import open_page from ._provenance import page_metadata, pixel_transform diff --git a/src/simplecadapi/inspect/drawing/section.py b/src/simplecadapi/inspect/drawing/section.py index 06ef2ca2..6966e776 100644 --- a/src/simplecadapi/inspect/drawing/section.py +++ b/src/simplecadapi/inspect/drawing/section.py @@ -13,9 +13,13 @@ import math from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Mapping, Sequence + +if TYPE_CHECKING: + import pymupdf +else: + from ._pymupdf import pymupdf -import pymupdf import numpy as np from ._provenance import digest, model_metadata, pixel_transform, file_hash diff --git a/src/simplecadapi/inspect/drawing/summary.py b/src/simplecadapi/inspect/drawing/summary.py index 49fda483..5feb1f69 100644 --- a/src/simplecadapi/inspect/drawing/summary.py +++ b/src/simplecadapi/inspect/drawing/summary.py @@ -5,9 +5,12 @@ import json from dataclasses import asdict, dataclass from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any -import pymupdf +if TYPE_CHECKING: + import pymupdf +else: + from ._pymupdf import pymupdf from ._pdf import color_to_hex, item_kind diff --git a/src/simplecadapi/inspect/drawing/text.py b/src/simplecadapi/inspect/drawing/text.py index 773febcc..6fed8dcf 100644 --- a/src/simplecadapi/inspect/drawing/text.py +++ b/src/simplecadapi/inspect/drawing/text.py @@ -5,9 +5,12 @@ import json from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any -import pymupdf +if TYPE_CHECKING: + import pymupdf +else: + from ._pymupdf import pymupdf from ._pdf import box_to_display, open_page from ._provenance import page_metadata diff --git a/src/simplecadapi/kernel/ocp_properties.py b/src/simplecadapi/kernel/ocp_properties.py index 941c6eba..1e14f543 100644 --- a/src/simplecadapi/kernel/ocp_properties.py +++ b/src/simplecadapi/kernel/ocp_properties.py @@ -107,7 +107,9 @@ def surface_area(face: TopoDS_Face) -> float: def volume(solid: TopoDS_Solid) -> float: props = GProp_GProps() - BRepGProp.VolumeProperties_s(solid, props) + # Integrate NURBS by knot spans. The fixed-order routine both loses + # precision on rational curves and can crash on affine transformed trims. + BRepGProp.VolumePropertiesGK_s(solid, props, 1e-9, True, True) return float(props.Mass()) @@ -146,7 +148,7 @@ def center_of_mass(shape: TopoDS_Shape) -> Vec3: if kind in (TopAbs_SOLID, TopAbs_COMPSOLID, TopAbs_COMPOUND): props = GProp_GProps() try: - BRepGProp.VolumeProperties_s(shape, props) + BRepGProp.VolumePropertiesGK_s(shape, props, 1e-9, True, True) point = _props_mass_point(props) if point is not None: return point diff --git a/src/simplecadapi/operators/__init__.py b/src/simplecadapi/operators/__init__.py index 9942bbba..4f3299d8 100644 --- a/src/simplecadapi/operators/__init__.py +++ b/src/simplecadapi/operators/__init__.py @@ -16,7 +16,7 @@ import sys as _sys from types import ModuleType as _ModuleType -from . import _support, geometry, transform, boolean, product, sketch, selection, features +from . import _support, geometry, transform, boolean, product, sketch, selection, features, deformation, section_features, limited_extrusion, draft from ._support import * from .geometry import * from .transform import * @@ -25,6 +25,10 @@ from .sketch import * from .selection import * from .features import * +from .deformation import * +from .section_features import * +from .limited_extrusion import * +from .draft import * _IMPLEMENTATION_MODULES = ( _support, @@ -35,6 +39,10 @@ sketch, selection, features, + deformation, + section_features, + limited_extrusion, + draft, ) __all__ = tuple( diff --git a/src/simplecadapi/operators/_support.py b/src/simplecadapi/operators/_support.py index 57f7a77a..c5c1576e 100644 --- a/src/simplecadapi/operators/_support.py +++ b/src/simplecadapi/operators/_support.py @@ -1245,6 +1245,7 @@ def _record_geo_selection_nodes( op=op, params={ "target_kind": kind, + "source_output_slot": int(source_shape._get_runtime("graph.output_slot", 0)), "geo_selector": _make_geo_selector( selected, source_shape=source_shape, diff --git a/src/simplecadapi/operators/deformation.py b/src/simplecadapi/operators/deformation.py new file mode 100644 index 00000000..1121be9f --- /dev/null +++ b/src/simplecadapi/operators/deformation.py @@ -0,0 +1,217 @@ +"""Recorded scaling and signed offsets for CAD source translation.""" +from __future__ import annotations + +import math +from typing import Sequence + +import numpy as np +from OCP.BRepBuilderAPI import BRepBuilderAPI_Copy, BRepBuilderAPI_GTransform, BRepBuilderAPI_Transform +from OCP.BRepCheck import BRepCheck_Analyzer +from OCP.BRepGProp import BRepGProp +from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid +from OCP.GeomAbs import GeomAbs_Arc, GeomAbs_Intersection +from OCP.GProp import GProp_GProps +from OCP.TopTools import TopTools_ListOfShape +from OCP.gp import gp_GTrsf, gp_Pnt, gp_Trsf + +from ._support import ( + Solid, Face, ShapeSelector, ScalarLike, evaluate_scalar, evaluate_value, + get_current_cs, solids_of, record_operation_if_active, + _attach_lineage_from_source, _current_context_metadata, + _ensure_geo_selection_node_ids, _resolve_selector_or_shapes, + _serialize_shape_refs, + _ensure_geo_selection_input_nodes, +) + + +def _finite_vector(value: Sequence[float], name: str) -> tuple[float, float, float]: + values = tuple(float(v) for v in evaluate_value(value)) + if len(values) != 3 or not all(math.isfinite(v) for v in values): + raise ValueError(f"{name} must contain three finite numbers") + return values + + +def _finish(source: Solid, wrapped, op: str, params: dict, inputs: list) -> Solid: + solids = solids_of(wrapped) + if len(solids) != 1: + raise ValueError(f"{op} must produce exactly one solid, got {len(solids)}") + result = Solid(solids[0]) + props = GProp_GProps() + BRepGProp.VolumePropertiesGK_s(result.wrapped, props, 1e-9, True, True) + if props.Mass() < 0: + result.wrapped.Reverse() + if not BRepCheck_Analyzer(result.wrapped).IsValid(): + raise ValueError(f"{op} produced invalid geometry") + result._metadata = source._metadata.copy() + # These builders do not yet provide a complete face-history channel. + _attach_lineage_from_source(source, result, derivation="continuation", op=op, coverage="partial") + _ensure_geo_selection_input_nodes(inputs) + params = {**params, "input_refs": _serialize_shape_refs(inputs)} + record_operation_if_active(op=op, params=params, outputs=result, + input_shapes=inputs, context=_current_context_metadata()) + return result + + +def scale_rsolid( + solid: Solid, + factors: Sequence[float], + center: Sequence[float] = (0, 0, 0), +) -> Solid: + """Scale along current coordinate-system axes about an explicit center. + + Every factor must be finite and nonzero. A uniform scale preserves analytic + geometry; a nonuniform scale converts curves/surfaces where the kernel needs + it. The input is unchanged and this operation is recorded for graph replay. + """ + values = _finite_vector(factors, "factors") + center_value = _finite_vector(center, "center") + if any(v == 0 for v in values): + raise ValueError("scale factors must be nonzero") + cs = get_current_cs() + global_center = cs.transform_point(np.array(center_value)) + if values[0] == values[1] == values[2]: + transform = gp_Trsf() + transform.SetScale(gp_Pnt(*global_center), values[0]) + builder = BRepBuilderAPI_Transform(solid.wrapped, transform, True) + else: + basis = np.column_stack([cs.transform_vector(np.eye(3)[i]) for i in range(3)]) + matrix = basis @ np.diag(values) @ basis.T + translation = global_center - matrix @ global_center + transform = gp_GTrsf() + for row in range(3): + for col in range(3): + transform.SetValue(row + 1, col + 1, float(matrix[row, col])) + transform.SetValue(row + 1, 4, float(translation[row])) + # GTransform's copy flag copies topology, but can retain shared curve + # geometry from boolean/fillet history. Isolate that geometry first: + # otherwise OCCT 7.9 can produce invalid p-curves and crash its checker. + copied = BRepBuilderAPI_Copy(solid.wrapped, True, False).Shape() + builder = BRepBuilderAPI_GTransform(copied, transform, True) + builder.Build() + if not builder.IsDone(): + raise ValueError("kernel could not scale the solid") + return _finish(solid, builder.Shape(), "make_scale_rsolid", + {"factors": factors, "center": center}, [solid]) + + +def transform_rsolid(solid: Solid, matrix: Sequence[Sequence[float]]) -> Solid: + """Apply a finite nonsingular 4x4 affine matrix in the current frame. + + General affine transformations include shear and reflection. Perspective + matrices and singular maps cannot define a solid and are rejected. + """ + values = np.asarray(evaluate_value(matrix), dtype=float) + if values.shape != (4, 4) or not np.isfinite(values).all(): + raise ValueError("matrix must be a finite 4x4 affine matrix") + if not np.allclose(values[3], (0, 0, 0, 1), rtol=0, atol=1e-12): + raise ValueError("perspective transforms cannot define an affine solid") + if abs(float(np.linalg.det(values[:3, :3]))) < 1e-14: + raise ValueError("matrix must be nonsingular") + cs = get_current_cs() + basis = np.column_stack([cs.transform_vector(np.eye(3)[i]) for i in range(3)]) + origin = cs.transform_point(np.zeros(3)) + linear = basis @ values[:3, :3] @ basis.T + translation = origin + basis @ values[:3, 3] - linear @ origin + transform = gp_GTrsf() + for row in range(3): + for col in range(3): + transform.SetValue(row + 1, col + 1, float(linear[row, col])) + transform.SetValue(row + 1, 4, float(translation[row])) + copied = BRepBuilderAPI_Copy(solid.wrapped, True, False).Shape() + builder = BRepBuilderAPI_GTransform(copied, transform, True) + builder.Build() + if not builder.IsDone(): + raise ValueError("kernel could not transform the solid") + return _finish(solid, builder.Shape(), "make_transform_rsolid", {"matrix": matrix}, [solid]) + + +def offset_rsolid( + solid: Solid, + amount: ScalarLike, + openings: Sequence[Face] | ShapeSelector = (), + *, + tolerance: float = 1e-4, + kind: str = "ARC", +) -> Solid: + """Offset a solid outward (positive) or inward (negative). + + Selected openings form an open shell. Empty openings produce the closed + offset solid. This is distinct from shell_rsolid's positive inward wall + thickness, and supports both signs and ARC/INTERSECTION joins explicitly. + """ + distance = evaluate_scalar(amount) + if not math.isfinite(distance) or distance == 0: + raise ValueError("amount must be finite and nonzero") + if not math.isfinite(tolerance) or tolerance <= 0: + raise ValueError("tolerance must be finite and positive") + joins = {"ARC": GeomAbs_Arc, "INTERSECTION": GeomAbs_Intersection} + if kind not in joins: + raise ValueError("kind must be ARC or INTERSECTION") + faces = list(_resolve_selector_or_shapes(solid, openings)) + if any(not isinstance(face, Face) for face in faces): + raise ValueError("openings must select faces") + selected_ids = _ensure_geo_selection_node_ids(solid, faces) + face_list = TopTools_ListOfShape() + for face in faces: + face_list.Append(face.wrapped) + builder = BRepOffsetAPI_MakeThickSolid() + builder.MakeThickSolidByJoin(solid.wrapped, face_list, distance, tolerance, + Intersection=True, RemoveIntEdges=True, Join=joins[kind]) + builder.Build() + if not builder.IsDone(): + raise ValueError("kernel could not offset the solid") + params = {"amount": amount, "tolerance": tolerance, "kind": kind, + "removed_face_count": len(faces), "selected_faces": _serialize_shape_refs(faces)} + if selected_ids: + params["selected_face_node_ids"] = selected_ids + return _finish(solid, builder.Shape(), "make_offset_rsolid", params, [solid, *faces]) + + +def boolean_rsolidlist( + left: Sequence[Solid], + right: Sequence[Solid], + operation: str, +) -> list[Solid]: + """Apply a set boolean to two body groups, retaining every result solid. + + Members of each group are a geometric set. Unlike single-solid booleans, + a cut or intersection may return several solids or an empty list. + Topology lineage is partial; the ordered inputs and all output slots are + recorded so downstream features can replay the same geometry. + """ + from ..kernel.ocp_booleans import cut_shapes, common_shapes, fuse_shapes + from ..kernel.ocp_export import make_compound_always + lhs, rhs = list(left), list(right) + if operation not in {"cut", "intersect", "union"}: + raise ValueError("operation must be cut, intersect or union") + inputs = [*lhs, *rhs] + if any(not isinstance(body, Solid) for body in inputs): + raise TypeError("boolean groups must contain Solid values") + if any(body.wrapped.IsNull() or not BRepCheck_Analyzer(body.wrapped).IsValid() for body in inputs): + raise ValueError("boolean groups must contain valid nonempty topology") + if operation == "union": + shape = fuse_shapes([body.wrapped for body in inputs]) if inputs else None + elif not lhs or (operation == "intersect" and not rhs): + shape = None + elif operation == "cut": + shape = cut_shapes(make_compound_always([body.wrapped for body in lhs]), + [body.wrapped for body in rhs]) + else: + shape = common_shapes([make_compound_always([body.wrapped for body in group]) + for group in (lhs, rhs)]) + if shape is not None and not BRepCheck_Analyzer(shape).IsValid(): + raise ValueError("set boolean produced invalid topology") + result = [Solid(solid) for solid in solids_of(shape)] if shape is not None else [] + for body in result: + for source in inputs: + _attach_lineage_from_source(source, body, derivation="continuation", + op="make_boolean_rsolidlist", coverage="partial") + _ensure_geo_selection_input_nodes(inputs) + record_operation_if_active(op="make_boolean_rsolidlist", + params={"operation": operation, "left_count": len(lhs), "right_count": len(rhs), + "input_refs": _serialize_shape_refs(inputs)}, outputs=result, + input_shapes=inputs, context=_current_context_metadata()) + return result + + +__all__ = ("scale_rsolid", "offset_rsolid", "transform_rsolid", "boolean_rsolidlist") diff --git a/src/simplecadapi/operators/draft.py b/src/simplecadapi/operators/draft.py new file mode 100644 index 00000000..1b427779 --- /dev/null +++ b/src/simplecadapi/operators/draft.py @@ -0,0 +1,83 @@ +"""Draft selected analytic solid faces around a recorded neutral plane.""" +from __future__ import annotations + +import math +from typing import Sequence + +import numpy as np +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.BRepOffsetAPI import BRepOffsetAPI_DraftAngle +from OCP.GeomAbs import GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone +from OCP.gp import gp_Dir, gp_Pln, gp_Pnt + +from ._support import ( + Face, Solid, ShapeSelector, ScalarLike, evaluate_scalar, get_current_cs, + _resolve_selector_or_shapes, _ensure_geo_selection_node_ids, _serialize_shape_refs, +) +from .deformation import _finish, _finite_vector + + +def draft_rsolid( + solid: Solid, + faces_to_draft: Sequence[Face] | ShapeSelector, + angle: ScalarLike, + plane_origin: Sequence[float] = (0, 0, 0), + plane_normal: Sequence[float] = (0, 0, 1), +) -> Solid: + """Draft planar, cylindrical or conical faces by a signed angle in degrees. + + The neutral plane fixes the unchanged section. Its origin and normal are in + current coordinate-system coordinates. Face selection is checked against + the input body and retained as graph inputs for strict replay. The original + solid is preserved. Other surface families are explicitly rejected. + """ + if not isinstance(solid, Solid): + raise TypeError("solid must be a Solid") + degrees = float(evaluate_scalar(angle)) + if not math.isfinite(degrees) or abs(degrees) >= 90: + raise ValueError("angle must be finite and strictly between -90 and 90 degrees") + origin = _finite_vector(plane_origin, "plane_origin") + normal = _finite_vector(plane_normal, "plane_normal") + cs = get_current_cs() + global_origin = cs.transform_point(np.asarray(origin)) + global_normal = cs.transform_vector(np.asarray(normal)) + magnitude = float(np.linalg.norm(global_normal)) + if magnitude <= 1e-14: + raise ValueError("plane_normal must be nonzero") + global_normal = global_normal / magnitude + selected = list(_resolve_selector_or_shapes(solid, faces_to_draft)) + if not selected: + raise ValueError("draft requires at least one selected face") + original_faces = solid._iter_faces() + for face in selected: + if not isinstance(face, Face): + raise ValueError("faces_to_draft must select only Face objects") + if not any(face.wrapped.IsSame(other.wrapped) for other in original_faces): + raise ValueError("selected draft faces must belong to the input solid") + if BRepAdaptor_Surface(face.wrapped).GetType() not in {GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone}: + raise ValueError("draft supports only planar, cylindrical, and conical faces") + # Remove repeated references without changing the ordered face selection. + selected = list({face.topo_id: face for face in selected}.values()) + selected_ids = _ensure_geo_selection_node_ids(solid, selected) + if degrees == 0: + shape = solid.wrapped + else: + direction = gp_Dir(*global_normal) + plane = gp_Pln(gp_Pnt(*global_origin), direction) + builder = BRepOffsetAPI_DraftAngle(solid.wrapped) + for face in selected: + builder.Add(face.wrapped, direction, math.radians(degrees), plane, True) + if not builder.AddDone(): + raise ValueError("kernel could not add the requested draft angle to a selected face") + builder.Build() + if not builder.IsDone(): + raise ValueError("kernel could not build the drafted solid") + shape = builder.Shape() + params = {"angle": angle, "plane_origin": plane_origin, "plane_normal": plane_normal, + "selected_face_count": len(selected), "selected_faces": _serialize_shape_refs(selected)} + if selected_ids: + params["selected_face_node_ids"] = selected_ids + return _finish(solid, shape, "make_draft_rsolid", params, [solid, *selected]) + + +__all__ = ("draft_rsolid",) diff --git a/src/simplecadapi/operators/limited_extrusion.py b/src/simplecadapi/operators/limited_extrusion.py new file mode 100644 index 00000000..b6de8f27 --- /dev/null +++ b/src/simplecadapi/operators/limited_extrusion.py @@ -0,0 +1,180 @@ +"""Recorded extrusion terminated by a real target face or curved shell. + +The construction follows the surface-history approach used by build123d: +intersect a long prism, sew the affected target faces, order contact surfaces, +and split by the selected surface. Bounding boxes only bound the temporary +prism; they never define the termination surface. +""" +from __future__ import annotations + +import math +from typing import Sequence + +import numpy as np +from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Splitter +from OCP.BRepBuilderAPI import BRepBuilderAPI_Sewing +from OCP.BRepCheck import BRepCheck_Analyzer +from OCP.BRepGProp import BRepGProp, BRepGProp_Face +from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace, BRepPrimAPI_MakePrism +from OCP.BRepTools import BRepTools +from OCP.GProp import GProp_GProps +from OCP.TopAbs import TopAbs_COMPOUND, TopAbs_FACE, TopAbs_SHELL +from OCP.TopoDS import TopoDS, TopoDS_Iterator +from OCP.TopTools import TopTools_ListOfShape +from OCP.gp import gp_Pnt, gp_Vec + +from ..kernel.ocp_booleans import solids_of +from ..kernel.ocp_export import make_compound_always +from ..kernel.ocp_properties import bounding_box, center_of_mass, distance, volume +from ..kernel.ocp_topology import faces_of +from ._support import Face, Solid, evaluate_value, get_current_cs +from .deformation import _finish, _finite_vector + + +def _top_level(shape): + if shape.ShapeType() != TopAbs_COMPOUND: + return [shape] + output = [] + iterator = TopoDS_Iterator(shape) + while iterator.More(): + output.extend(_top_level(iterator.Value())) + iterator.Next() + return output + + +def _crosses_direction(face, axis): + """Ignore side walls tangent to the extrusion, including coincident walls.""" + u0, u1, v0, v1 = BRepTools.UVBounds_s(face) + properties = BRepGProp_Face(face) + for u in (.25, .5, .75): + for v in (.25, .5, .75): + point, normal = gp_Pnt(), gp_Vec() + properties.Normal(u0 + u * (u1-u0), v0 + v * (v1-v0), point, normal) + length = normal.Magnitude() + if length > 1e-14 and abs(normal.X()*axis[0] + normal.Y()*axis[1] + normal.Z()*axis[2]) / length > 1e-8: + return True + return False + + +def _contact_surfaces(target, prism, axis): + common = BRepAlgoAPI_Common(target, prism) + common.Build() + if not common.IsDone() or not faces_of(common.Shape()): + raise ValueError("No intersection: extrusion does not contact target") + history = common.History() + modified = [] + intersection_faces = faces_of(common.Shape()) + for face in faces_of(target): + affected = list(history.Modified(face)) + if affected: + modified.extend(TopoDS.Face_s(item) for item in affected if item.ShapeType() == TopAbs_FACE) + elif not history.IsRemoved(face) and any(item.IsSame(face) for item in intersection_faces): + # A whole target may lie inside the temporary prism, in which case + # unchanged boundary faces are still genuine contact surfaces. + modified.append(face) + if not modified: + raise ValueError("No target boundary surfaces found in extrusion intersection") + sewing = BRepBuilderAPI_Sewing(1e-6) + for face in modified: + if _crosses_direction(face, axis): + sewing.Add(face) + sewing.Perform() + surfaces = _top_level(sewing.SewedShape()) + if not surfaces or any(part.ShapeType() not in {TopAbs_FACE, TopAbs_SHELL} for part in surfaces): + raise ValueError("Kernel could not sew target contact surfaces") + return surfaces + + +def _split_on_surface(prism, surface, keep_top): + arguments, tools = TopTools_ListOfShape(), TopTools_ListOfShape() + arguments.Append(prism) + tools.Append(surface) + splitter = BRepAlgoAPI_Splitter() + splitter.SetArguments(arguments) + splitter.SetTools(tools) + splitter.Build() + if not splitter.IsDone(): + raise ValueError("Kernel could not split extrusion on target surface") + reference_face = TopoDS.Face_s(surface) if surface.ShapeType() == TopAbs_FACE else faces_of(surface)[0] + u0, u1, v0, v1 = BRepTools.UVBounds_s(reference_face) + point, normal = gp_Pnt(), gp_Vec() + BRepGProp_Face(reference_face).Normal((u0+u1)/2, (v0+v1)/2, point, normal) + if normal.Magnitude() <= 1e-14: + raise ValueError("Undefined normal on termination surface") + normal.Normalize() + reference = point.Translated(normal) + if surface.ShapeType() == TopAbs_FACE: + halfspace = BRepPrimAPI_MakeHalfSpace(TopoDS.Face_s(surface), reference).Solid() + else: + halfspace = BRepPrimAPI_MakeHalfSpace(TopoDS.Shell_s(surface), reference).Solid() + candidates = [] + for part in solids_of(splitter.Shape()): + common = BRepAlgoAPI_Common(part, halfspace) + common.Build() + if not common.IsDone(): + raise ValueError("Kernel could not classify the termination side") + properties = GProp_GProps() + BRepGProp.LinearProperties_s(common.Shape(), properties) + is_top = properties.Mass() >= 1e-6 + if is_top == keep_top: + candidates.append(part) + return candidates + + +def extrude_until_rsolid( + profile: Face, + target: Solid | Sequence[Solid], + direction: Sequence[float] = (0, 0, 1), + until: str = "NEXT", +) -> Solid: + """Extrude a planar face until a target surface, including curved surfaces. + + NEXT and LAST select the nearest and farthest forward contact. PREVIOUS + and FIRST reverse the direction and select the nearest and farthest contact + behind the profile. Target surfaces must cover the profile at the contact; + an uncovered/open or disconnected output is rejected. Every target remains + a graph input, so changes to target geometry replay this feature correctly. + """ + if not isinstance(profile, Face): + raise TypeError("profile must be a Face") + targets = [target] if isinstance(target, Solid) else list(target) + if not targets: + raise ValueError("target must contain at least one Solid") + if any(not isinstance(item, Solid) for item in targets): + raise TypeError("target must be a Solid or a sequence of Solid") + mode = str(getattr(until, "name", until)).upper() + if mode not in {"NEXT", "LAST", "PREVIOUS", "FIRST"}: + raise ValueError("until must be NEXT, LAST, PREVIOUS, or FIRST") + values = _finite_vector(direction, "direction") + global_direction = get_current_cs().transform_vector(np.asarray(values)) + magnitude = float(np.linalg.norm(global_direction)) + if not math.isfinite(magnitude) or magnitude <= 1e-14: + raise ValueError("direction must be a finite nonzero vector") + axis = global_direction / magnitude + if mode in {"PREVIOUS", "FIRST"}: + axis = -axis + bound_target = make_compound_always([item.wrapped for item in targets]) + box = bounding_box(make_compound_always([profile.wrapped, bound_target])) + length = math.sqrt((box.xmax-box.xmin)**2 + (box.ymax-box.ymin)**2 + (box.zmax-box.zmin)**2) + if length <= 1e-12: + raise ValueError("Degenerate profile and target bounds") + prism = BRepPrimAPI_MakePrism(profile.wrapped, gp_Vec(*(axis * length))).Shape() + surfaces = sorted(_contact_surfaces(bound_target, prism, axis), key=lambda part: distance(part, profile.wrapped)) + nearest = mode in {"NEXT", "PREVIOUS"} + surface = surfaces[0] if nearest else surfaces[-1] + candidates = _split_on_surface(prism, surface, keep_top=nearest) + if not candidates: + raise ValueError("Extrusion produced no solid on the requested side of the target") + origin = np.asarray(tuple(center_of_mass(profile.wrapped))) + chosen = min(candidates, key=lambda part: np.dot(np.asarray(tuple(center_of_mass(part))) - origin, axis)) + if abs(volume(chosen) - volume(prism)) <= max(1e-8, abs(volume(prism))*1e-9): + raise ValueError("Target termination surface does not completely cover the extrusion profile") + if volume(chosen) <= 1e-12 or not BRepCheck_Analyzer(chosen).IsValid(): + raise ValueError("Target surface did not completely cover a valid limited extrusion") + result = _finish(profile, chosen, "make_extrude_until_rsolid", + {"direction": direction, "until": mode, "target_count": len(targets)}, [profile, *targets]) + result._apply_tag("solid.extrusion", propagate=False) + return result + + +__all__ = ("extrude_until_rsolid",) diff --git a/src/simplecadapi/operators/product.py b/src/simplecadapi/operators/product.py index b6fb1fab..fede3b48 100644 --- a/src/simplecadapi/operators/product.py +++ b/src/simplecadapi/operators/product.py @@ -131,9 +131,10 @@ def make_part_rpart( try: part = Part(part_id, body, name=name) _reserve_semantic_id("part", part.part_id) + _ensure_geo_selection_input_nodes([body]) record_operation_if_active( _OP_MAKE_PART_RPART, - _part_params(part), + {**_part_params(part), "input_refs": _serialize_shape_refs([body])}, outputs=part, input_shapes=[body], semantic_delta=_semantic_created("Part", part.part_id, part.to_dict()), diff --git a/src/simplecadapi/operators/section_features.py b/src/simplecadapi/operators/section_features.py new file mode 100644 index 00000000..9292eea8 --- /dev/null +++ b/src/simplecadapi/operators/section_features.py @@ -0,0 +1,150 @@ +"""Recorded tapered extrusions and sweeps with multiple explicit sections.""" +from __future__ import annotations + +import math +from typing import Sequence + +import numpy as np +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut +from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform +from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeOffset, BRepOffsetAPI_MakePipeShell, BRepOffsetAPI_ThruSections +from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism +from OCP.BRepTools import BRepTools +from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_Plane +from OCP.LocOpe import LocOpe_DPrism +from OCP.TopAbs import TopAbs_WIRE +from OCP.TopExp import TopExp_Explorer +from OCP.TopoDS import TopoDS +from OCP.gp import gp_Dir, gp_Trsf, gp_Vec + +from ._support import Face, Wire, Solid, evaluate_scalar, get_current_cs +from .deformation import _finish, _finite_vector + + +def extrude_taper_rsolid( + profile: Face, + vector: Sequence[float], + taper: float, + flip_inner: bool = True, +) -> Solid: + """Extrude a planar face by a vector, with a taper in degrees. + + Positive taper shrinks the outer section. Inner loops optionally taper in + the opposite direction. Analytic draft prisms are retained where valid; + other orientations/holes use exact offset wire sections and solid lofts. + """ + values = _finite_vector(vector, "vector") + angle = float(evaluate_scalar(taper)) + if not math.isfinite(angle) or abs(angle) >= 90: + raise ValueError("taper must be finite and between -90 and 90 degrees") + displacement = get_current_cs().transform_vector(np.asarray(values)) + length = float(np.linalg.norm(displacement)) + if length <= 1e-12: + raise ValueError("extrusion vector must be nonzero") + surface = BRepAdaptor_Surface(profile.wrapped, True) + if surface.GetType() != GeomAbs_Plane: + raise ValueError("taper extrusion requires a planar profile") + normal = np.asarray(tuple(profile.get_normal_at()), dtype=float) + outer = BRepTools.OuterWire_s(profile.wrapped) + wires = [outer] + explorer = TopExp_Explorer(profile.wrapped, TopAbs_WIRE) + while explorer.More(): + wire = TopoDS.Wire_s(explorer.Current()) + if not wire.IsSame(outer): + wires.append(wire) + explorer.Next() + if angle == 0: + shape = BRepPrimAPI_MakePrism(profile.wrapped, gp_Vec(*displacement)).Shape() + elif np.linalg.norm(displacement / length - normal) < 1e-8 and normal[2] > 0 and len(wires) == 1 and angle > 0: + shape = LocOpe_DPrism(profile.wrapped, length / math.cos(math.radians(angle)), math.radians(angle)).Shape() + else: + # Offset in the profile plane before translating the final section. + move = gp_Trsf() + move.SetTranslation(gp_Vec(*displacement)) + solids = [] + for i, wire in enumerate(wires): + # Face inner wires wind opposite to the outer boundary. Normalize + # their winding before applying the signed 2-D offset so that + # flip_inner controls geometry rather than kernel wire orientation. + if i: + wire = TopoDS.Wire_s(wire.Reversed()) + offset = -length * math.tan(math.radians(angle)) * (-1 if i and flip_inner else 1) + builder = BRepOffsetAPI_MakeOffset(wire, GeomAbs_Intersection, False) + builder.Perform(offset) + if not builder.IsDone(): + raise ValueError("taper collapses a profile loop") + offset_shape = builder.Shape() + exp = TopExp_Explorer(offset_shape, TopAbs_WIRE) + found = [] + while exp.More(): + found.append(TopoDS.Wire_s(exp.Current())) + exp.Next() + if len(found) != 1: + raise ValueError("taper changes loop connectivity") + moved = BRepBuilderAPI_Transform(found[0], move, True).Shape() + loft = BRepOffsetAPI_ThruSections(True, False) + loft.AddWire(wire) + loft.AddWire(TopoDS.Wire_s(moved)) + loft.Build() + if not loft.IsDone(): + raise ValueError("kernel could not build tapered sections") + solids.append(loft.Shape()) + shape = solids[0] + for tool in solids[1:]: + cut = BRepAlgoAPI_Cut(shape, tool) + cut.Build() + if not cut.IsDone(): + raise ValueError("kernel could not subtract tapered inner loop") + shape = cut.Shape() + return _finish(profile, shape, "make_extrude_taper_rsolid", + {"vector": vector, "taper": taper, "flip_inner": flip_inner}, [profile]) + + +def sweep_multisection_rsolid( + profiles: Sequence[Face | Wire], + path: Wire, + is_frenet: bool = False, + binormal: Sequence[float] | Wire | None = None, +) -> Solid: + """Sweep through explicit ordered section wires along a continuous path. + + Face sections use their outer boundary, as in build123d's multi-section + pipe contract. Binormal may be a direction vector or auxiliary spine. + Each supplied section and path remains a graph input for replay. + """ + sections = list(profiles) + if len(sections) < 2: + raise ValueError("multi-section sweep requires at least two sections") + builder = BRepOffsetAPI_MakePipeShell(path.wrapped) + rotate = False + if isinstance(binormal, Wire): + builder.SetMode(binormal.wrapped, True) + elif binormal is not None: + values = _finite_vector(binormal, "binormal") + direction = get_current_cs().transform_vector(np.asarray(values)) + if np.linalg.norm(direction) < 1e-12: + raise ValueError("binormal direction must be nonzero") + builder.SetMode(gp_Dir(*direction)) + rotate = True + else: + builder.SetMode(bool(is_frenet)) + for section in sections: + if not isinstance(section, (Face, Wire)): + raise TypeError("sections must be Face or Wire") + wire = BRepTools.OuterWire_s(section.wrapped) if isinstance(section, Face) else section.wrapped + builder.Add(wire, False, rotate) + builder.Build() + if not builder.IsDone() or not builder.MakeSolid(): + raise ValueError("kernel could not make a solid multi-section sweep") + inputs = [*sections, path] + params = {"profile_count": len(sections), "is_frenet": bool(is_frenet), "binormal": None} + if isinstance(binormal, Wire): + inputs.append(binormal) + params["binormal_is_wire"] = True + elif binormal is not None: + params["binormal"] = binormal + return _finish(sections[0], builder.Shape(), "make_sweep_multisection_rsolid", params, inputs) + + +__all__ = ("extrude_taper_rsolid", "sweep_multisection_rsolid") diff --git a/src/simplecadapi/operators/sketch.py b/src/simplecadapi/operators/sketch.py index b70330bd..bf23c13d 100644 --- a/src/simplecadapi/operators/sketch.py +++ b/src/simplecadapi/operators/sketch.py @@ -326,7 +326,9 @@ def add_bspline_rsketch( """Add a B-spline curve entity to a sketch. The start/end point refs link the B-spline into a closed profile - loop. Control points may be literal 2-D coordinates or point refs. + loop. A closed curve may reuse one seam ref for both endpoints. + Control points may be literal 2-D coordinates or point refs. A periodic + seam links topology without replacing the periodic curve's poles. """ try: start_ref = _resolve_sketch_target(sketch, start, expected="point") diff --git a/src/simplecadapi/ql.py b/src/simplecadapi/ql.py index 28821558..d88cea5a 100644 --- a/src/simplecadapi/ql.py +++ b/src/simplecadapi/ql.py @@ -148,6 +148,15 @@ def _lookup_property(obj: Any, path: str) -> Any: gtype = _geom_type(obj) return gtype if gtype is not None else MISSING + if path.startswith("geom.bbox.") and hasattr(obj, "wrapped"): + from .kernel.ocp_properties import bounding_box + field = path.removeprefix("geom.bbox.") + names = {f"{side}.{axis}": axis + bound + for side, bound in (("min", "min"), ("max", "max")) for axis in "xyz"} + if field in names: + return float(getattr(bounding_box(obj.wrapped), names[field])) + return MISSING + if path == "geom.family": cls_name = obj.__class__.__name__.lower() if cls_name == "edge": @@ -1307,12 +1316,11 @@ def _boundary_items(scope: Any, target_kind: str) -> List[Any]: for child in scope.get_children() if child.__class__.__name__ == "Vertex" ] - if target_kind == "vertex" and hasattr(scope, "get_edges"): - vertices = [] - for edge in _boundary_items(scope, "edge"): - vertices.extend(_boundary_items(edge, "vertex")) - return _dedupe_items(vertices) - return [] + # Any scope with edges to walk: a wire, a face, a solid. + vertices = [] + for edge in _boundary_items(scope, "edge"): + vertices.extend(_boundary_items(edge, "vertex")) + return _dedupe_items(vertices) if target_kind == "solid": if cls_name == "Compound" and hasattr(scope, "get_solids"): @@ -1712,11 +1720,17 @@ def _resolve_scope_items(scope: Any, target_kind: str) -> List[Any]: ] if target_kind == "vertex": if hasattr(scope, "get_children"): - return [ + vertices = [ child for child in scope.get_children() if child.__class__.__name__ == "Vertex" ] + if vertices: + return vertices + # A wire, face or solid has vertices only through its edges. + vertices = _boundary_items(scope, "vertex") + if vertices: + return vertices if isinstance(scope, Iterable) and not isinstance(scope, (str, bytes, dict)): return list(scope) raise TypeError(f"cannot resolve QL selector scope for target_kind={target_kind}") diff --git a/src/simplecadapi/recording/serializer.py b/src/simplecadapi/recording/serializer.py index 94f0e2eb..c0e9515b 100644 --- a/src/simplecadapi/recording/serializer.py +++ b/src/simplecadapi/recording/serializer.py @@ -421,6 +421,14 @@ }, "translate_shape": {"status": "replayable", "op": "make_translate_rshape"}, "rotate_shape": {"status": "replayable", "op": "make_rotate_rshape"}, + "scale_rsolid": {"status": "replayable", "op": "make_scale_rsolid"}, + "transform_rsolid": {"status": "replayable", "op": "make_transform_rsolid"}, + "offset_rsolid": {"status": "replayable", "op": "make_offset_rsolid"}, + "boolean_rsolidlist": {"status": "replayable", "op": "make_boolean_rsolidlist"}, + "extrude_taper_rsolid": {"status": "replayable", "op": "make_extrude_taper_rsolid"}, + "sweep_multisection_rsolid": {"status": "replayable", "op": "make_sweep_multisection_rsolid"}, + "extrude_until_rsolid": {"status": "replayable", "op": "make_extrude_until_rsolid"}, + "draft_rsolid": {"status": "replayable", "op": "make_draft_rsolid"}, "mirror_shape": {"status": "replayable", "op": "make_mirror_rshape"}, "extrude_rsolid": {"status": "replayable", "op": "make_extrude_rsolid"}, "revolve_rsolid": {"status": "replayable", "op": "make_revolve_rsolid"}, @@ -597,6 +605,14 @@ "make_twisted_sweep_rsolid", "make_translate_rshape", "make_rotate_rshape", + "make_scale_rsolid", + "make_transform_rsolid", + "make_offset_rsolid", + "make_boolean_rsolidlist", + "make_extrude_taper_rsolid", + "make_sweep_multisection_rsolid", + "make_extrude_until_rsolid", + "make_draft_rsolid", "make_mirror_rshape", "make_cut_rsolid", "make_union_rsolid", @@ -2145,6 +2161,14 @@ def _ordered_input_shapes( return resolved +def _feature_input_shapes(ctx, graph, outputs, node, params): + # New features preserve order and output slots, including repeated inputs. + # Older graphs only carried distinct upstream nodes. + if "input_refs" in params: + return _ordered_input_shapes(ctx, graph, outputs, node, params) + return _all_input_outputs(ctx, outputs, node) + + def _semantic_view_origin( ctx: _ReplayContext, graph: OperationGraph, @@ -2910,7 +2934,7 @@ def _store_semantic_outputs(node, result: Any) -> None: if op_name == "make_part_rpart": ctx.require_params(node.node_id, op_name, params, ("part_id",)) - body_outputs = _input_outputs(ctx, outputs, node, 0) + body_outputs = _feature_input_shapes(ctx, graph, outputs, node, params) if body_outputs: result = ops.make_part_rpart( str(params["part_id"]), @@ -3404,8 +3428,11 @@ def _store_semantic_outputs(node, result: Any) -> None: ) source_outputs = _input_outputs(ctx, outputs, node, 0) if source_outputs: + source_slot = int(params.get("source_output_slot", 0)) + if source_slot < 0 or source_slot >= len(source_outputs): + ctx.fail(f"Graph node '{node.node_id}' selection source output slot {source_slot} is missing") result = _resolve_shape_from_geo_selector( - source_outputs[0], + source_outputs[source_slot], cast(Dict[str, Any], params["geo_selector"]), ) _store_outputs(node, result) @@ -3895,6 +3922,95 @@ def _store_semantic_outputs(node, result: Any) -> None: _store_outputs(node, result) continue + if op_name == "make_draft_rsolid": + ctx.require_params(node.node_id, op_name, params, ("angle", "selected_face_count", "plane_origin", "plane_normal")) + values = _input_outputs(ctx, outputs, node, 0) + if values: + solid = values[0] + faces = _resolve_feature_selection(ctx, node=node, solid=solid, + params=params, kind="face", outputs=outputs) + if ctx.strict and len(faces) != params["selected_face_count"]: + ctx.fail(f"Graph node '{node.node_id}' expected {params['selected_face_count']} draft faces, got {len(faces)}") + result = ops.draft_rsolid(solid=solid, faces_to_draft=faces, angle=params["angle"], + plane_origin=params["plane_origin"], plane_normal=params["plane_normal"]) + _store_outputs(node, result) + continue + + if op_name == "make_extrude_until_rsolid": + ctx.require_params(node.node_id, op_name, params, ("direction", "until", "target_count")) + count = int(params["target_count"]) + values = _feature_input_shapes(ctx, graph, outputs, node, params) + if len(values) != count + 1: + ctx.fail(f"Graph node '{node.node_id}' requires a profile and {count} ordered targets") + result = ops.extrude_until_rsolid(profile=values[0], target=values[1:], + direction=params["direction"], until=params["until"]) + _store_outputs(node, result) + continue + + if op_name == "make_extrude_taper_rsolid": + ctx.require_params(node.node_id, op_name, params, ("vector", "taper")) + input_values = _feature_input_shapes(ctx, graph, outputs, node, params) + if input_values: + result = ops.extrude_taper_rsolid(profile=input_values[0], vector=params["vector"], + taper=params["taper"], flip_inner=params.get("flip_inner", True)) + _store_outputs(node, result) + continue + + if op_name == "make_sweep_multisection_rsolid": + ctx.require_params(node.node_id, op_name, params, ("profile_count", "is_frenet")) + count = int(params["profile_count"]) + values = _feature_input_shapes(ctx, graph, outputs, node, params) + expected = count + 1 + int(bool(params.get("binormal_is_wire"))) + if len(values) != expected: + ctx.fail(f"Graph node '{node.node_id}' requires {expected} ordered sweep inputs") + binormal = values[count + 1] if params.get("binormal_is_wire") else params.get("binormal") + result = ops.sweep_multisection_rsolid(profiles=values[:count], path=values[count], + is_frenet=params["is_frenet"], binormal=binormal) + _store_outputs(node, result) + continue + + if op_name == "make_transform_rsolid": + ctx.require_params(node.node_id, op_name, params, ("matrix",)) + inputs = _feature_input_shapes(ctx, graph, outputs, node, params) + if inputs: + result = ops.transform_rsolid(solid=inputs[0], matrix=params["matrix"]) + _store_outputs(node, result) + continue + + if op_name == "make_scale_rsolid": + ctx.require_params(node.node_id, op_name, params, ("factors", "center")) + inputs = _feature_input_shapes(ctx, graph, outputs, node, params) + if inputs: + result = ops.scale_rsolid(solid=inputs[0], factors=params["factors"], center=params["center"]) + _store_outputs(node, result) + continue + + if op_name == "make_offset_rsolid": + ctx.require_params(node.node_id, op_name, params, ("amount", "removed_face_count")) + inputs = _feature_input_shapes(ctx, graph, outputs, node, params) + if inputs: + solid = inputs[0] + faces = _resolve_feature_selection(ctx, node=node, solid=solid, + params=params, kind="face", outputs=outputs) if params["removed_face_count"] else [] + if ctx.strict and len(faces) != params["removed_face_count"]: + ctx.fail(f"Graph node '{node.node_id}' expected {params['removed_face_count']} opening faces, got {len(faces)}") + result = ops.offset_rsolid(solid=solid, amount=params["amount"], openings=faces, + tolerance=params.get("tolerance", 1e-4), kind=params.get("kind", "ARC")) + _store_outputs(node, result) + continue + + if op_name == "make_boolean_rsolidlist": + ctx.require_params(node.node_id, op_name, params, + ("operation", "left_count", "right_count", "input_refs")) + inputs = _ordered_input_shapes(ctx, graph, outputs, node, params) + count, right_count = int(params["left_count"]), int(params["right_count"]) + if count < 0 or right_count < 0 or len(inputs) != count + right_count: + ctx.fail(f"Graph node '{node.node_id}' boolean input counts do not match") + result = ops.boolean_rsolidlist(left=inputs[:count], right=inputs[count:], + operation=params["operation"]) + _store_outputs(node, result) + continue + if op_name == "make_extrude_rsolid": ctx.require_params( node.node_id, op_name, params, ("direction", "distance") diff --git a/src/simplecadapi/runtime/_marimo.py b/src/simplecadapi/runtime/_marimo.py index 971161f8..2760aea5 100644 --- a/src/simplecadapi/runtime/_marimo.py +++ b/src/simplecadapi/runtime/_marimo.py @@ -78,7 +78,7 @@ def load_notebook(path: Path, *, cell_prefix: str) -> App | None: marimo compiles each cell under a file name made of its cell id, and registers the cell's source in :mod:`linecache` under that name. Cell ids come from a fixed seed, so every notebook has the same ones: two - notebooks loaded into one process (a ``use``\ d child, or a headless + notebooks loaded into one process (a child run by ``use``, or a headless run inside the editor kernel) would show each other's source in tracebacks, source positions and restored functions. A prefix per notebook keeps them apart, as marimo's own prefix for nested apps does. diff --git a/src/simplecadapi/runtime/cache.py b/src/simplecadapi/runtime/cache.py new file mode 100644 index 00000000..8cb3f083 --- /dev/null +++ b/src/simplecadapi/runtime/cache.py @@ -0,0 +1,239 @@ +"""The cell cache: marimo's, kept per cell. + +marimo keys a cell's cache entry by the cell's code and the values it reads. +It cannot see what a SimpleCAD notebook also depends on — local modules, child +notebooks, project input files — so an entry is also stored under a digest of +those files (:mod:`.deps`). The digest used to be one for the whole notebook: +any change to any such file discarded every cell's entries. It is now one +per cell, over only the files that cell depends on (:func:`.deps.cell_module_files`), +so editing ``dimensions.py`` recomputes the cells that read the dimensions and +restores the rest, and a cell downstream of a recomputed one is restored again +if the values it reads came out the same. + +The same cache serves a headless run (:func:`~.runner.run_notebook`, ``sca +run``) and the editor kernel (:mod:`.executor`), so what one computed the +other restores. + +Layout: ``/cache//`` holds the entries of every cell whose +digest it is. :func:`prune` removes the directories the last two runs did not +use, so going back to a previous value is still instant. +""" + +from __future__ import annotations + +import hashlib +import json +import shutil +from collections.abc import Callable, Mapping +from pathlib import Path +from typing import Any + +from ._marimo import ( + CachedLifecycle, + CellId_t, + CellImpl, + DirectedGraph, + FileStore, + LazyLoader, + LazyStore, + MutableGlobals, + RunResult, + Skip, +) +from .config import NotebookConfig +from .deps import ( + CellSource, + ancestors, + cell_digest, + cell_module_files, + read_dependencies, + recorded_dependents, + state_dir, +) + +_CELLS = "cells" +"""The cell cache's loader name, fixed so a cache directory can be renamed: +its entries reference their values by paths starting with this name.""" + +_RUNS = "_runs.json" + + +def cache_root(config: NotebookConfig) -> Path: + """The notebook's cell caches: one directory per dependency digest.""" + + return state_dir(config) / "cache" + + +class CellDigests: + """The digest of each cell of a notebook, from what it depends on. + + *saved* are values an editor applies to variables after the cell that + defines them ran (:mod:`.params`). They are not in any cell's code, and + marimo keys a cell that reads an object by the code of the cells that made + it, so every cell downstream of a saved value carries it in its digest. + With *kernel*, cells that depend on what a run records (child notebooks) + have no digest: only a headless run records them, so the kernel runs those + cells live (the child notebooks themselves are cached). + """ + + def __init__( + self, + config: NotebookConfig, + cells: list[CellImpl], + setup: CellImpl | None, + *, + saved: Mapping[str, Any] | None = None, + kernel: bool = False, + ) -> None: + self.config = config + self.setup_id = setup.cell_id if setup is not None else None + sources = [ + CellSource(cell.cell_id, cell.code, frozenset(cell.refs), frozenset(cell.defs)) + for cell in cells + if cell.cell_id != self.setup_id + ] + self._files = cell_module_files( + config, + setup.code if setup is not None else None, + sources, + read_dependencies(config), + ) + # A cell that runs a child notebook, or reads what one makes, depends + # on what the last run recorded; its entries are kept apart so they + # can follow the record. + self._uses = recorded_dependents(sources) + self._kernel = kernel + self._saved_tag: dict[str, str] = {} + if saved: + reached = ancestors(sources) + own = {source.key: source.defs & saved.keys() for source in sources} + for source in sources: + names = set(own[source.key]).union(*(own[key] for key in reached[source.key])) + if names: + payload = json.dumps({name: saved[name] for name in sorted(names)}, sort_keys=True) + self._saved_tag[source.key] = "p" + hashlib.sha256(payload.encode()).hexdigest()[:12] + + def digest(self, cell_id: CellId_t) -> str | None: + """The digest for *cell_id*, ``None`` for a cell that is not cached.""" + + files = self._files.get(cell_id) + if files is None: + return None # the setup cell + if self._kernel and cell_id in self._uses: + return None + tag = ("use" if cell_id in self._uses else "") + self._saved_tag.get(cell_id, "") + return cell_digest(self.config, files, tag=tag) + + +class CellCache(CachedLifecycle): + """marimo's cell cache, a store per digest, reporting its hits.""" + + def __init__( + self, + graph: DirectedGraph, + root: Path, + digest_for: Callable[[CellImpl], str | None], + ) -> None: + # CachedLifecycle only takes a loader kind and stores in marimo's + # default place; its loader is replaced by one stored under *root* + # (the pickle kind is merely the cheapest to build and discard). + super().__init__(graph, loader="pickle") + self._root = root + self._digest_for = digest_for + self._stores: dict[str, LazyLoader] = {} + self._cell_store: dict[CellId_t, LazyLoader] = {} + self._active: dict[CellId_t, LazyLoader] = {} + self.restored: set[CellId_t] = set() + self._used: dict[str, None] = {} + + @property + def used(self) -> set[str]: + """Digests of the cells this lifecycle has run or restored.""" + + return set(self._used) + + def recent(self, limit: int) -> set[str]: + """The *limit* digests used most recently.""" + + return set(list(self._used)[-limit:]) + + def _store(self, digest: str) -> LazyLoader: + if digest not in self._stores: + self._stores[digest] = LazyLoader( + name=_CELLS, store=LazyStore(FileStore(str(self._root / digest))) + ) + return self._stores[digest] + + def setup(self, cell: CellImpl, glbls: MutableGlobals) -> Skip | None: + digest = self._digest_for(cell) + if digest is None: + return None # an uncached cell runs live + loader = self._store(digest) + self._used.pop(digest, None) + self._used[digest] = None # most recently used last + self._loader = loader + self._cell_store[cell.cell_id] = loader + self._active[cell.cell_id] = loader + decision = super().setup(cell, glbls) + if isinstance(decision, Skip): + self.restored.add(cell.cell_id) + return decision + + def teardown(self, cell: CellImpl, glbls: MutableGlobals, run_result: RunResult) -> None: + loader = self._active.pop(cell.cell_id, None) + if loader is None: + return + self._loader = loader + super().teardown(cell, glbls, run_result) + + def _invalidate(self, cell_id: CellId_t) -> None: + # The producer's entry lives in the store of its own digest. + loader = self._cell_store.get(cell_id) + if loader is None: + super()._invalidate(cell_id) + return + current, self._loader = self._loader, loader + try: + super()._invalidate(cell_id) + finally: + self._loader = current + + def move(self, old: str, new: str) -> None: + """Rename the store of digest *old* to *new* (a cell whose dependencies became known).""" + + source = self._root / old + target = self._root / new + self._stores.pop(old, None) + if source != target and source.is_dir(): + shutil.rmtree(target, ignore_errors=True) + source.rename(target) + + def flush(self) -> None: + """Wait until every entry is written (they are pickled on background threads).""" + + for loader in self._stores.values(): + loader.flush() + + +def prune(root: Path, used: set[str]) -> None: + """Keep the stores this run used and the last run's; remove the rest.""" + + if not root.is_dir(): + return + runs = root / _RUNS + try: + previous = set(json.loads(runs.read_text(encoding="utf-8"))) + except (OSError, ValueError): + previous = set() + keep = used | previous + for entry in root.iterdir(): + if entry.name == _RUNS or entry.name in keep: + continue + if entry.is_dir(): + shutil.rmtree(entry, ignore_errors=True) + else: + entry.unlink(missing_ok=True) + runs.write_text(json.dumps(sorted(used)), encoding="utf-8") + + +__all__ = ["CellCache", "CellDigests", "cache_root", "prune"] diff --git a/src/simplecadapi/runtime/cells.py b/src/simplecadapi/runtime/cells.py index b6fced29..36b7bf53 100644 --- a/src/simplecadapi/runtime/cells.py +++ b/src/simplecadapi/runtime/cells.py @@ -85,6 +85,14 @@ def __init__(self, path: Path, cells: Iterable[CellImpl]) -> None: self._spans.setdefault(filename, []).append(span) self._placements: dict[str, CellPlacement] | None = None + def __contains__(self, cell_id: object) -> bool: + """Whether *cell_id* is a cell of the notebook. + + The editor kernel also runs cells that are not (its scratch cell, + for one); they are not recorded. + """ + return cell_id in self._keys + def key(self, cell_id: CellId_t) -> str: """Return the key of the cell marimo calls *cell_id*.""" diff --git a/src/simplecadapi/runtime/cli.py b/src/simplecadapi/runtime/cli.py index 25409b32..8765b6d7 100644 --- a/src/simplecadapi/runtime/cli.py +++ b/src/simplecadapi/runtime/cli.py @@ -7,6 +7,8 @@ sca run link_bar.py --id crank # run one member of a part family sca run bracket.py --out bracket.scadpkg # also write the product package sca run bracket.py --no-cache # ignore the cell cache + sca ls [DIR] # the notebooks under DIR + sca params bracket.py # features and parameters The report is one JSON object on stdout. """ @@ -21,6 +23,7 @@ from ..product.packages import build_product_package, encode_product_package from .runner import run_notebook +from .workspace import describe_notebook, scan_modules, scan_workspace def configure(subparsers: argparse._SubParsersAction) -> None: @@ -51,6 +54,23 @@ def configure(subparsers: argparse._SubParsersAction) -> None: parser.add_argument("--out", type=Path, help="write the product package here") parser.set_defaults(handler=_execute, printer=_print_report) + listing = subparsers.add_parser( + "ls", + help="list the SimpleCAD notebooks under a directory", + description="List the notebooks under DIR whose script header has " + "[tool.simplecadapi]: id, kind, the notebooks each uses, and the local " + "modules (shared dimensions, helpers) their cells import.", + ) + listing.add_argument("directory", type=Path, nargs="?", default=Path.cwd()) + listing.set_defaults(handler=_list, printer=_print_report) + + describe = subparsers.add_parser( + "params", + help="list a notebook's features and parameters (without running it)", + ) + describe.add_argument("notebook", type=Path, help="marimo notebook (.py)") + describe.set_defaults(handler=_describe, printer=_print_report) + def parse_overrides(items: Sequence[str]) -> dict[str, Any]: """Parse ``NAME=VALUE`` items; ``VALUE`` is a literal or else a string.""" @@ -97,6 +117,25 @@ def _execute(args: argparse.Namespace) -> tuple[dict[str, Any], int]: return report, 0 +def _list(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + root = args.directory.expanduser().resolve() + if not root.is_dir(): + raise ValueError(f"{args.directory}: not a directory") + entries = scan_workspace(root) + return { + "root": str(root), + "notebooks": [entry.to_dict() for entry in entries], + "modules": [module.to_dict() for module in scan_modules(root, entries)], + }, 0 + + +def _describe(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + path = args.notebook.expanduser().resolve() + if not path.is_file(): + raise ValueError(f"{args.notebook}: no such file") + return {"notebook": str(path), **describe_notebook(path)}, 0 + + def _print_report(report: dict[str, Any]) -> None: print(json.dumps(report, sort_keys=True)) diff --git a/src/simplecadapi/runtime/deps.py b/src/simplecadapi/runtime/deps.py index 26aa842e..fdc2d547 100644 --- a/src/simplecadapi/runtime/deps.py +++ b/src/simplecadapi/runtime/deps.py @@ -29,9 +29,11 @@ import os import sys import threading -from collections.abc import Iterable, Iterator, Mapping +import time +from collections.abc import Iterable, Iterator, Mapping, Sequence from contextlib import contextmanager from contextvars import ContextVar +from dataclasses import dataclass from pathlib import Path from types import ModuleType @@ -130,11 +132,19 @@ def local_modules(config: NotebookConfig, codes: Iterable[str]) -> dict[str, Pat and marimo's output directory never count. """ - root = config.directory - output = notebook_output_dir(config.path) found: dict[str, Path] = {} # (source, package its relative imports resolve against) - pending: list[tuple[str, str]] = [(code, "") for code in codes] + _follow_imports(config, [(code, "") for code in codes], found) + return found + + +def _follow_imports( + config: NotebookConfig, pending: list[tuple[str, str]], found: dict[str, Path] +) -> None: + """Add to *found* every local module the imports in *pending* sources reach.""" + + root = config.directory + output = notebook_output_dir(config.path) while pending: source, package = pending.pop() for imported in _imported_names(source, package): @@ -148,7 +158,6 @@ def local_modules(config: NotebookConfig, codes: Iterable[str]) -> dict[str, Pat continue # still a dependency; its imports are unknown is_package = path.name == "__init__.py" pending.append((text, name if is_package else name.rpartition(".")[0])) - return found _loaded: dict[str, tuple[Path, str | None]] = {} @@ -176,7 +185,7 @@ def refresh_local_modules(modules: Mapping[str, Path]) -> None: with _loaded_lock: for name, path in modules.items(): - state = (path, _file_digest(path)) + state = (path, file_digest(path)) module = sys.modules.get(name) if module is not None and _loaded.get(name) != state and _replaceable(name, module): for loaded in [item for item in sys.modules if item == name or item.startswith(name + ".")]: @@ -263,6 +272,234 @@ def _module_files(root: Path, name: str) -> list[tuple[str, Path]]: return files +# --------------------------------------------------------------------------- +# What each cell depends on +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class CellSource: + """A cell as the dependency analysis needs it.""" + + key: str + code: str + refs: frozenset[str] + """The names the cell reads (marimo's ``cell.refs``).""" + defs: frozenset[str] + """The names the cell defines (marimo's ``cell.defs``).""" + + +def cell_module_files( + config: NotebookConfig, + setup: str | None, + cells: Sequence[CellSource], + recorded: Iterable[Path] = (), +) -> dict[str, frozenset[Path]]: + """Return, per cell key, the local files whose content its result depends on. + + A cell depends on a local module when it reads a name that an ``import`` + in the notebook (the setup cell, or any cell) bound to that module, or + calls a function the notebook defines (an ``@app.function`` cell, a + ``def`` in the setup cell) that reads such a name; and on every local + module that module imports in turn. It also depends on whatever the cells + it reads from depend on: marimo keys a cell that reads an object (a solid, + a part) by the code of the cells that made it, not by its value, so it + cannot see a module that changed upstream. A cell that calls ``use`` also + depends on everything the last run recorded (the child notebooks, which + only a run can name, and what they depend on): *recorded*. + + Reading a name never imported (a standard-library or installed module) adds + nothing: those are pinned by the software versions in the digest. + """ + + root = config.directory + sources = ([setup] if setup else []) + [cell.code for cell in cells] + bound: dict[str, set[tuple[str, Path]]] = {} + for source in sources: + for name, modules in _import_bindings(root, source).items(): + bound.setdefault(name, set()).update(modules) + star = bound.pop("*", set()) + + closures: dict[tuple[str, Path], frozenset[Path]] = {} + + def closure(module: tuple[str, Path]) -> frozenset[Path]: + if module not in closures: + name, path = module + found: dict[str, Path] = {name: path} + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError): + text = "" + package = name if path.name == "__init__.py" else name.rpartition(".")[0] + _follow_imports(config, [(text, package)], found) + closures[module] = frozenset(found.values()) + return closures[module] + + # Names each function or class of the notebook reads. + reads: dict[str, set[str]] = {} + for source in sources: + for name, loaded in _definitions(source).items(): + reads.setdefault(name, set()).update(loaded) + + def reach(name: str, seen: set[str]) -> set[str]: + """The imported names the notebook function *name* reads, through other functions too.""" + if name in seen: + return set() + seen.add(name) + imported: set[str] = set() + for read in reads.get(name, ()): + if read in bound: + imported.add(read) + elif read in reads: + imported |= reach(read, seen) + return imported + + star_files = frozenset(path for module in star for path in closure(module)) + recorded_files = frozenset(recorded) + result: dict[str, frozenset[Path]] = {} + for cell in cells: + names = set(cell.refs) | (cell.defs & bound.keys()) | _loaded_names(cell.code) + needed: set[str] = set() + for name in names: + if name in bound: + needed.add(name) + if name in reads: + needed |= reach(name, set()) + for name in cell.defs & reads.keys(): + needed |= reach(name, set()) + files: set[Path] = set(star_files) + for name in needed: + for module in bound.get(name, ()): + files |= closure(module) + if calls_use(cell.code): + files |= recorded_files + result[cell.key] = frozenset(files) + + # marimo keys a cell that reads an object (a solid, a part) by the code of + # the cells that made it, not by its value: a file an ancestor depends on + # is one this cell depends on. + reached = ancestors(cells) + return { + cell.key: frozenset(result[cell.key].union(*(result[key] for key in reached[cell.key]))) + for cell in cells + } + + +def ancestors(cells: Sequence[CellSource]) -> dict[str, frozenset[str]]: + """Per cell key, the keys of every cell it reads from, directly or not.""" + + producers = {name: cell.key for cell in cells for name in cell.defs} + parents = { + cell.key: {producers[name] for name in cell.refs if name in producers} - {cell.key} + for cell in cells + } + found: dict[str, frozenset[str]] = {} + + def of(key: str, stack: frozenset[str] = frozenset()) -> frozenset[str]: + if key not in found: + reached: set[str] = set() + for parent in parents[key] - stack - {key}: + reached.add(parent) + reached |= of(parent, stack | {key}) + found[key] = frozenset(reached) + return found[key] + + return {cell.key: of(cell.key) for cell in cells} + + +def recorded_dependents(cells: Sequence[CellSource]) -> frozenset[str]: + """The keys of the cells that depend on the files a run records. + + Those are the cells that call ``use`` and every cell that reads, however + indirectly, what such a cell makes (see :func:`cell_module_files`). + """ + + direct = {cell.key for cell in cells if calls_use(cell.code)} + reached = ancestors(cells) + return frozenset(key for key in reached if key in direct or reached[key] & direct) + + +def _import_bindings(root: Path, source: str) -> dict[str, set[tuple[str, Path]]]: + """The local modules each name an ``import`` in *source* binds comes from. + + ``from m import a`` binds ``a`` to ``m`` (and to ``m.a`` when that is a + submodule); ``import m.n`` binds ``m``; a star import binds an unknown set + of names, recorded under ``"*"``. + """ + + try: + tree = ast.parse(source) + except (SyntaxError, ValueError): + return {} + bindings: dict[str, set[tuple[str, Path]]] = {} + + def bind(name: str, modules: Iterable[tuple[str, Path]]) -> None: + found = set(modules) + if found: + bindings.setdefault(name, set()).update(found) + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + bind(alias.asname or alias.name.split(".")[0], _module_files(root, alias.name)) + elif isinstance(node, ast.ImportFrom): + base = _absolute(node.module, node.level, "") + if base is None: + continue + for alias in node.names: + modules = list(_module_files(root, base)) + if alias.name == "*": + bind("*", modules) + else: + modules += _module_files(root, f"{base}.{alias.name}") + bind(alias.asname or alias.name, modules) + return bindings + + +def _definitions(source: str) -> dict[str, set[str]]: + """The names each top-level function and class in *source* reads.""" + + try: + tree = ast.parse(source) + except (SyntaxError, ValueError): + return {} + return { + node.name: _loaded_names_of(node) + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + } + + +def _loaded_names(source: str) -> set[str]: + try: + return _loaded_names_of(ast.parse(source)) + except (SyntaxError, ValueError): + return set() + + +def _loaded_names_of(node: ast.AST) -> set[str]: + return { + child.id + for child in ast.walk(node) + if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load) + } + + +def calls_use(source: str) -> bool: + try: + tree = ast.parse(source) + except (SyntaxError, ValueError): + return True # cannot tell: depend on everything + for node in ast.walk(tree): + if isinstance(node, ast.Call): + function = node.func + if (isinstance(function, ast.Name) and function.id == "use") or ( + isinstance(function, ast.Attribute) and function.attr == "use" + ): + return True + return False + + # --------------------------------------------------------------------------- # The digest # --------------------------------------------------------------------------- @@ -272,14 +509,15 @@ def input_files(config: NotebookConfig) -> frozenset[Path]: return frozenset((config.directory / item.path).resolve() for item in config.inputs) -def dependency_digest(config: NotebookConfig, files: Iterable[Path]) -> str: +def dependency_digest(config: NotebookConfig, files: Iterable[Path], *, tag: str = "") -> str: """Return *D* for *files* and the inputs as they are now: 16 hex digits. - A missing file digests as missing, so deleting one invalidates too. + A missing file digests as missing, so deleting one invalidates too. A + *tag* keeps digests of different kinds of cell apart. """ contents = { - _display(config, path): _file_digest(path) + _display(config, path): file_digest(path) for path in set(files) | input_files(config) } payload = { @@ -288,14 +526,51 @@ def dependency_digest(config: NotebookConfig, files: Iterable[Path]) -> str: "marimo": marimo.__version__, "files": contents, } + if tag: + payload["tag"] = tag return hashlib.sha256(canonical_bytes(payload)).hexdigest()[:16] -def _file_digest(path: Path) -> str | None: +def cell_digest(config: NotebookConfig, files: Iterable[Path], *, tag: str = "") -> str: + """The digest a cell's cache entries are stored under: 16 hex digits. + + Like :func:`dependency_digest`, but over the files one cell depends on (see + :func:`cell_module_files`); the project inputs are every cell's. + """ + + return dependency_digest(config, files, tag=tag) + + +_digests: dict[Path, tuple[int, int, str | None]] = {} +_digests_lock = threading.Lock() + + +def file_digest(path: Path) -> str | None: + """The content digest of *path*; ``None`` if it cannot be read. + + Remembered per (mtime, size): the editor kernel asks for the digests of + the same few files for every cell it runs. + """ + try: - return hashlib.sha256(path.read_bytes()).hexdigest() + stat = path.stat() except OSError: return None + signature = (stat.st_mtime_ns, stat.st_size) + # A file touched a moment ago may be edited again within the clock's + # resolution without its (mtime, size) changing: read those every time. + settled = time.time_ns() - stat.st_mtime_ns > 2_000_000_000 + with _digests_lock: + known = _digests.get(path) + if settled and known is not None and known[:2] == signature: + return known[2] + try: + digest: str | None = hashlib.sha256(path.read_bytes()).hexdigest() + except OSError: + digest = None + with _digests_lock: + _digests[path] = (*signature, digest) + return digest def _display(config: NotebookConfig, path: Path) -> str: @@ -308,7 +583,14 @@ def _display(config: NotebookConfig, path: Path) -> str: __all__ = [ + "CellSource", + "ancestors", + "calls_use", + "cell_digest", + "cell_module_files", "dependency_digest", + "file_digest", + "recorded_dependents", "deps_file", "input_files", "local_modules", diff --git a/src/simplecadapi/runtime/executor.py b/src/simplecadapi/runtime/executor.py index 9a3f8b5f..d3e3900d 100644 --- a/src/simplecadapi/runtime/executor.py +++ b/src/simplecadapi/runtime/executor.py @@ -25,10 +25,12 @@ from __future__ import annotations +import os from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass +from pathlib import Path from typing import Any from ..recording.graph import GraphSession @@ -37,12 +39,16 @@ CellImpl, DefaultExecutor, Executor, + MarimoRescheduleError, MarimoRuntimeException, MutableGlobals, + RunResult, safe_get_context, ) +from .cache import CellCache, CellDigests, cache_root, prune from .cells import NotebookCells, bound_requirements from .config import NotebookConfig, read_notebook_config +from .params import apply_saved_params, read_saved_params class UnboundRequirementError(RuntimeError): @@ -59,6 +65,8 @@ class NotebookScope: config: NotebookConfig cells: NotebookCells + graph: Any = None + """marimo's dataflow graph of the notebook, known when marimo itself runs it.""" _running: ContextVar[NotebookScope | None] = ContextVar( @@ -88,7 +96,9 @@ class SimpleCadExecutor: Pass *scope* when the caller knows the notebook (a headless run, which may be nested inside another notebook's kernel). Without it the executor looks the notebook up from marimo's runtime context for each - cell, which is what the editor kernel needs. + cell, which is what the editor kernel needs. The kernel's cells go + through the same cell cache a headless run uses (:mod:`.cache`); a headless + run has its own lifecycle for that. """ name = "simplecadapi" @@ -99,17 +109,141 @@ def __init__(self, scope: NotebookScope | None = None) -> None: def execute_cell(self, cell: CellImpl, glbls: MutableGlobals) -> Any: scope = self._scope or _context_scope() - if scope is None: - return self._delegate.execute_cell(cell, glbls) - with _cell_recording(scope, cell, glbls): + if scope is None or cell.cell_id not in scope.cells: return self._delegate.execute_cell(cell, glbls) + cache = self._cache_for(scope, cell) + restored = _restore(cache, cell, glbls) + if restored is not _MISS: + return restored + cache = cache if restored is _MISS else None + try: + with _cell_recording(scope, cell, glbls): + result = self._delegate.execute_cell(cell, glbls) + self._apply_saved(scope, cell, glbls) + except BaseException as error: + _store(cache, cell, glbls, None, error) + raise + _store(cache, cell, glbls, result, None) + return result async def execute_cell_async(self, cell: CellImpl, glbls: MutableGlobals) -> Any: scope = self._scope or _context_scope() - if scope is None: - return await self._delegate.execute_cell_async(cell, glbls) - with _cell_recording(scope, cell, glbls): + if scope is None or cell.cell_id not in scope.cells: return await self._delegate.execute_cell_async(cell, glbls) + cache = self._cache_for(scope, cell) + restored = _restore(cache, cell, glbls) + if restored is not _MISS: + return restored + try: + with _cell_recording(scope, cell, glbls): + result = await self._delegate.execute_cell_async(cell, glbls) + self._apply_saved(scope, cell, glbls) + except BaseException as error: + _store(cache, cell, glbls, None, error) + raise + _store(cache, cell, glbls, result, None) + return result + + def _apply_saved( + self, scope: NotebookScope, cell: CellImpl, glbls: MutableGlobals + ) -> None: + # Only the editor kernel (no scope given) takes the values an editor + # saved; a headless run takes the overrides it is passed (see params). + if self._scope is None: + apply_saved_params(scope.config, set(cell.defs), glbls) + + def _cache_for(self, scope: NotebookScope, cell: CellImpl) -> CellCache | None: + """The cell cache of the editor kernel's notebook; ``None`` where there is none.""" + + if self._scope is not None or scope.graph is None or os.environ.get("SCA_NO_KERNEL_CACHE"): + return None + return _kernel_cache(scope, cell) + + +_MISS = object() +"""Returned by :func:`_restore` when the cell must run.""" + + +def _restore(cache: CellCache | None, cell: CellImpl, glbls: MutableGlobals) -> Any: + """Restore *cell* from the cache: its output, or ``_MISS`` if it has to run.""" + + if cache is None: + return _MISS + try: + skip = cache.setup(cell, glbls) + except MarimoRescheduleError: + # A value the cache could not restore: run the cell live, store nothing. + cache._active.pop(cell.cell_id, None) + return _MISS + if skip is None: + return _MISS + return skip.result.output + + +def _store( + cache: CellCache | None, + cell: CellImpl, + glbls: MutableGlobals, + output: Any, + error: BaseException | None, +) -> None: + if cache is not None: + cache.teardown(cell, glbls, RunResult(output=output, exception=error)) + + +@dataclass +class _KernelCache: + graph: Any + signature: tuple + digests: CellDigests + lifecycle: CellCache + + +_kernel_caches: dict[Path, _KernelCache] = {} +_KEEP_DIGESTS = 64 +"""How many of the most recently used digests the kernel keeps on disk.""" + + +def _kernel_cache(scope: NotebookScope, cell: CellImpl) -> CellCache | None: + """The cell cache for the notebook marimo is running; rebuilt when its cells change.""" + + if cell.cell_id == "setup": + return None # like marimo, never cache the setup cell + config = scope.config + graph = scope.graph + cells = list(graph.cells.values()) + saved = read_saved_params(config) + signature = ( + tuple((item.cell_id, item.code) for item in cells), + tuple(sorted(saved.items())), + ) + entry = _kernel_caches.get(config.path) + if entry is not None and entry.graph is graph and entry.signature == signature: + return entry.lifecycle + digests = CellDigests(config, cells, graph.cells.get("setup"), saved=saved, kernel=True) + if entry is not None and entry.graph is graph: + entry.signature, entry.digests = signature, digests + return entry.lifecycle + entry = _KernelCache(graph=graph, signature=signature, digests=digests, lifecycle=None) # type: ignore[arg-type] + entry.lifecycle = CellCache( + graph, cache_root(config), lambda item: entry.digests.digest(item.cell_id) + ) + _kernel_caches[config.path] = entry + return entry.lifecycle + + +def finish_kernel_cache(config: NotebookConfig) -> None: + """Write out the kernel's pending cache entries and drop the old ones. + + The cache pickles values on background threads, and projecting the product + modifies it: flush first. Called by :func:`.live.live_package`. + """ + + entry = _kernel_caches.get(config.path) + if entry is None: + return + entry.lifecycle.flush() + prune(cache_root(config), entry.lifecycle.recent(_KEEP_DIGESTS)) def executor_factory() -> Executor: @@ -127,7 +261,9 @@ def _context_scope() -> NotebookScope | None: config = read_notebook_config(context.filename) if config is None: return None - return NotebookScope(config, NotebookCells(config.path, context.graph.cells.values())) + return NotebookScope( + config, NotebookCells(config.path, context.graph.cells.values()), graph=context.graph + ) @contextmanager @@ -178,5 +314,6 @@ def _check_bound(session: GraphSession, cell: CellImpl, glbls: MutableGlobals) - "SimpleCadExecutor", "UnboundRequirementError", "executor_factory", + "finish_kernel_cache", "notebook_id", ] diff --git a/src/simplecadapi/runtime/live.py b/src/simplecadapi/runtime/live.py new file mode 100644 index 00000000..4e323688 --- /dev/null +++ b/src/simplecadapi/runtime/live.py @@ -0,0 +1,171 @@ +"""The product of a notebook running in an editor's marimo kernel. + +Headless, :func:`~.runner.run_notebook` runs the cells and projects the +product. In the editor marimo runs them, reactively, and keeps their +values in the kernel; :func:`live_package` projects the product from +those. Same executor, same projection, same package: a notebook gives the +same product wherever it runs. + +The editor's bridge calls :func:`publish_live` from a snippet it executes +in the kernel once the cells have settled. +""" + +from __future__ import annotations + +import urllib.error +import urllib.request +from dataclasses import dataclass +from typing import Any + +from ..artifacts.assembly_io import attached_definition +from ..product.packages import build_product_package, encode_product_package +from ..scene.preview import PreviewScene, compile_preview, encode_preview, entity_info +from ._marimo import safe_get_context +from .cells import NotebookCells +from .config import read_notebook_config +from .executor import finish_kernel_cache +from .projection import find_product, project_product + +PACKAGE_MEDIA_TYPE = "application/vnd.simplecad.scadpkg" +PREVIEW_MEDIA_TYPE = "application/vnd.simplecad.preview" + + +class LiveUnavailableError(RuntimeError): + """No SimpleCAD notebook is running in this kernel.""" + + +@dataclass(frozen=True) +class LivePackage: + data: bytes + """The ``.scadpkg``.""" + definition_id: str + definition_kind: str + content_hash: str + + +def live_package(*, validate: bool = False) -> LivePackage: + """Project the running notebook's product and package it. + + Call from inside the marimo kernel. Raises :class:`LiveUnavailableError` + outside one, and :class:`~.projection.ProductNotFoundError` while no + cell has produced the product yet (or a cell failed). ``validate`` + runs the package's full contract validation; the live path skips it. + """ + + context = safe_get_context() + if context is None or context.filename is None: + raise LiveUnavailableError("not running inside a marimo kernel") + config = read_notebook_config(context.filename) + if config is None: + raise LiveUnavailableError( + f"{context.filename}: not a SimpleCAD notebook (no [tool.simplecadapi])" + ) + finish_kernel_cache(config) # the product is modified below: let the cache finish pickling it + glbls = context.globals + ordered = list(context.graph.cells.values()) + cells = NotebookCells(config.path, ordered) + values = { + name: glbls[name] for cell in ordered for name in cell.defs if name in glbls + } + read = frozenset().union(*(cell.refs for cell in ordered)) + product = project_product( + find_product(config, values, read), + config=config, + cells=cells, + values=values.values(), + ) + definition = attached_definition(product) + # project_product attaches one to every product it returns. + assert definition is not None + package = build_product_package(definition, validate=validate) + return LivePackage( + data=encode_product_package(package), + definition_id=definition.definition_id, + definition_kind=definition.definition_kind, + content_hash=definition.content_hash, + ) + + +_last_preview: PreviewScene | None = None +"""The scene last previewed in this kernel: what :func:`preview_entity_info` answers about.""" + + +def live_preview() -> tuple[bytes, PreviewScene]: + """The preview of the running notebook's product, from the values in the kernel. + + Unlike :func:`live_package` this does not project the product onto a + definition: it looks at the solids as they are, which takes a fraction of the + time (see :mod:`simplecadapi.scene.preview`). + """ + + global _last_preview + context = safe_get_context() + if context is None or context.filename is None: + raise LiveUnavailableError("not running inside a marimo kernel") + config = read_notebook_config(context.filename) + if config is None: + raise LiveUnavailableError( + f"{context.filename}: not a SimpleCAD notebook (no [tool.simplecadapi])" + ) + glbls = context.globals + ordered = list(context.graph.cells.values()) + values = {name: glbls[name] for cell in ordered for name in cell.defs if name in glbls} + read = frozenset().union(*(cell.refs for cell in ordered)) + scene = compile_preview(find_product(config, values, read)) + _last_preview = scene + return encode_preview(scene), scene + + +def publish_preview(url: str, *, step: str = "", timeout: float = 60.0) -> dict[str, Any]: + """:func:`live_preview`, then POST the bytes to *url*; returns a short summary.""" + + data, scene = live_preview() + request = urllib.request.Request( + url, + data=data, + headers={"Content-Type": PREVIEW_MEDIA_TYPE, "X-Live-Step": step}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + response.read() + except (urllib.error.URLError, OSError) as error: + raise RuntimeError(f"publishing the preview to {url} failed: {error}") from error + return {"nodes": len(scene.nodes), "parts": len(scene.parts), "bytes": len(data)} + + +def preview_entity_info(entity_id: str) -> dict[str, Any] | None: + """The properties of one entity of the scene last previewed (see :func:`entity_info`).""" + + if _last_preview is None: + return None + return entity_info(_last_preview, entity_id) + + +def publish_live(url: str, *, step: str = "", timeout: float = 60.0) -> LivePackage: + """:func:`live_package`, then POST the package bytes to *url*.""" + + package = live_package() + request = urllib.request.Request( + url, + data=package.data, + headers={"Content-Type": PACKAGE_MEDIA_TYPE, "X-Live-Step": step}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + response.read() + except (urllib.error.URLError, OSError) as error: + raise RuntimeError(f"publishing the package to {url} failed: {error}") from error + return package + + +__all__ = [ + "LivePackage", + "LiveUnavailableError", + "live_package", + "live_preview", + "preview_entity_info", + "publish_live", + "publish_preview", +] diff --git a/src/simplecadapi/runtime/params.py b/src/simplecadapi/runtime/params.py new file mode 100644 index 00000000..57e565ed --- /dev/null +++ b/src/simplecadapi/runtime/params.py @@ -0,0 +1,635 @@ +"""Notebook parameters: what an editor can drive, and the values it saved. + +A parameter is a top-level variable a cell assigns a literal (or a +``scad.var``), in a cell headed ``# ---- params ----`` (``# ---- params: ----`` +to name the group; the feature tree convention's header) or on a +line that carries a ``# ui:`` comment:: + + # ---- params: flange ---- + OUTER_D = 100.0 # ui: slider(50, 200, step=1, unit="mm", label="Outer diameter") + BOLTS = 6 # ui: number(3, 12, step=1) + CHAMFER = True # no comment: a toggle, inferred + width = scad.var("width", 20.0, unit="mm") # ui: slider(10, 40) + +The comment is optional: without it the control follows the literal +(a number box for numbers, a toggle for booleans). Controls are +``slider(min, max, step=...)``, ``number(min=, max=, step=...)`` and +``toggle()``, each taking ``unit=`` and ``label=``. Notebooks written +before this convention (a params cell of plain constants) list their +parameters with no change. + +:func:`list_parameters` reads them from the file, without running it. + +**Saved values.** An editor that drives a slider keeps the values in +``params.json`` in the notebook's :func:`~.deps.state_dir`. The editor's +kernel executor (:mod:`.executor`) applies them as the defining cell +finishes, so the cells that read the variable see the saved value and +marimo's dataflow reruns exactly those. A saved value replaces a literal +and, for a ``scad.var``, the variable's default (its unit, tolerance and +comment stay). A headless run (``sca run``, ``run_notebook``, ``use``) +takes only the overrides it is given, never the saved values: the notebook +file stays the one source of truth, and committing a value is an edit of +the file. +""" + +from __future__ import annotations + +import ast +import io +import json +import re +import tokenize +from dataclasses import dataclass, field, replace +from functools import lru_cache +from pathlib import Path +from typing import Any + +from ._marimo import parse_notebook +from .cells import cell_keys +from .config import NotebookConfig +from .deps import state_dir + +_GROUP = re.compile(r"^#\s*-{2,}\s*params(?::\s*(.*?))?\s*-*\s*$", re.MULTILINE) +_UI = re.compile(r"#\s*ui:\s*(.+?)\s*$") +_CONTROLS = {"slider", "number", "toggle"} +_UI_KEYS = {"min", "max", "step", "unit", "label"} +_SAVED_SCHEMA = 1 + + +@dataclass(frozen=True) +class ParamSpec: + """One parameter of a notebook, as declared in its file.""" + + name: str + """The top-level variable.""" + group: str + """The label of the ``# ---- params: ... ----`` header, or ``""``.""" + cell: str + """Key of the declaring cell (see :func:`~.cells.cell_keys`).""" + line: int + """Line of the assignment within the cell's code, 1-based.""" + kind: str + """``"float"``, ``"int"`` or ``"bool"``.""" + declared: str + """``"literal"`` or ``"var"`` (a ``scad.var``).""" + default: float | int | bool + control: str + """``"slider"``, ``"number"`` or ``"toggle"``.""" + min: float | None = None + max: float | None = None + step: float | None = None + unit: str | None = None + label: str | None = None + comment: str | None = None + issues: tuple[str, ...] = field(default=()) + """Problems with the ``# ui:`` comment; the control fell back.""" + + def to_dict(self) -> dict[str, Any]: + return { + "name": self.name, + "group": self.group, + "cell": self.cell, + "line": self.line, + "kind": self.kind, + "declared": self.declared, + "default": self.default, + "control": self.control, + "min": self.min, + "max": self.max, + "step": self.step, + "unit": self.unit, + "label": self.label or self.name.rsplit(".", 1)[-1], + "comment": self.comment, + "issues": list(self.issues), + } + + +# --------------------------------------------------------------------------- +# Listing +# --------------------------------------------------------------------------- + + +def list_parameters(path: str | Path) -> list[ParamSpec]: + """Return the parameters declared in the notebook file at *path*. + + The file is parsed, not run, so the answer is available before any cell + has executed. Cells that do not parse contribute nothing. + """ + + resolved = Path(path).expanduser().resolve() + stat = resolved.stat() + return list(_list_version(resolved, stat.st_mtime_ns, stat.st_size)) + + +@lru_cache(maxsize=32) +def _list_version(path: Path, _mtime_ns: int, _size: int) -> tuple[ParamSpec, ...]: + try: + notebook = parse_notebook(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, SyntaxError): + return () + if notebook is None: + return () + cells = list(notebook.cells) + keys = cell_keys(cell.code for cell in cells) + specs: list[ParamSpec] = [] + for cell, key in zip(cells, keys): + specs.extend(parameters_in_cell(cell.code, key)) + return tuple(specs) + + +def parameters_in_cell(code: str, key: str = "") -> list[ParamSpec]: + """Return the parameters one cell's *code* declares.""" + + try: + tree = ast.parse(code) + except (SyntaxError, ValueError): + return [] + header = _GROUP.search(code) + comments = _line_comments(code) + specs: list[ParamSpec] = [] + for statement in tree.body: + if not ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + ): + continue + name = statement.targets[0].id + if name.startswith("_"): + continue + literal = _read_value(statement.value) + if literal is None: + continue + ui = _ui_comment(statement, comments) + if header is None and ui is None: + continue # an ordinary constant, not offered as a parameter + specs.append( + _spec( + name, + (header.group(1) or "") if header else "", + key, + statement.lineno, + literal, + ui, + ) + ) + return specs + + +@dataclass(frozen=True) +class _Value: + default: float | int | bool + declared: str + unit: str | None = None + comment: str | None = None + + +def _read_value(node: ast.expr) -> _Value | None: + """A literal number or bool, or ``var(name, default, ...)`` of them.""" + + literal = _literal(node) + if literal is not None: + return _Value(literal, "literal") + if ( + isinstance(node, ast.Call) + and _callee(node.func) == "var" + and len(node.args) >= 2 + ): + default = _literal(node.args[1]) + if default is None or isinstance(default, bool): + return None + unit = comment = None + for keyword in node.keywords: + if keyword.arg == "unit" and isinstance(keyword.value, ast.Constant): + unit = keyword.value.value if isinstance(keyword.value.value, str) else None + if keyword.arg == "comment" and isinstance(keyword.value, ast.Constant): + comment = keyword.value.value if isinstance(keyword.value.value, str) else None + if comment is None and len(node.args) >= 3: + third = node.args[2] + if isinstance(third, ast.Constant) and isinstance(third.value, str): + comment = third.value + return _Value(default, "var", unit, comment) + return None + + +def _callee(node: ast.expr) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return node.attr + return None + + +def _literal(node: ast.expr) -> float | int | bool | None: + if isinstance(node, ast.Constant) and isinstance(node.value, (bool, int, float)): + return node.value + if ( + isinstance(node, ast.UnaryOp) + and isinstance(node.op, (ast.USub, ast.UAdd)) + and isinstance(node.operand, ast.Constant) + and isinstance(node.operand.value, (int, float)) + and not isinstance(node.operand.value, bool) + ): + value = node.operand.value + return -value if isinstance(node.op, ast.USub) else value + return None + + +def _line_comments(code: str) -> dict[int, str]: + comments: dict[int, str] = {} + try: + for token in tokenize.generate_tokens(io.StringIO(code).readline): + if token.type == tokenize.COMMENT: + comments[token.start[0]] = token.string + except (tokenize.TokenError, IndentationError, SyntaxError): + pass + return comments + + +def _ui_comment(statement: ast.Assign, comments: dict[int, str]) -> str | None: + end = statement.end_lineno or statement.lineno + for line in range(statement.lineno, end + 1): + match = _UI.search(comments.get(line, "")) + if match: + return match.group(1) + return None + + +def _spec( + name: str, + group: str, + cell: str, + line: int, + value: _Value, + ui: str | None, +) -> ParamSpec: + default = value.default + kind = "bool" if isinstance(default, bool) else "int" if isinstance(default, int) else "float" + control = "toggle" if kind == "bool" else "number" + options: dict[str, Any] = {} + issues: list[str] = [] + if ui is not None: + parsed = _parse_ui(ui, issues) + if parsed is not None: + requested, options = parsed + if kind == "bool" and requested != "toggle": + issues.append(f"{requested} needs a number; {name} is a bool") + elif kind != "bool" and requested == "toggle": + issues.append(f"toggle needs a bool; {name} is a number") + elif requested == "slider" and ("min" not in options or "max" not in options): + issues.append("slider needs min and max") + else: + control = requested + lower, upper = options.get("min"), options.get("max") + if lower is not None and upper is not None and lower > upper: + issues.append(f"min {lower} is above max {upper}") + lower = upper = None + control = "number" if kind != "bool" else control + return ParamSpec( + name=name, + group=group, + cell=cell, + line=line, + kind=kind, + declared=value.declared, + default=default, + control=control, + min=lower, + max=upper, + step=options.get("step"), + unit=options.get("unit", value.unit), + label=options.get("label"), + comment=value.comment, + issues=tuple(issues), + ) + + +def _parse_ui(text: str, issues: list[str]) -> tuple[str, dict[str, Any]] | None: + """Parse ``slider(1, 5, step=1, unit="mm")`` into control and options.""" + + try: + call = ast.parse(text, mode="eval").body + except SyntaxError: + issues.append(f"cannot parse ui comment {text!r}") + return None + if not isinstance(call, ast.Call) or _callee(call.func) not in _CONTROLS: + issues.append(f"ui comment must be one of {sorted(_CONTROLS)}: {text!r}") + return None + control = _callee(call.func) + assert control is not None + options: dict[str, Any] = {} + positional = ("min", "max") if control != "toggle" else () + if len(call.args) > len(positional): + issues.append(f"too many positional arguments for {control}") + return None + pairs = [(key, arg) for key, arg in zip(positional, call.args)] + pairs += [(keyword.arg, keyword.value) for keyword in call.keywords] + for key, node in pairs: + if key not in _UI_KEYS: + issues.append(f"unknown ui option {key!r}") + return None + try: + value = ast.literal_eval(node) + except (ValueError, SyntaxError): + issues.append(f"ui option {key} must be a literal") + return None + if key in ("min", "max", "step"): + if isinstance(value, bool) or not isinstance(value, (int, float)): + issues.append(f"ui option {key} must be a number") + return None + elif not isinstance(value, str): + issues.append(f"ui option {key} must be a string") + return None + options[key] = value + if options.get("step") is not None and options["step"] <= 0: + issues.append("step must be positive") + return None + return control, options + + +# --------------------------------------------------------------------------- +# Parameters of a plain module: the fields of its dataclasses +# --------------------------------------------------------------------------- + + +def list_module_parameters(path: str | Path) -> list[ParamSpec]: + """Return the parameter fields of the dataclasses in the module at *path*. + + A notebook that imports shared dimensions (``DIMS = ULinkDimensions()``) + has no parameter cell; the numbers live in the module. Every field of a + ``@dataclass`` with a literal default (a number or bool) is a parameter, + named ``.``, and so is a top-level constant that carries a + ``# ui:`` comment (named as itself, group ``module``); the comment picks + the control exactly as it does in a notebook cell. Editing a value means + editing the default in the file (:func:`commit_field_value`, + :func:`commit_value`). + """ + + resolved = Path(path).expanduser().resolve() + stat = resolved.stat() + return list(_module_version(resolved, stat.st_mtime_ns, stat.st_size)) + + +@lru_cache(maxsize=32) +def _module_version(path: Path, _mtime_ns: int, _size: int) -> tuple[ParamSpec, ...]: + try: + return tuple(parameters_in_module(path.read_text(encoding="utf-8"))) + except (OSError, UnicodeError): + return () + + +def parameters_in_module(code: str) -> list[ParamSpec]: + try: + tree = ast.parse(code) + except (SyntaxError, ValueError): + return [] + comments = _line_comments(code) + specs: list[ParamSpec] = [] + for node in tree.body: + if ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and not node.targets[0].id.startswith("_") + ): + # A module constant is a parameter only when it says so (`# ui:`): + # most are tags and tables, not numbers to tune. + literal = _read_value(node.value) + ui = _ui_comment(node, comments) + if literal is not None and literal.declared == "literal" and ui is not None: + specs.append(_spec(node.targets[0].id, "module", "", node.lineno, literal, ui)) + continue + if not (isinstance(node, ast.ClassDef) and _is_dataclass(node)): + continue + for statement in node.body: + if not ( + isinstance(statement, ast.AnnAssign) + and isinstance(statement.target, ast.Name) + and statement.value is not None + and not statement.target.id.startswith("_") + ): + continue + literal = _read_value(statement.value) + if literal is None or literal.declared != "literal": + continue + ui = _ui_comment(statement, comments) # type: ignore[arg-type] + specs.append( + _spec(f"{node.name}.{statement.target.id}", node.name, "", statement.lineno, literal, ui) + ) + return specs + + +def _is_dataclass(node: ast.ClassDef) -> bool: + for decorator in node.decorator_list: + target = decorator.func if isinstance(decorator, ast.Call) else decorator + if _callee(target) == "dataclass": + return True + return False + + +def commit_field_value(code: str, class_name: str, field: str, value: float | int | bool) -> str | None: + """Return *code* with the default of ``.`` set to *value*. + + The literal is replaced in place; the rest of the line (type annotation, + comments) is kept. ``None`` if the module has no such dataclass field or + *value* does not fit its kind. + """ + + try: + tree = ast.parse(code) + except (SyntaxError, ValueError): + return None + for node in tree.body: + if not (isinstance(node, ast.ClassDef) and node.name == class_name and _is_dataclass(node)): + continue + for statement in node.body: + if not ( + isinstance(statement, ast.AnnAssign) + and isinstance(statement.target, ast.Name) + and statement.target.id == field + and statement.value is not None + ): + continue + declared = _read_value(statement.value) + if declared is None or declared.declared != "literal": + return None + text = _format_value(declared.default, value) + target = statement.value + if text is None or target.end_lineno != target.lineno: + return None + lines = code.splitlines(keepends=True) + line = lines[target.lineno - 1].encode("utf-8") + replaced = line[: target.col_offset] + text.encode("utf-8") + line[target.end_col_offset :] + lines[target.lineno - 1] = replaced.decode("utf-8") + return "".join(lines) + return None + + +# --------------------------------------------------------------------------- +# Saved values +# --------------------------------------------------------------------------- + + +def saved_params_file(config: NotebookConfig) -> Path: + return state_dir(config) / "params.json" + + +def read_saved_params(config: NotebookConfig) -> dict[str, float | int | bool]: + """Return the values the editor saved for the notebook; none if absent.""" + + path = saved_params_file(config) + try: + stat = path.stat() + except OSError: + return {} + return dict(_read_saved_version(path, stat.st_mtime_ns, stat.st_size)) + + +@lru_cache(maxsize=64) +def _read_saved_version( + path: Path, _mtime_ns: int, _size: int +) -> dict[str, float | int | bool]: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError): + return {} + if not isinstance(data, dict) or data.get("schema") != _SAVED_SCHEMA: + return {} + values = data.get("values") + if not isinstance(values, dict): + return {} + return { + name: value + for name, value in values.items() + if isinstance(name, str) and isinstance(value, (bool, int, float)) + } + + +def write_saved_params( + config: NotebookConfig, values: dict[str, float | int | bool] +) -> None: + """Replace the saved values; an empty mapping removes the file.""" + + path = saved_params_file(config) + if not values: + path.unlink(missing_ok=True) + return + path.parent.mkdir(parents=True, exist_ok=True) + payload = {"schema": _SAVED_SCHEMA, "values": dict(sorted(values.items()))} + temporary = path.with_suffix(".json.tmp") + temporary.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + temporary.replace(path) + + +def apply_saved_params( + config: NotebookConfig, defined: set[str], glbls: dict[str, Any] +) -> list[str]: + """Replace the variables a cell just defined with their saved values. + + Returns the names replaced. A saved value of the wrong kind for the + variable (a number for a bool) is ignored. + """ + + saved = read_saved_params(config) + applied: list[str] = [] + for name in defined & saved.keys(): + if name not in glbls: + continue + replacement = _replace(glbls[name], saved[name]) + if replacement is not _NO_REPLACEMENT: + glbls[name] = replacement + applied.append(name) + return applied + + +_NO_REPLACEMENT = object() + + +def _replace(current: Any, saved: float | int | bool) -> Any: + from ..params.expr import Var + + if isinstance(current, Var): + if isinstance(saved, bool): + return _NO_REPLACEMENT + return replace(current, default=float(saved), expr_id="") + if isinstance(current, bool): + return saved if isinstance(saved, bool) else _NO_REPLACEMENT + if isinstance(saved, bool): + return _NO_REPLACEMENT + if isinstance(current, int): + return int(saved) if float(saved).is_integer() else float(saved) + if isinstance(current, float): + return float(saved) + return _NO_REPLACEMENT + + +# --------------------------------------------------------------------------- +# Committing a value to the source +# --------------------------------------------------------------------------- + + +def commit_value(code: str, name: str, value: float | int | bool) -> str | None: + """Return *code* with the declaration of parameter *name* set to *value*. + + The literal (or the default argument of a ``scad.var``) is replaced in + place; the rest of the line, a ``# ui:`` comment included, is kept. + Returns ``None`` if the cell does not declare *name* as a parameter or + *value* does not fit its kind. + """ + + try: + tree = ast.parse(code) + except (SyntaxError, ValueError): + return None + for statement in tree.body: + if not ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and statement.targets[0].id == name + ): + continue + declared = _read_value(statement.value) + if declared is None: + return None + node = statement.value + if declared.declared == "var": + assert isinstance(node, ast.Call) + node = node.args[1] + text = _format_value(declared.default, value) + if text is None or node.end_lineno != node.lineno: + return None + lines = code.splitlines(keepends=True) + line = lines[node.lineno - 1].encode("utf-8") + # ast offsets count UTF-8 bytes. + replaced = line[: node.col_offset] + text.encode("utf-8") + line[node.end_col_offset :] + lines[node.lineno - 1] = replaced.decode("utf-8") + return "".join(lines) + return None + + +def _format_value( + current: float | int | bool, value: float | int | bool +) -> str | None: + if isinstance(current, bool): + return repr(value) if isinstance(value, bool) else None + if isinstance(value, bool): + return None + if isinstance(current, int) and float(value).is_integer(): + return str(int(value)) + return repr(float(value)) + + +__all__ = [ + "ParamSpec", + "apply_saved_params", + "commit_field_value", + "commit_value", + "list_module_parameters", + "list_parameters", + "parameters_in_cell", + "parameters_in_module", + "read_saved_params", + "saved_params_file", + "write_saved_params", +] diff --git a/src/simplecadapi/runtime/runner.py b/src/simplecadapi/runtime/runner.py index 228b8a2c..37bc59e2 100644 --- a/src/simplecadapi/runtime/runner.py +++ b/src/simplecadapi/runtime/runner.py @@ -6,9 +6,9 @@ * cells run through :class:`~.executor.SimpleCadExecutor`, so each records into a session of its own; -* cells are cached with marimo's cell cache, under the dependency digest - *D* (:mod:`.deps`) so that a changed local module, child notebook or - input file invalidates the cache; +* cells are cached with marimo's cell cache, each under a digest of the + local modules, child notebooks and input files *it* depends on + (:mod:`.cache`), so a changed file recomputes only the cells that read it; * after the last cell the product is projected onto its definition (:mod:`.projection`). @@ -23,7 +23,6 @@ from __future__ import annotations import hashlib -import shutil import sys from collections.abc import Callable, Iterator, Mapping from concurrent.futures import ThreadPoolExecutor @@ -38,15 +37,11 @@ from ..recording.graph import isolated_recording from ._marimo import ( AppScriptRunner, - CachedLifecycle, CellId_t, CellImpl, Evaluator, ExecutionLifecycle, - FileStore, InternalApp, - LazyLoader, - LazyStore, MarimoRescheduleError, MarimoRuntimeException, MutableGlobals, @@ -57,16 +52,15 @@ runtime_context_installed, safe_get_context, ) +from .cache import CellCache, CellDigests, cache_root, prune from .cells import NotebookCells from .config import NotebookConfig, NotebookConfigError, read_notebook_config from .deps import ( - dependency_digest, local_modules, read_dependencies, record_dependencies, recording_dependencies, refresh_local_modules, - state_dir, write_dependencies, ) from .executor import NotebookScope, SimpleCadExecutor @@ -78,11 +72,6 @@ """``ran``: executed; ``cached``: restored from the cell cache; ``skipped``: not run, because overrides replace its variables or the cell is disabled.""" -_CELLS = "cells" -"""The cell cache's loader name, fixed so a cache directory can be renamed: -its entries reference their values by paths starting with this name.""" - - @dataclass(frozen=True) class CellReport: """What happened to one cell during a run. @@ -204,10 +193,13 @@ def _run(config: NotebookConfig, overrides: dict[str, Any], *, cache: bool) -> N modules = local_modules(config, (cell.code for cell in ordered)) module_files = frozenset(modules.values()) previous = read_dependencies(config) - digest = dependency_digest(config, module_files | previous) - cache_root = _cache_root(config) + setup_cell = internal.graph.cells.get(internal.cell_manager.setup_cell_id) + digests = CellDigests(config, ordered, setup_cell) + root = cache_root(config) progress = _Progress() - cell_cache = _CellCache(internal, cache_root / digest) if cache else None + cell_cache = ( + CellCache(internal.graph, root, lambda cell: digests.digest(cell.cell_id)) if cache else None + ) refresh_local_modules(modules) with isolated_recording(), _importable(config.directory), recording_dependencies() as used: @@ -247,8 +239,18 @@ def _run(config: NotebookConfig, overrides: dict[str, Any], *, cache: bool) -> N recorded = frozenset(used) | (previous if restored else frozenset()) write_dependencies(config, recorded) dependencies = module_files | recorded - if cache: - _keep_cache(cache_root, digest, dependency_digest(config, dependencies)) + if cache and cell_cache is not None: + # A cell that runs a child notebook depends on the record just written: + # its entries move to the digest of what it now depends on. + after = CellDigests(config, ordered, setup_cell) + used_digests = set(cell_cache.used) + for cell in ordered: + old, new = digests.digest(cell.cell_id), after.digest(cell.cell_id) + if old is not None and new is not None and old != new and old in used_digests: + cell_cache.move(old, new) + used_digests.discard(old) + used_digests.add(new) + prune(root, used_digests) reports = tuple( CellReport( @@ -276,7 +278,7 @@ def _execute( scope: NotebookScope, overrides: dict[str, Any], progress: _Progress, - cell_cache: _CellCache | None = None, + cell_cache: CellCache | None = None, ) -> MutableGlobals: glbls = {**_run_setup(internal, scope, progress), **overrides} runner = AppScriptRunner(internal, str(scope.config.path), glbls) @@ -371,30 +373,6 @@ def teardown(self, cell: CellImpl, glbls: MutableGlobals, run_result: RunResult) return None -class _CellCache(CachedLifecycle): - """marimo's cell cache, stored in *directory* and reporting its hits.""" - - def __init__(self, internal: InternalApp, directory: Path) -> None: - # CachedLifecycle only takes a loader kind and stores in marimo's - # default place; its loader is replaced by one stored in *directory* - # (the pickle kind is merely the cheapest to build and discard). - super().__init__(internal.graph, loader="pickle") - self._lazy_loader = LazyLoader( - name=_CELLS, store=LazyStore(FileStore(str(directory))) - ) - self._loader = self._lazy_loader - self.restored: set[CellId_t] = set() - - def setup(self, cell: CellImpl, glbls: MutableGlobals) -> Skip | None: - decision = super().setup(cell, glbls) - if isinstance(decision, Skip): - self.restored.add(cell.cell_id) - return decision - - def flush(self) -> None: - self._lazy_loader.flush() - - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -406,36 +384,6 @@ def _cell_prefix(config: NotebookConfig) -> str: return hashlib.sha256(str(config.path).encode()).hexdigest()[:8] + "-" -def _cache_root(config: NotebookConfig) -> Path: - """The notebook's cell caches: one directory per dependency digest.""" - - return state_dir(config) / "cache" - - -def _keep_cache(root: Path, used: str, current: str) -> None: - """Keep the cache the run used, under the digest of what it now depends on. - - The two digests differ when the run found out what it depends on anew - (a ``use`` it had not made before, or one it no longer makes); every - entry is valid for the files' current content either way. - Caches under any other digest are stale and removed. - """ - - source = root / used - target = root / current - if source != target and source.is_dir(): - shutil.rmtree(target, ignore_errors=True) - source.rename(target) - if not root.is_dir(): # nothing was cached - return - for stale in root.iterdir(): - if stale != target: - if stale.is_dir(): - shutil.rmtree(stale, ignore_errors=True) - else: - stale.unlink(missing_ok=True) - - @contextmanager def _importable(directory: Path) -> Iterator[None]: """Let the cells import modules next to the notebook, as marimo does.""" diff --git a/src/simplecadapi/runtime/workspace.py b/src/simplecadapi/runtime/workspace.py new file mode 100644 index 00000000..3a1445e6 --- /dev/null +++ b/src/simplecadapi/runtime/workspace.py @@ -0,0 +1,292 @@ +"""A workspace: a directory of SimpleCAD notebooks. + +There is no manifest and no layout to adopt. A workspace is any directory +tree; its models are the notebooks whose PEP 723 header carries +``[tool.simplecadapi]`` (:mod:`.config`), wherever they are. Plain modules, +verification scripts and generated output are not notebooks and are never +listed. What :func:`scan_workspace` reports is read from the files alone: + +* the notebook's id and revision (the header); +* ``kind`` — ``"assembly"`` if a cell builds an assembly or calls + :func:`~.runner.use`, else ``"part"``; +* ``uses`` — the notebooks it runs with ``use("")``, so a + front end can order and refresh a product's parts; +* the feature tree (:func:`list_features`, the ``# ---- feature: ... ----`` + cell headers of the feature tree convention) and the parameters + (:func:`~.params.list_parameters`). +""" + +from __future__ import annotations + +import ast +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from ._marimo import parse_notebook +from .cells import cell_keys +from .config import NotebookConfigError, read_notebook_config +from .deps import local_modules +from .params import list_parameters + +_FEATURE = re.compile( + r"^#\s*-{2,}\s*feature:\s*(?P[^\s(]+)\s*(?:\((?P[^)]*)\))?\s*-*\s*$", + re.MULTILINE, +) +_SKIPPED = frozenset({"node_modules", "venv", "site-packages", "build", "dist"}) +_ASSEMBLY_CALLS = frozenset({"make_assembly_rassembly", "Assembly", "assemble", "use"}) + + +@dataclass(frozen=True) +class FeatureSpec: + """One ``# ---- feature: (, ...) ----`` cell header.""" + + slug: str + role: str + """``build``, ``add``, ``subtract``, ... ``""`` if the header gives none.""" + tags: tuple[tuple[str, str], ...] + """The ``key=value`` annotations, e.g. ``("profile", "sketch")``.""" + cell: str + """Key of the cell the header opens.""" + line: int + """Line of the header within the cell's code, 1-based.""" + + def to_dict(self) -> dict[str, Any]: + return { + "slug": self.slug, + "role": self.role, + "tags": dict(self.tags), + "cell": self.cell, + "line": self.line, + } + + +@dataclass(frozen=True) +class NotebookEntry: + path: str + """Relative to the workspace root, POSIX separators.""" + id: str + revision: str + kind: str + """``"part"`` or ``"assembly"``.""" + uses: tuple[str, ...] + """Notebooks run with ``use("...")`` in a literal path, workspace-relative.""" + error: str | None = None + """Why the header could not be read, if it could not.""" + modules: tuple[str, ...] = () + """The local modules its cells import (workspace-relative), however deep: + plain ``.py`` files such as shared dimensions.""" + + def to_dict(self) -> dict[str, Any]: + return { + "path": self.path, + "id": self.id, + "revision": self.revision, + "kind": self.kind, + "uses": list(self.uses), + "modules": list(self.modules), + "error": self.error, + } + + +@dataclass(frozen=True) +class ModuleEntry: + """A local module some notebook imports.""" + + path: str + """Relative to the workspace root, POSIX separators.""" + name: str + used_by: tuple[str, ...] + """The notebooks (workspace-relative) whose cells import it.""" + + def to_dict(self) -> dict[str, Any]: + return {"path": self.path, "name": self.name, "used_by": list(self.used_by)} + + +def scan_workspace(root: str | Path) -> list[NotebookEntry]: + """Return the SimpleCAD notebooks under *root*, sorted by path.""" + + base = Path(root).expanduser().resolve() + entries: list[NotebookEntry] = [] + for directory, subdirectories, files in os.walk(base): + subdirectories[:] = sorted( + name + for name in subdirectories + if not name.startswith((".", "_")) and name not in _SKIPPED + ) + for name in sorted(files): + if not name.endswith(".py") or name.startswith("_"): + continue + path = Path(directory) / name + entry = _entry(base, path) + if entry is not None: + entries.append(entry) + return sorted(entries, key=lambda entry: entry.path) + + +def _entry(base: Path, path: Path) -> NotebookEntry | None: + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError): + return None + if "tool.simplecadapi" not in text: # cheap filter before parsing the header + return None + relative = path.relative_to(base).as_posix() + try: + config = read_notebook_config(path) + except NotebookConfigError as error: + return NotebookEntry(relative, path.stem, "", "part", (), error=str(error)) + if config is None: + return None + kind, uses = _structure(base, path, text) + modules = tuple( + sorted( + file.relative_to(base).as_posix() + for file in notebook_modules(path).values() + if file.is_relative_to(base) + ) + ) + return NotebookEntry(relative, config.id, config.revision, kind, uses, modules=modules) + + +def notebook_modules(path: str | Path) -> dict[str, Path]: + """The local modules the notebook at *path* imports, name -> file. + + Found statically, from the imports in every cell (the setup cell + included), as the cell cache's dependency digest finds them. + """ + + resolved = Path(path).expanduser().resolve() + config = read_notebook_config(resolved) + if config is None: + return {} + try: + notebook = parse_notebook(resolved.read_text(encoding="utf-8")) + except (OSError, UnicodeError, SyntaxError): + return {} + if notebook is None: + return {} + return local_modules(config, [cell.code for cell in notebook.cells]) + + +def scan_modules(root: str | Path, entries: list[NotebookEntry] | None = None) -> list[ModuleEntry]: + """The local modules the notebooks under *root* import, sorted by path.""" + + base = Path(root).expanduser().resolve() + users: dict[str, set[str]] = {} + names: dict[str, str] = {} + for entry in entries if entries is not None else scan_workspace(base): + for name, file in notebook_modules(base / entry.path).items(): + if not file.is_relative_to(base): + continue + relative = file.relative_to(base).as_posix() + users.setdefault(relative, set()).add(entry.path) + names[relative] = name + return [ + ModuleEntry(path=path, name=names[path], used_by=tuple(sorted(users[path]))) + for path in sorted(users) + ] + + +def _structure(base: Path, path: Path, text: str) -> tuple[str, tuple[str, ...]]: + try: + notebook = parse_notebook(text) + except SyntaxError: + notebook = None + codes = [cell.code for cell in notebook.cells] if notebook is not None else [] + assembly = False + uses: list[str] = [] + for code in codes: + try: + tree = ast.parse(code) + except (SyntaxError, ValueError): + continue + for node in ast.walk(tree): + if isinstance(node, ast.Call): + name = _callee(node.func) + if name in _ASSEMBLY_CALLS: + assembly = True + if name == "use" and node.args: + target = node.args[0] + if isinstance(target, ast.Constant) and isinstance(target.value, str): + resolved = (path.parent / target.value).resolve() + try: + uses.append(resolved.relative_to(base).as_posix()) + except ValueError: + pass # a notebook outside the workspace + return ("assembly" if assembly else "part"), tuple(dict.fromkeys(uses)) + + +def _callee(node: ast.expr) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return node.attr + return None + + +def list_features(path: str | Path) -> list[FeatureSpec]: + """Return the feature headers of the notebook at *path*, in cell order.""" + + try: + notebook = parse_notebook(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, SyntaxError): + return [] + if notebook is None: + return [] + cells = list(notebook.cells) + keys = cell_keys(cell.code for cell in cells) + features: list[FeatureSpec] = [] + for cell, key in zip(cells, keys): + features.extend(features_in_cell(cell.code, key)) + return features + + +def features_in_cell(code: str, key: str = "") -> list[FeatureSpec]: + features: list[FeatureSpec] = [] + for match in _FEATURE.finditer(code): + role = "" + tags: list[tuple[str, str]] = [] + for part in (match.group("spec") or "").split(","): + part = part.strip() + if not part: + continue + name, separator, value = part.partition("=") + if separator: + tags.append((name.strip(), value.strip())) + elif not role: + role = part + features.append( + FeatureSpec( + slug=match.group("slug"), + role=role, + tags=tuple(tags), + cell=key, + line=code.count("\n", 0, match.start()) + 1, + ) + ) + return features + + +def describe_notebook(path: str | Path) -> dict[str, Any]: + """Features and parameters of one notebook, as plain data.""" + + return { + "features": [feature.to_dict() for feature in list_features(path)], + "parameters": [spec.to_dict() for spec in list_parameters(path)], + } + + +__all__ = [ + "FeatureSpec", + "ModuleEntry", + "NotebookEntry", + "describe_notebook", + "features_in_cell", + "list_features", + "notebook_modules", + "scan_modules", + "scan_workspace", +] diff --git a/src/simplecadapi/scene/preview.py b/src/simplecadapi/scene/preview.py new file mode 100644 index 00000000..9fc726a3 --- /dev/null +++ b/src/simplecadapi/scene/preview.py @@ -0,0 +1,488 @@ +"""A preview of a product for a viewer that is being edited with. + +A product package (:mod:`.product_scene`) is the durable, exchangeable form of +a product: definitions, BREP, feature graph, topology snapshot, hashed and +validated GLB assets and entity sidecars. Making one takes seconds, nearly all +of it for what a viewer of a model under edit never reads. This module makes the +other thing a viewer needs, straight from the solids in memory: + +* the triangles of each part (positions, normals, indices), the line segments of + its edges and the positions of its vertices, with the index range each face and + edge occupies, so a viewer can highlight and pick them; +* the parts of an assembly, each once, and where each of its instances stands; +* :func:`entity_info`, the geometric properties of one picked face, edge, vertex + or solid, worked out on demand (about 0.05 ms for a face). + +The geometry of a part is derived data: it is kept per (BREP content, tolerance), +so a part that did not change costs nothing to preview again, even if its object +was rebuilt (a notebook cell ran again, a cache restored it). Nothing here is +hashed canonically, validated or written as a document. + +Coordinates are glTF's (metres, +Y up: CAD x, z, -y / 1000), as in the package's +GLB assets, so a viewer built for those uses the arrays as they are. +""" + +from __future__ import annotations + +import hashlib +import json +import struct +import threading +from collections import OrderedDict +from dataclasses import dataclass +from typing import Any + +import numpy as np + +from .._internal.mesh import DEFAULT_ANGULAR_TOLERANCE, DEFAULT_LINEAR_TOLERANCE, cached_mesh +from ..artifacts.brep import _bintools_bytes +from ..core import Solid +from ..kernel.ocp_properties import center_of_mass +from ..product.assembly import Assembly +from ..product.part import Part +from ..product.placement import Placement, identity_placement +from .compiler import ( + _curve_geometry, + _edge_endpoints, + _frame, + _normalize, + _orientation, + _shape_bounds, + _surface_geometry, + _vec, +) +from .compiler import _bounds as _bounds_document +from .render_mesh import build_edge_mesh, solid_asset_bounds + +PREVIEW_SCHEMA = "sca-preview-1" +MAGIC = b"SCAPREV1" +_CACHE_PARTS = 96 + + +class PreviewError(ValueError): + """The product cannot be previewed (it has no solid to draw).""" + + +@dataclass(frozen=True) +class PartPreview: + """Everything a viewer draws of one solid; immutable and independent of its object.""" + + key: str + positions: np.ndarray # (n, 3) float32, glTF metres + normals: np.ndarray # (n, 3) float32 + indices: np.ndarray # (3 * triangles,) uint32 + face_groups: tuple[tuple[str, int, int], ...] # (face topo id, first index, index count) + edge_positions: np.ndarray # (k, 3) float32 + edge_indices: np.ndarray # (2 * segments,) uint32 + edge_groups: tuple[tuple[str, int, int], ...] + vertex_positions: np.ndarray # (v, 3) float32 + vertex_ids: tuple[str, ...] + solid_topo_id: str + triangle_count: int + + +@dataclass(frozen=True) +class PreviewNode: + node_id: str + part_key: str + definition_id: str + display_name: str + placement: Placement # CAD space, millimetres, relative to the product + + +@dataclass +class PreviewScene: + parts: dict[str, PartPreview] + nodes: list[PreviewNode] + solids: dict[str, Solid] + """The solid behind each part key, for :func:`entity_info` (not serialized).""" + definitions: dict[str, str] + """The definition id each part key belongs to.""" + + +# -- the per-part cache -------------------------------------------------------------- + +_parts: OrderedDict[str, PartPreview] = OrderedDict() +_parts_lock = threading.Lock() + + +def _remember(preview: PartPreview) -> PartPreview: + with _parts_lock: + _parts[preview.key] = preview + _parts.move_to_end(preview.key) + while len(_parts) > _CACHE_PARTS: + _parts.popitem(last=False) + return preview + + +def _recall(key: str) -> PartPreview | None: + with _parts_lock: + preview = _parts.get(key) + if preview is not None: + _parts.move_to_end(key) + return preview + + +def clear_preview_cache() -> None: + with _parts_lock: + _parts.clear() + + +def part_key(solid: Solid, linear_tolerance: float, angular_tolerance: float) -> str: + """A key for the geometry of *solid* at a tolerance: its BREP content, not its identity. + + Remembered on the solid, so asking again about the same object is free. + """ + tolerance = f"{float(linear_tolerance).hex()}:{float(angular_tolerance).hex()}" + remembered = solid._get_runtime("preview.key", None) + if isinstance(remembered, tuple) and remembered[0] == tolerance: + return remembered[1] + digest = hashlib.sha256(_bintools_bytes(solid.wrapped)).hexdigest()[:24] + key = f"{digest}-{hashlib.sha256(tolerance.encode()).hexdigest()[:6]}" + solid._set_runtime("preview.key", (tolerance, key)) + return key + + +# -- one solid ----------------------------------------------------------------------- + + +def preview_solid( + solid: Solid, + *, + linear_tolerance: float = DEFAULT_LINEAR_TOLERANCE, + angular_tolerance: float = DEFAULT_ANGULAR_TOLERANCE, +) -> PartPreview: + """The preview geometry of *solid*, from the cache if its content was seen at this tolerance.""" + + key = part_key(solid, linear_tolerance, angular_tolerance) + known = _recall(key) + if known is not None: + return known + mesh = cached_mesh(solid, linear_tolerance=linear_tolerance, angular_tolerance=angular_tolerance) + if mesh is None: + raise PreviewError("the solid has no mesh") + triangles = np.asarray(mesh.triangles, dtype=np.int64) + corner_normals = np.asarray(mesh.corner_normals, dtype=np.float64) + vertices = np.asarray(mesh.vertices, dtype=np.float64) + positions, normals, indices = _vertex_buffers(vertices, triangles, corner_normals) + face_groups = tuple( + (str(item.source_topo_id), item.start * 3, item.count * 3) + for item in mesh.face_triangle_ranges + if item.count > 0 + ) + + edges = solid._iter_edges() + edge_mesh = build_edge_mesh( + solid, + edge_entity_ids=[edge.topo_id for edge in edges], + linear_tolerance=linear_tolerance, + angular_tolerance=angular_tolerance, + ) + edge_positions = np.asarray(edge_mesh.positions, dtype=np.float32).reshape((-1, 3)) + edge_indices = np.asarray(edge_mesh.indices, dtype=np.uint32) + edge_groups = tuple((group.entity_id, group.first_index, group.index_count) for group in edge_mesh.groups) + + unique: dict[str, Any] = {} + for edge in edges: + for vertex in edge._iter_vertices(): + unique.setdefault(vertex.topo_id, vertex) + vertex_ids = tuple(unique) + vertex_positions = _gltf(np.asarray([list(_vec(unique[i].get_coordinates())) for i in vertex_ids], dtype=np.float64).reshape((-1, 3))) + + return _remember( + PartPreview( + key=key, + positions=positions, + normals=normals, + indices=indices, + face_groups=face_groups, + edge_positions=edge_positions, + edge_indices=edge_indices, + edge_groups=edge_groups, + vertex_positions=vertex_positions.astype(np.float32), + vertex_ids=vertex_ids, + solid_topo_id=str(solid.topo_id), + triangle_count=int(len(triangles)), + ) + ) + + +def _gltf(points: np.ndarray) -> np.ndarray: + """CAD millimetres (x, y, z) to glTF metres (x, z, -y).""" + return np.stack([points[:, 0], points[:, 2], -points[:, 1]], axis=1) / 1000.0 + + +def _gltf_direction(vectors: np.ndarray) -> np.ndarray: + return np.stack([vectors[:, 0], vectors[:, 2], -vectors[:, 1]], axis=1) + + +def _vertex_buffers( + vertices: np.ndarray, triangles: np.ndarray, corner_normals: np.ndarray +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Per-vertex position and normal buffers and the index buffer. + + The mesh keeps a normal for each triangle corner. Where every corner that + uses a vertex agrees on it (the usual case: a face's vertices are its own) + the vertex keeps that normal; otherwise vertices are split by (position, + normal). + """ + if len(triangles) == 0: + empty = np.zeros((0, 3), dtype=np.float32) + return empty, empty, np.zeros(0, dtype=np.uint32) + flat = triangles.reshape(-1) + corners = corner_normals.reshape(-1, 3) + per_vertex = np.zeros((len(vertices), 3), dtype=np.float64) + per_vertex[flat] = corners + if np.abs(per_vertex[flat] - corners).max() <= 1e-9: + return ( + _gltf(vertices).astype(np.float32), + _gltf_direction(per_vertex).astype(np.float32), + flat.astype(np.uint32), + ) + combined = np.concatenate([vertices[flat], corners], axis=1) + unique, inverse = np.unique(combined, axis=0, return_inverse=True) + return ( + _gltf(unique[:, :3]).astype(np.float32), + _gltf_direction(unique[:, 3:]).astype(np.float32), + inverse.reshape(-1).astype(np.uint32), + ) + + +# -- a product ----------------------------------------------------------------------- + + +def compile_preview( + product: Part | Assembly, + *, + linear_tolerance: float = DEFAULT_LINEAR_TOLERANCE, + angular_tolerance: float = DEFAULT_ANGULAR_TOLERANCE, +) -> PreviewScene: + """Preview *product*: every part once, every instance with its placement.""" + + parts: dict[str, PartPreview] = {} + solids: dict[str, Solid] = {} + definitions: dict[str, str] = {} + nodes: list[PreviewNode] = [] + + def add(part: Part, path: tuple[str, ...], placement: Placement, name: str) -> None: + body = part.body + if not isinstance(body, Solid): + raise PreviewError(f"part {part.part_id!r} has no solid body") + preview = preview_solid(body, linear_tolerance=linear_tolerance, angular_tolerance=angular_tolerance) + parts[preview.key] = preview + solids.setdefault(preview.key, body) + definitions.setdefault(preview.key, part.part_id) + nodes.append( + PreviewNode( + node_id="node/" + "/".join(path), + part_key=preview.key, + definition_id=part.part_id, + display_name=name, + placement=placement, + ) + ) + + def visit(item: Part | Assembly, path: tuple[str, ...], placement: Placement, name: str) -> None: + if isinstance(item, Part): + add(item, path, placement, name) + return + for component in item.components: + visit( + component.item, + path + (component.component_id,), + placement.compose(component.placement), + component.name or component.component_id, + ) + + root_id = product.part_id if isinstance(product, Part) else product.assembly_id + visit(product, (root_id,), identity_placement(), root_id) + if not nodes: + raise PreviewError("the product has nothing to draw") + return PreviewScene(parts=parts, nodes=nodes, solids=solids, definitions=definitions) + + +# -- the wire format ----------------------------------------------------------------- + + +def encode_preview(scene: PreviewScene) -> bytes: + """Serialize *scene*: ``SCAPREV1``, a header length, a JSON header, then the arrays. + + The header lists the parts (each array as an ``offset`` and ``count`` into the + data that follows), the face and edge groups, and the nodes. Arrays are + little-endian ``float32`` (positions, normals) and ``uint32`` (indices). + """ + chunks: list[bytes] = [] + cursor = 0 + + def array(values: np.ndarray, dtype: str) -> dict[str, int]: + nonlocal cursor + data = np.ascontiguousarray(values, dtype=dtype).tobytes() + entry = {"offset": cursor, "count": int(values.size)} + chunks.append(data) + cursor += len(data) + return entry + + parts: list[dict[str, Any]] = [] + for key, part in scene.parts.items(): + definition = scene.definitions[key] + prefix = f"entity/{definition}/" + parts.append( + { + "key": key, + "definition_id": definition, + "positions": array(part.positions, " dict[str, Any]: + """The header of an encoded preview (the arrays follow it).""" + if not data.startswith(MAGIC): + raise PreviewError("not a preview") + (length,) = struct.unpack_from(" dict[str, Any] | None: + """The geometric properties of the entity *entity_id* of a part of *scene*. + + ``entity///``; returns what the package's entity + sidecar holds for it (``properties``, ``geometry``, ``tags``) except the + ``source`` links into the feature graph, or ``None`` if there is no such entity. + """ + _, _, rest = entity_id.partition("entity/") + definition, _, tail = rest.partition("/") + kind, _, topo_id = tail.partition("/") + key = next((k for k, d in scene.definitions.items() if d == definition), None) + if key is None: + return None + solid = scene.solids[key] + if kind == "solid": + return { + "entity_id": entity_id, + "kind": "solid", + "topo_id": solid.topo_id, + "geometry": {"type": "brep_solid"}, + "properties": { + "bounds": _bounds_document(solid_asset_bounds(solid)), + "volume": solid.get_volume(), + "surface_area": sum(face.get_area() for face in solid._iter_faces()), + "centroid": list(_vec(center_of_mass(solid.wrapped))), + }, + "tags": sorted(solid._list_tags()), + } + if kind == "face": + face = next((f for f in solid._iter_faces() if f.topo_id == topo_id), None) + if face is None: + return None + center = _vec(face.get_center()) + return { + "entity_id": entity_id, + "kind": "face", + "topo_id": topo_id, + "geometry": _surface_geometry(face), + "properties": { + "bounds": _bounds_document(_shape_bounds(face)), + "area": face.get_area(), + "centroid": list(center), + "orientation": _orientation(face.wrapped.Orientation()), + "connector_frame": _frame(center, _vec(face.get_normal_at())), + }, + "tags": sorted(face._list_tags()), + } + if kind == "edge": + edge = next((e for e in solid._iter_edges() if e.topo_id == topo_id), None) + if edge is None: + return None + start, end = _edge_endpoints(edge) + frame = None + if start is not None and end is not None and start != end: + direction = _normalize(tuple(end[i] - start[i] for i in range(3))) + frame = _frame(_vec(edge.get_center()), direction) + return { + "entity_id": entity_id, + "kind": "edge", + "topo_id": topo_id, + "geometry": _curve_geometry(edge), + "properties": { + "bounds": _bounds_document(_shape_bounds(edge)), + "length": edge.get_length(), + "centroid": list(_vec(edge.get_center())), + "connector_frame": frame, + }, + "tags": sorted(edge._list_tags()), + } + if kind == "vertex": + for edge in solid._iter_edges(): + for vertex in edge._iter_vertices(): + if vertex.topo_id == topo_id: + point = list(_vec(vertex.get_coordinates())) + return { + "entity_id": entity_id, + "kind": "vertex", + "topo_id": topo_id, + "geometry": {"type": "point", "position": point}, + "properties": {"bounds": {"min": point, "max": point}, "position": point}, + "tags": sorted(vertex._list_tags()), + } + return None + return None + + +__all__ = [ + "MAGIC", + "PREVIEW_SCHEMA", + "PartPreview", + "PreviewError", + "PreviewNode", + "PreviewScene", + "clear_preview_cache", + "compile_preview", + "decode_preview_header", + "encode_preview", + "entity_info", + "part_key", + "preview_solid", +] diff --git a/src/simplecadapi/scene/render_mesh.py b/src/simplecadapi/scene/render_mesh.py index d7a27f79..04c061f6 100644 --- a/src/simplecadapi/scene/render_mesh.py +++ b/src/simplecadapi/scene/render_mesh.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import Iterable, Sequence +import numpy as np + from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.GCPnts import GCPnts_TangentialDeflection from OCP.TopAbs import TopAbs_FORWARD, TopAbs_REVERSED @@ -219,43 +221,57 @@ def _triangle_block( face_range, entity_id: str, ) -> CanonicalTriangleBlock: - vertex_records: dict[bytes, tuple[tuple[float, float, float], tuple[float, float, float]]] = {} - triangle_keys: list[tuple[bytes, bytes, bytes]] = [] - triangle_start = face_range.start - triangle_end = triangle_start + face_range.count - for triangle_index in range(triangle_start, triangle_end): - node_indices = tuple(int(index) for index in mesh.triangles[triangle_index]) - positions = tuple(cad_to_gltf(mesh.vertices[index]) for index in node_indices) - if len(set(_position_key(position) for position in positions)) != 3: - continue - cross = profile_cross( - tuple(positions[1][axis] - positions[0][axis] for axis in range(3)), - tuple(positions[2][axis] - positions[0][axis] for axis in range(3)), - ) - if all(component == 0.0 for component in cross): - continue - fallback_normal = profile_normalize(cross) - keys: list[bytes] = [] - for corner in range(3): - raw_normal = tuple( - float(value) for value in mesh.corner_normals[triangle_index, corner] - ) - normal = ( - fallback_normal - if all(component == 0.0 for component in raw_normal) - else profile_normalize(cad_direction_to_gltf(raw_normal)) - ) - position = positions[corner] - key = _vertex_key(position, normal) - vertex_records[key] = (position, normal) - keys.append(key) - triangle_keys.append(_least_cyclic_rotation(tuple(keys))) - if not triangle_keys: + """One face's canonical triangle block. + + Every value follows the profile's fixed steps: binary64 arithmetic in a + fixed order, then rounding to binary32 with ``-0`` written as ``+0`` + (:func:`~simplecadapi.scene.glb.profile_f32`). They are evaluated here for + all of the face's triangles at once, with numpy, which performs the same + IEEE operations in the same order as the scalar helpers (and never fuses a + multiply and an add), so the bytes are identical, at a fraction of the cost. + """ + + start = face_range.start + stop = start + face_range.count + corners = np.asarray(mesh.triangles[start:stop], dtype=np.int64) + vertices = np.asarray(mesh.vertices, dtype=np.float64) + positions = _cad_to_gltf_rows(vertices[corners]) # (n, 3 corners, 3 axes) + + # A triangle whose corners collapse to fewer than three distinct positions + # (after binary32 rounding) is dropped. + distinct = ( + (positions[:, 0] != positions[:, 1]).any(axis=1) + & (positions[:, 0] != positions[:, 2]).any(axis=1) + & (positions[:, 1] != positions[:, 2]).any(axis=1) + ) + keep = np.flatnonzero(distinct) + positions = positions[keep] + a, b, c = positions[:, 0], positions[:, 1], positions[:, 2] + cross = _cross_rows(b - a, c - a) + nonzero = ~(cross == 0.0).all(axis=1) + keep = keep[nonzero] + positions = positions[nonzero] + cross = cross[nonzero] + if len(keep) == 0: raise ValueError(f"face {entity_id} has no non-degenerate triangle") + + fallback = _normalize_rows(cross) + raw = np.asarray(mesh.corner_normals[start:stop], dtype=np.float64)[keep] # (n, 3 corners, 3) + given = ~(raw == 0.0).all(axis=2) # corners that carry a normal of their own + normals = np.broadcast_to(fallback[:, None, :], raw.shape).copy() + if given.any(): + normals[given] = _normalize_rows(_cad_to_gltf_rows(raw[given])) + + # One 24-byte key per corner: position bits, then normal bits. + packed = np.concatenate([positions, normals], axis=2).astype(np.float32).astype(" "np.ndarray": + """:func:`profile_f32` of every value: binary32 rounding, ``-0`` as ``+0`` (as binary64).""" + + if not np.isfinite(values).all(): + raise ValueError("f32 input must be a finite number") + with np.errstate(over="ignore"): + narrow = values.astype(np.float32) + if not np.isfinite(narrow).all(): + raise ValueError("f32 conversion overflowed") + return np.where(narrow == 0, np.float32(0.0), narrow).astype(np.float64) + + +def _cad_to_gltf_rows(points: "np.ndarray") -> "np.ndarray": + """:func:`cad_to_gltf` of every point (the last axis is x, y, z).""" + + return _f32_rows( + np.stack( + [points[..., 0] / 1000.0, points[..., 2] / 1000.0, (-points[..., 1]) / 1000.0], + axis=-1, + ) + ) + + +def _cross_rows(left: "np.ndarray", right: "np.ndarray") -> "np.ndarray": + """:func:`profile_cross` of every pair, in its fixed operation order.""" + + p1x, p2x = left[:, 1] * right[:, 2], left[:, 2] * right[:, 1] + p1y, p2y = left[:, 2] * right[:, 0], left[:, 0] * right[:, 2] + p1z, p2z = left[:, 0] * right[:, 1], left[:, 1] * right[:, 0] + result = np.stack([p1x - p2x, p1y - p2y, p1z - p2z], axis=1) + if not np.isfinite(result).all(): + raise ValueError("cross product produced a non-finite value") + return result + + +def _normalize_rows(vectors: "np.ndarray") -> "np.ndarray": + """:func:`profile_normalize` of every vector.""" + + if not np.isfinite(vectors).all(): + raise ValueError("normalize input must contain finite values") + squared = vectors[:, 0] * vectors[:, 0] + vectors[:, 1] * vectors[:, 1] + squared = squared + vectors[:, 2] * vectors[:, 2] + if not (np.isfinite(squared).all() and (squared > 0).all()): + raise ValueError("normalize input has zero or non-finite length") + result = _f32_rows(vectors / np.sqrt(squared)[:, None]) + norm_squared = result[:, 0] * result[:, 0] + result[:, 1] * result[:, 1] + norm_squared = norm_squared + result[:, 2] * result[:, 2] + norm = np.sqrt(norm_squared) + if not ((norm >= 1.0 - 1e-6) & (norm <= 1.0 + 1e-6)).all(): + raise ValueError("normalized binary32 vector is outside the profile tolerance") + return result + + def _edge_block( edge, *, diff --git a/src/simplecadapi/sketch.py b/src/simplecadapi/sketch.py index 2cc907ba..4e7200d7 100644 --- a/src/simplecadapi/sketch.py +++ b/src/simplecadapi/sketch.py @@ -519,8 +519,10 @@ def add_bspline( """Add a B-spline whose poles participate in sketch solving. Control points may be literal 2-D coordinates or refs to points in - this sketch. The first and last poles always share the declared - start/end point entities. + this sketch. For nonperiodic curves the first and last poles share + the declared start/end point entities. A closed curve may use one + seam point for both endpoints; a periodic seam does not replace its + control poles, which generally lie away from the seam. """ from .operators.geometry import ( _normalize_bspline_knots, @@ -529,8 +531,6 @@ def add_bspline( start_id = self.resolve_point_id(start) end_id = self.resolve_point_id(end) - if start_id == end_id: - raise ValueError("A sketch bspline requires two distinct endpoint refs") if isinstance(degree, bool) or int(degree) != degree: raise ValueError("degree must be an integer") degree_value = int(degree) @@ -591,18 +591,19 @@ def resolved_control_point(point: Any) -> List[float]: end_entity = self.entities[end_id] start_point = [_as_float(start_entity.data["x"]), _as_float(start_entity.data["y"])] end_point = [_as_float(end_entity.data["x"]), _as_float(end_entity.data["y"])] - if isinstance(normalized_cps[0], Mapping): + if not periodic and isinstance(normalized_cps[0], Mapping): if normalized_cps[0]["point_id"] != start_id: raise ValueError("The first B-spline control-point ref must match start") - else: + elif not periodic: normalized_cps[0] = start_point - if isinstance(normalized_cps[-1], Mapping): + if not periodic and isinstance(normalized_cps[-1], Mapping): if normalized_cps[-1]["point_id"] != end_id: raise ValueError("The last B-spline control-point ref must match end") - else: + elif not periodic: normalized_cps[-1] = end_point - resolved_cps[0] = start_point - resolved_cps[-1] = end_point + if not periodic: + resolved_cps[0] = start_point + resolved_cps[-1] = end_point self._add_entity( SketchEntity( entity_id, diff --git a/src/simplecadapi/sketch_solver/py_slvs_backend.py b/src/simplecadapi/sketch_solver/py_slvs_backend.py index c364789f..1a7c6348 100644 --- a/src/simplecadapi/sketch_solver/py_slvs_backend.py +++ b/src/simplecadapi/sketch_solver/py_slvs_backend.py @@ -234,9 +234,9 @@ def _add_bspline_entity( if isinstance(point, Mapping) else None ) - if point_id is None and index == 0: + if point_id is None and index == 0 and not data.get("periodic", False): point_id = str(data["start"]) - elif point_id is None and index == len(raw_points) - 1: + elif point_id is None and index == len(raw_points) - 1 and not data.get("periodic", False): point_id = str(data["end"]) if point_id is not None: endpoint = self.point_handles[str(point_id)] diff --git a/src/simplecadapi/skill/cli.py b/src/simplecadapi/skill/cli.py index c10c4fc2..8073c898 100644 --- a/src/simplecadapi/skill/cli.py +++ b/src/simplecadapi/skill/cli.py @@ -15,6 +15,13 @@ def configure(subparsers: argparse._SubParsersAction) -> None: ) commands = skill.add_subparsers(dest="command", required=True) commands.add_parser("targets", help="list supported harness targets") + where = commands.add_parser( + "where", help="print where `install` would put the skill (nothing is written)" + ) + where.add_argument( + "--skills-dir", type=Path, + help="skills parent directory (default: config/env or ~/.agents/skills)", + ) install = commands.add_parser( "install", help="compile the bundled skill into /" ) @@ -37,6 +44,9 @@ def _run(args: argparse.Namespace) -> tuple[dict[str, Any], int]: if args.command == "targets": return {"targets": list(project.targets)}, 0 skills_dir = resolve_paths(skills_dir=args.skills_dir).skills_dir + if args.command == "where": + destination = skills_dir.expanduser().absolute() / project.name + return {"where": str(destination), "exists": destination.exists()}, 0 report = build_skill( project=project, target=args.target, @@ -50,6 +60,8 @@ def _run(args: argparse.Namespace) -> tuple[dict[str, Any], int]: def _print(report: dict[str, Any]) -> None: if "targets" in report: print("\n".join(report["targets"])) + elif "where" in report: + print(report["where"]) else: print( f"installed {report['name']} for {report['target']}: " diff --git a/src/simplecadapi/topology/model.py b/src/simplecadapi/topology/model.py index 65635103..c88e8b7b 100644 --- a/src/simplecadapi/topology/model.py +++ b/src/simplecadapi/topology/model.py @@ -648,6 +648,8 @@ def _node_display_category(op: str) -> str: "mirror", "make_translate_rshape", "make_rotate_rshape", + "make_scale_rsolid", + "make_transform_rsolid", "make_mirror_rshape", }: return "transform" @@ -664,6 +666,9 @@ def _node_display_category(op: str) -> str: "make_loft_rsolid", "make_sweep_rsolid", "make_twisted_sweep_rsolid", + "make_extrude_taper_rsolid", + "make_extrude_until_rsolid", + "make_sweep_multisection_rsolid", }: return "feature" if op in { @@ -673,6 +678,8 @@ def _node_display_category(op: str) -> str: "make_fillet_rsolid", "make_chamfer_rsolid", "make_shell_rsolid", + "make_offset_rsolid", + "make_draft_rsolid", }: return "detail" if op.startswith("make_"): diff --git a/test/build123d_traces/README.md b/test/build123d_traces/README.md new file mode 100644 index 00000000..bb03c7e8 --- /dev/null +++ b/test/build123d_traces/README.md @@ -0,0 +1,18 @@ +# Portable build123d feature traces + +These fixtures contain the actual operation inputs captured from six of the +50 build123d examples supplied for the converter: lines and arcs, a selected +circular fillet, a mirrored profile, one closed font glyph with a hole, and a +nonplanar spring sweep, and a spherical cut/fillet followed by nonuniform scale. +The glyph is feature `s3` of the text example; the affine case retains the first +eight features of the keycap example. Other traces retain their entire history. + +The tests require SimpleCADAPI but do not import build123d, download source +programs, load result BReps, or use paths outside the repository. Curve data +contains exact analytic geometry or B-spline poles, knots, multiplicities, +and weights; it is not a mesh or a fitted final-body snapshot. + +Reference volumes use OCCT's adaptive Gauss–Kronrod integration with +`eps=1e-9` and B-spline span splitting. Original non-adaptive volumes remain +as diagnostic data because they differ materially on closed rational curves. +Each fixture records its build123d and source-kernel versions. diff --git a/test/build123d_traces/affine_trimmed_sphere.trace.json b/test/build123d_traces/affine_trimmed_sphere.trace.json new file mode 100644 index 00000000..1ed6153d --- /dev/null +++ b/test/build123d_traces/affine_trimmed_sphere.trace.json @@ -0,0 +1,820 @@ +{ + "schema_version": "1.0", + "steps": [ + { + "id": "s1", + "op": "extrude_taper", + "inputs": [], + "parameters": { + "direction": [ + 0.0, + 0.0, + 10.0 + ], + "taper": 15, + "flip_inner": true + }, + "source": "Solid.extrude_taper", + "result": { + "volume": 2371.1119335472795, + "volume_gk": 2371.1119335472795, + "area": 1118.2335726410245, + "default_volume": 2371.11193354728, + "bbox": [ + -9.0, + -9.0, + 0.0, + 9.0, + 9.0, + 10.000000000000005 + ], + "solid_count": 1 + }, + "profile": { + "kind": "face", + "origin": [ + -2.0816681711721685e-17, + -2.4671622769447924e-16, + 0.0 + ], + "x_dir": [ + 1.0, + 0.0, + -0.0 + ], + "y_dir": [ + -0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + 1.0 + ], + "wires": [ + { + "edges": [ + { + "kind": "line", + "start": [ + -9.0, + 9.0, + 0.0 + ], + "end": [ + -9.0, + -9.0, + 0.0 + ], + "parameter_range": [ + -9.0, + 9.0 + ], + "reversed": true + }, + { + "kind": "line", + "start": [ + -9.0, + -9.0, + 0.0 + ], + "end": [ + 9.0, + -9.0, + 0.0 + ], + "parameter_range": [ + -9.0, + 9.0 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + 9.0, + -9.0, + 0.0 + ], + "end": [ + 9.0, + 9.0, + 0.0 + ], + "parameter_range": [ + -9.0, + 9.0 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + 9.0, + 9.0, + 0.0 + ], + "end": [ + -9.0, + 9.0, + 0.0 + ], + "parameter_range": [ + -9.0, + 9.0 + ], + "reversed": true + } + ], + "closed": true + } + ] + } + }, + { + "id": "s2", + "op": "sphere", + "inputs": [], + "parameters": { + "radius": 40, + "plane": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x_dir": [ + 1.0, + 0.0, + -0.0 + ], + "y_dir": [ + -0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + 1.0 + ] + }, + "angle1": -90, + "angle2": 90, + "angle3": 360 + }, + "source": "Solid.make_sphere", + "result": { + "volume": 268082.57310632826, + "volume_gk": 268082.57310632826, + "area": 20106.192982974666, + "default_volume": 268082.5731063291, + "bbox": [ + -40.0, + -40.0, + -40.0, + 40.0, + 40.0, + 40.0 + ], + "solid_count": 1 + } + }, + { + "id": "s3", + "op": "transform", + "inputs": [ + "s2" + ], + "parameters": { + "loc": { + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + -0.0 + ], + [ + 0.0, + 1.0, + 0.0, + -0.0 + ], + [ + 0.0, + 0.0, + 1.0, + -0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "method": "move", + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + -0.0 + ], + [ + 0.0, + 1.0, + 0.0, + -0.0 + ], + [ + 0.0, + 0.0, + 1.0, + -0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "source": "Shape.move", + "result": { + "volume": 268082.57310632826, + "volume_gk": 268082.57310632826, + "area": 20106.192982974666, + "default_volume": 268082.5731063291, + "bbox": [ + -40.0, + -40.0, + -40.0, + 40.0, + 40.0, + 40.0 + ], + "solid_count": 1 + } + }, + { + "id": "s4", + "op": "transform", + "inputs": [ + "s3" + ], + "parameters": { + "loc": { + "matrix": [ + [ + 1.0, + -0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 2.220446049250313e-16, + -1.0, + 0.0 + ], + [ + 0.0, + 1.0, + 2.220446049250313e-16, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "method": "moved", + "matrix": [ + [ + 1.0, + -0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 2.220446049250313e-16, + -1.0, + 0.0 + ], + [ + 0.0, + 1.0, + 2.220446049250313e-16, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "source": "Shape.moved", + "result": { + "volume": 268082.5731063284, + "volume_gk": 268082.5731063284, + "area": 20106.192982974666, + "default_volume": 268082.5731063291, + "bbox": [ + -40.0, + -40.0, + -40.0, + 40.0, + 40.0, + 40.0 + ], + "solid_count": 1 + } + }, + { + "id": "s5", + "op": "transform", + "inputs": [ + "s4" + ], + "parameters": { + "loc": { + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 1.0, + 0.0, + -3.0 + ], + [ + 0.0, + 0.0, + 1.0, + 47.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "method": "moved", + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 1.0, + 0.0, + -3.0 + ], + [ + 0.0, + 0.0, + 1.0, + 47.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "source": "Shape.moved", + "result": { + "volume": 268082.5731063284, + "volume_gk": 268082.5731063284, + "area": 20106.192982974666, + "default_volume": 268082.5731063291, + "bbox": [ + -40.0, + -43.0, + 7.0, + 40.0, + 37.0, + 87.0 + ], + "solid_count": 1 + } + }, + { + "id": "s6", + "op": "cut", + "inputs": [ + "s1", + "s5" + ], + "parameters": {}, + "source": "Shape.cut", + "result": { + "volume": 1929.8444343829303, + "volume_gk": 1929.8444343829303, + "area": 1035.6152278365398, + "default_volume": 1929.844434382934, + "bbox": [ + -9.0, + -9.0, + 0.0, + 9.0, + 9.0, + 8.756864140153603 + ], + "solid_count": 1 + } + }, + { + "id": "s7", + "op": "fillet", + "inputs": [ + "s6" + ], + "parameters": { + "radius": 1 + }, + "source": "Mixin3D.fillet", + "result": { + "volume": 1914.6009359753375, + "volume_gk": 1914.6009359753375, + "area": 1002.7137487227009, + "default_volume": 1914.6009359753382, + "bbox": [ + -9.000000100000001, + -9.000000100000001, + -1e-07, + 9.000000100000001, + 9.000000100000001, + 8.441500309937386 + ], + "solid_count": 1 + }, + "selection": [ + { + "kind": "edge", + "geom_type": "LINE", + "center": [ + -7.955513473192318, + -7.955513473192318, + 3.898076785867421 + ], + "bbox": [ + -9.0, + -9.0, + 0.0, + -6.911026946384637, + -6.911026946384637, + 7.796153571734842 + ], + "length": 8.337123445861655, + "start": [ + -6.911026946384637, + -6.911026946384637, + 7.796153571734842 + ], + "mid": [ + -7.955513473192318, + -7.955513473192318, + 3.898076785867421 + ], + "end": [ + -9.0, + -9.0, + 0.0 + ], + "index": 4 + }, + { + "kind": "edge", + "geom_type": "LINE", + "center": [ + 7.955513473192318, + -7.955513473192318, + 3.898076785867421 + ], + "bbox": [ + 6.911026946384637, + -9.0, + 0.0, + 9.0, + -6.911026946384637, + 7.796153571734842 + ], + "length": 8.337123445861655, + "start": [ + 6.911026946384637, + -6.911026946384637, + 7.796153571734842 + ], + "mid": [ + 7.955513473192318, + -7.955513473192318, + 3.898076785867421 + ], + "end": [ + 9.0, + -9.0, + 0.0 + ], + "index": 7 + }, + { + "kind": "edge", + "geom_type": "LINE", + "center": [ + -7.826802662708394, + 7.826802662708394, + 4.378432070076798 + ], + "bbox": [ + -9.0, + 6.653605325416787, + 0.0, + -6.653605325416787, + 9.0, + 8.756864140153596 + ], + "length": 9.364497077095645, + "start": [ + -9.0, + 9.0, + 0.0 + ], + "mid": [ + -7.826802662708394, + 7.826802662708394, + 4.378432070076798 + ], + "end": [ + -6.653605325416787, + 6.653605325416787, + 8.756864140153596 + ], + "index": 6 + }, + { + "kind": "edge", + "geom_type": "LINE", + "center": [ + 7.826802662708394, + 7.826802662708394, + 4.378432070076798 + ], + "bbox": [ + 6.653605325416787, + 6.653605325416787, + 0.0, + 9.0, + 9.0, + 8.756864140153596 + ], + "length": 9.364497077095645, + "start": [ + 6.653605325416787, + 6.653605325416787, + 8.756864140153596 + ], + "mid": [ + 7.826802662708394, + 7.826802662708394, + 4.378432070076798 + ], + "end": [ + 9.0, + 9.0, + 0.0 + ], + "index": 9 + }, + { + "kind": "edge", + "geom_type": "CIRCLE", + "center": [ + 2.2364273360068107e-14, + -7.016130216452036, + 7.40390282780167 + ], + "bbox": [ + -6.911026946384598, + -7.068763296161196, + 7.207473500168263, + 6.911026946384642, + -6.911026946384634, + 7.796153571734848 + ], + "length": 13.893601237415288, + "start": [ + -6.911026946384598, + -6.911026946384636, + 7.796153571734841 + ], + "mid": [ + 2.2373044540339126e-14, + -7.068763296161196, + 7.207473500168263 + ], + "end": [ + 6.911026946384642, + -6.911026946384634, + 7.796153571734848 + ], + "index": 8 + }, + { + "kind": "edge", + "geom_type": "CIRCLE", + "center": [ + -6.882861213849936, + -0.10022652368490893, + 7.9012695165867335 + ], + "bbox": [ + -6.960821167729055, + -6.911026946384645, + 7.610319007754136, + -6.653605325416786, + 6.6536053254167875, + 8.756864140153603 + ], + "length": 13.667946481986268, + "start": [ + -6.653605325416786, + 6.6536053254167875, + 8.756864140153603 + ], + "mid": [ + -6.933207795109661, + -0.08596340420454629, + 7.713373517338049 + ], + "end": [ + -6.911026946384634, + -6.911026946384645, + 7.796153571734848 + ], + "index": 5 + }, + { + "kind": "edge", + "geom_type": "CIRCLE", + "center": [ + 6.882861213849936, + -0.10022652368489449, + 7.901269516586735 + ], + "bbox": [ + 6.653605325416786, + -6.911026946384631, + 7.610319007754136, + 6.960821167729055, + 6.653605325416802, + 8.756864140153603 + ], + "length": 13.667946481986268, + "start": [ + 6.911026946384634, + -6.911026946384631, + 7.796153571734848 + ], + "mid": [ + 6.933207795109661, + -0.08596340420453208, + 7.713373517338049 + ], + "end": [ + 6.653605325416786, + 6.653605325416802, + 8.756864140153603 + ], + "index": 10 + }, + { + "kind": "edge", + "geom_type": "CIRCLE", + "center": [ + -4.503045217372866e-14, + 6.74972397417161, + 8.398144459445728 + ], + "bbox": [ + -6.653605325416833, + 6.653605325416785, + 8.218533741391056, + 6.653605325416744, + 6.7978505210263345, + 8.756864140153603 + ], + "length": 13.369366777347004, + "start": [ + 6.653605325416744, + 6.653605325416788, + 8.756864140153588 + ], + "mid": [ + -4.531965428549345e-14, + 6.7978505210263345, + 8.218533741391056 + ], + "end": [ + -6.653605325416833, + 6.653605325416785, + 8.756864140153603 + ], + "index": 11 + } + ] + }, + { + "id": "s8", + "op": "transform", + "inputs": [ + "s7" + ], + "parameters": { + "factor": [ + 0.925, + 0.925, + 0.85 + ], + "about": [ + 0.0, + 0.0, + 0.0 + ], + "method": "scale" + }, + "source": "Shape.scale", + "result": { + "volume": 1392.4533674204192, + "volume_gk": 1392.4533674204192, + "area": 826.3818860013073, + "default_volume": 1392.4533674401873, + "bbox": [ + -8.325000100000002, + -8.325000100000002, + -1e-07, + 8.325000100000002, + 8.325000100000002, + 7.175275363446779 + ], + "solid_count": 1 + } + } + ], + "result_refs": [ + "s8" + ], + "b3d_status": "ok", + "b3d_volume": 1392.4533674204192, + "b3d_bbox": [ + -8.325000100000002, + -8.325000100000002, + -1e-07, + 8.325000100000002, + 8.325000100000002, + 7.175275363446779 + ], + "solid_count": 1, + "unsupported": [], + "error": "", + "b3d_default_volume": 1392.4533674401873, + "b3d_volume_gk": 1392.4533674204192, + "b3d_valid": true, + "b3d_area": 826.3818860013073, + "source_file": "models/45_key_cap.py", + "source_sha256": "3e3683ba02aff7a4d0baca83532875541030b095d2e46b4de7307437e384a6b4", + "build123d_version": "0.13.0", + "stem": "affine_trimmed_sphere", + "source_metadata": { + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/key_cap.py", + "source_sha256": "0e3b3e77b88e488ef093b308415072dddded3e971e72081bf1c0a51ad9ce2e73", + "export_options": { + "precision_mode": "AVERAGE", + "write_pcurves": true + } + }, + "extraction_note": "First eight real feature operations of model 45, through affine scale; source provenance retained." +} diff --git a/test/build123d_traces/closed_text_outline.trace.json b/test/build123d_traces/closed_text_outline.trace.json new file mode 100644 index 00000000..f44eb7fc --- /dev/null +++ b/test/build123d_traces/closed_text_outline.trace.json @@ -0,0 +1,311 @@ +{ + "schema_version": "1.0", + "steps": [ + { + "id": "s3", + "op": "extrude", + "inputs": [], + "parameters": { + "direction": [ + 0.0, + 0.0, + 4.0 + ] + }, + "source": "Solid.extrude", + "result": { + "volume": 289.73641490428105, + "volume_gk": 289.73641490428105, + "default_volume": 290.6612252204212, + "bbox": [ + 14.874267428125, + -7.713174450168028e-15, + 4.9999999, + 27.020263721875, + 13.549804887499992, + 9.0000001 + ], + "solid_count": 1 + }, + "profile": { + "kind": "face", + "origin": [ + 20.952972409981427, + 6.772877390140605, + 5.0 + ], + "x_dir": [ + 1.0, + 0.0, + -0.0 + ], + "y_dir": [ + -0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + 1.0 + ], + "wires": [ + { + "edges": [ + { + "kind": "bspline", + "start": [ + 16.876220653125, + 12.109375099999992, + 5.0 + ], + "end": [ + 16.876220653125, + 12.109375099999992, + 5.0 + ], + "parameter_range": [ + 0.0, + 6.5628065631728365 + ], + "reversed": false, + "degree": 2, + "poles": [ + [ + 16.876220653125, + 12.109375099999992, + 5.0 + ], + [ + 14.874267528125, + 10.375976662499992, + 5.0 + ], + [ + 14.874267528125, + 3.4912110374999923, + 5.0 + ], + [ + 18.231201121875, + 9.999999228682555e-08, + 5.0 + ], + [ + 22.650146434375, + 9.999999228682555e-08, + 5.0 + ], + [ + 25.518798778125, + 1.5869141624999923, + 5.0 + ], + [ + 27.020263621875, + 4.455566506249992, + 5.0 + ], + [ + 27.020263621875, + 10.046386818749992, + 5.0 + ], + [ + 23.626708934375, + 13.549804787499992, + 5.0 + ], + [ + 18.548583934375, + 13.549804787499992, + 5.0 + ], + [ + 16.876220653125, + 12.109375099999992, + 5.0 + ] + ], + "weights": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "knots": [ + 0.0, + 1.0, + 1.911864406779661, + 2.823728813559322, + 3.39211066352512, + 3.9604925134909177, + 4.5288743634567155, + 5.230340744146212, + 5.931807124835709, + 6.5628065631728365 + ], + "multiplicities": [ + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3 + ], + "periodic": false + } + ], + "closed": true + }, + { + "edges": [ + { + "kind": "bspline", + "start": [ + 17.132568309375, + 6.774902443749992, + 5.0 + ], + "end": [ + 17.132568309375, + 6.774902443749992, + 5.0 + ], + "parameter_range": [ + 0.0, + 8.16675617962079 + ], + "reversed": false, + "degree": 2, + "poles": [ + [ + 17.132568309375, + 6.774902443749992, + 5.0 + ], + [ + 17.132568309375, + 9.265136818749992, + 5.0 + ], + [ + 19.305419871875, + 11.730957131249992, + 5.0 + ], + [ + 22.576904246875, + 11.730957131249992, + 5.0 + ], + [ + 24.761962840625, + 9.252929787499992, + 5.0 + ], + [ + 24.761962840625, + 4.296875099999992, + 5.0 + ], + [ + 22.589111278125, + 1.8066407249999923, + 5.0 + ], + [ + 19.305419871875, + 1.8066407249999923, + 5.0 + ], + [ + 17.132568309375, + 4.284668068749992, + 5.0 + ], + [ + 17.132568309375, + 6.774902443749992, + 5.0 + ] + ], + "weights": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "knots": [ + 0.0, + 1.0, + 2.0, + 2.985185185185185, + 3.97037037037037, + 5.015566835871404, + 6.060763301372438, + 7.113759740496614, + 8.16675617962079 + ], + "multiplicities": [ + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3 + ], + "periodic": false + } + ], + "closed": true + } + ] + } + } + ], + "result_refs": [ + "s3" + ], + "b3d_status": "ok", + "b3d_volume": 289.73641490428105, + "b3d_volume_gk": 289.73641490428105, + "b3d_default_volume": 290.6612252204212, + "b3d_bbox": [ + 14.874267428125, + -7.713174450168028e-15, + 4.9999999, + 27.020263721875, + 13.549804887499992, + 9.0000001 + ], + "solid_count": 1, + "unsupported": [], + "fixture_provenance": { + "source_stem": "34_embossed_debossed_text", + "source_feature": "s3", + "build123d_version": "0.13.0", + "source_kernel": "8.0.1.1", + "reference_volume_method": "Gauss-Kronrod, eps=1e-9, use_span=True" + } +} diff --git a/test/build123d_traces/face_mirror.trace.json b/test/build123d_traces/face_mirror.trace.json new file mode 100644 index 00000000..8dd7d189 --- /dev/null +++ b/test/build123d_traces/face_mirror.trace.json @@ -0,0 +1,249 @@ +{ + "schema_version": "1.0", + "steps": [ + { + "id": "s1", + "op": "extrude", + "inputs": [], + "parameters": { + "direction": [ + 0.0, + 0.0, + 20.0 + ] + }, + "source": "Solid.extrude", + "result": { + "volume": 42797.5432332819, + "volume_gk": 42797.5432332819, + "default_volume": 42797.54323328191, + "bbox": [ + -24.270509831248425, + -28.53169548885461, + 0.0, + 30.0, + 28.531695488854606, + 20.0 + ], + "solid_count": 1 + }, + "profile": { + "kind": "face", + "origin": [ + -9.23053696217674e-16, + 0.0, + 0.0 + ], + "x_dir": [ + 1.0, + 0.0, + -0.0 + ], + "y_dir": [ + -0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + 1.0 + ], + "wires": [ + { + "edges": [ + { + "kind": "line", + "start": [ + 30.0, + 0.0, + 0.0 + ], + "end": [ + 9.270509831248427, + 28.531695488854602, + 0.0 + ], + "parameter_range": [ + 0.0, + 35.26711513754839 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + 9.270509831248424, + 28.531695488854606, + 0.0 + ], + "end": [ + -24.27050983124842, + 17.633557568774197, + 0.0 + ], + "parameter_range": [ + 0.0, + 35.26711513754839 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + -24.27050983124842, + 17.633557568774197, + 0.0 + ], + "end": [ + -24.270509831248425, + -17.63355756877419, + 0.0 + ], + "parameter_range": [ + 0.0, + 35.26711513754839 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + -24.270509831248425, + -17.63355756877419, + 0.0 + ], + "end": [ + 9.270509831248418, + -28.53169548885461, + 0.0 + ], + "parameter_range": [ + 0.0, + 35.26711513754839 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + 9.270509831248416, + -28.53169548885461, + 0.0 + ], + "end": [ + 29.999999999999996, + 3.548257364643738e-16, + 0.0 + ], + "parameter_range": [ + 0.0, + 35.267115137548394 + ], + "reversed": false + } + ], + "closed": true + } + ] + } + }, + { + "id": "s2", + "op": "transform", + "inputs": [ + "s1" + ], + "parameters": { + "mirror_plane": { + "origin": [ + -7.500000000000003, + -23.0826265288144, + 10.0 + ], + "x_dir": [ + 0.9510565162951525, + -0.30901699437495084, + 0.0 + ], + "y_dir": [ + -0.0, + 0.0, + 1.0 + ], + "normal": [ + -0.30901699437495084, + -0.9510565162951525, + -0.0 + ] + }, + "method": "mirror" + }, + "source": "Shape.mirror", + "result": { + "volume": 42797.54323328192, + "volume_gk": 42797.54323328192, + "default_volume": 42797.54323328192, + "bbox": [ + -45.00000000000017, + -74.69694854648331, + 0.0, + 9.270509831248383, + -17.633557568774084, + 20.0 + ], + "solid_count": 1 + } + }, + { + "id": "s3", + "op": "union", + "inputs": [ + "s1", + "s2" + ], + "parameters": { + "glue": false, + "tol": null + }, + "source": "Shape.fuse", + "result": { + "volume": 85595.08646656375, + "volume_gk": 85595.08646656375, + "default_volume": 85595.08646656378, + "bbox": [ + -45.00000000000017, + -74.69694854648331, + 0.0, + 30.0, + 28.531695488854606, + 20.0 + ], + "solid_count": 1 + } + } + ], + "result_refs": [ + "s3" + ], + "b3d_volume": 85595.08646656375, + "b3d_volume_gk": 85595.08646656375, + "b3d_default_volume": 85595.08646656378, + "b3d_bbox": [ + -45.00000000000017, + -74.69694854648331, + 0.0, + 30.0, + 28.531695488854606, + 20.0 + ], + "solid_count": 1, + "b3d_status": "ok", + "unsupported": [], + "fixture_provenance": { + "source_stem": "17_face_mirror", + "build123d_version": "0.13.0", + "source_kernel": "8.0.1.1", + "reference_volume_method": "Gauss-Kronrod, eps=1e-9, use_span=True" + } +} diff --git a/test/build123d_traces/filleted_hole.trace.json b/test/build123d_traces/filleted_hole.trace.json new file mode 100644 index 00000000..713a9184 --- /dev/null +++ b/test/build123d_traces/filleted_hole.trace.json @@ -0,0 +1,365 @@ +{ + "schema_version": "1.0", + "steps": [ + { + "id": "s1", + "op": "box", + "inputs": [], + "parameters": { + "length": 80.0, + "width": 60.0, + "height": 10.0, + "plane": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x_dir": [ + 1.0, + 0.0, + -0.0 + ], + "y_dir": [ + -0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + 1.0 + ] + } + }, + "source": "Solid.make_box", + "result": { + "volume": 47999.99999999999, + "volume_gk": 47999.99999999999, + "default_volume": 48000.0, + "bbox": [ + 0.0, + 0.0, + 0.0, + 80.0, + 60.0, + 10.0 + ], + "solid_count": 1 + } + }, + { + "id": "s2", + "op": "transform", + "inputs": [ + "s1" + ], + "parameters": { + "loc": { + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + -40.0 + ], + [ + 0.0, + 1.0, + 0.0, + -30.0 + ], + [ + 0.0, + 0.0, + 1.0, + -5.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "method": "move", + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + -40.0 + ], + [ + 0.0, + 1.0, + 0.0, + -30.0 + ], + [ + 0.0, + 0.0, + 1.0, + -5.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "source": "Shape.move", + "result": { + "volume": 47999.99999999999, + "volume_gk": 47999.99999999999, + "default_volume": 48000.0, + "bbox": [ + -40.0, + -30.0, + -5.0, + 40.0, + 30.0, + 5.0 + ], + "solid_count": 1 + } + }, + { + "id": "s3", + "op": "cylinder", + "inputs": [], + "parameters": { + "radius": 15.0, + "height": 200.9975124224178, + "plane": { + "origin": [ + 0.0, + 0.0, + 100.4987562112089 + ], + "x_dir": [ + -1.0, + 0.0, + -0.0 + ], + "y_dir": [ + 0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + -1.0 + ] + }, + "angle": 360 + }, + "source": "Solid.make_cylinder", + "result": { + "volume": 142076.76939362014, + "volume_gk": 142076.76939362014, + "default_volume": 142076.76939362046, + "bbox": [ + -15.0, + -15.0, + -100.4987562112089, + 15.0, + 15.0, + 100.4987562112089 + ], + "solid_count": 1 + } + }, + { + "id": "s4", + "op": "transform", + "inputs": [ + "s3" + ], + "parameters": { + "loc": { + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + -0.0 + ], + [ + 0.0, + 1.0, + 0.0, + -0.0 + ], + [ + 0.0, + 0.0, + 1.0, + -0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "method": "move", + "matrix": [ + [ + 1.0, + 0.0, + 0.0, + -0.0 + ], + [ + 0.0, + 1.0, + 0.0, + -0.0 + ], + [ + 0.0, + 0.0, + 1.0, + -0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "source": "Shape.move", + "result": { + "volume": 142076.76939362014, + "volume_gk": 142076.76939362014, + "default_volume": 142076.76939362046, + "bbox": [ + -15.0, + -15.0, + -100.4987562112089, + 15.0, + 15.0, + 100.4987562112089 + ], + "solid_count": 1 + } + }, + { + "id": "s5", + "op": "cut", + "inputs": [ + "s2", + "s4" + ], + "parameters": {}, + "source": "Shape.cut", + "result": { + "volume": 40931.41652942296, + "volume_gk": 40931.41652942296, + "default_volume": 40931.41652942296, + "bbox": [ + -40.0, + -30.0, + -5.0, + 40.0, + 30.0, + 5.0 + ], + "solid_count": 1 + } + }, + { + "id": "s6", + "op": "fillet", + "inputs": [ + "s5" + ], + "parameters": { + "radius": 2 + }, + "source": "Mixin3D.fillet", + "result": { + "volume": 40848.104058581375, + "volume_gk": 40848.104058581375, + "default_volume": 40848.10405858136, + "bbox": [ + -40.0, + -30.0, + -5.0, + 40.0, + 30.0, + 5.0 + ], + "solid_count": 1 + }, + "selection": [ + { + "kind": "edge", + "geom_type": "CIRCLE", + "center": [ + -7.0682404105834e-14, + -4.637201104937301e-16, + 4.999999999999999 + ], + "bbox": [ + -15.0, + -15.0, + 5.0, + 15.0, + 15.0, + 5.0 + ], + "length": 94.2477796076938, + "start": [ + -15.0, + -1.699661669294394e-14, + 5.0 + ], + "mid": [ + 15.0, + 8.49830834647197e-15, + 5.0 + ], + "end": [ + -15.0, + 0.0, + 5.0 + ], + "index": 9 + } + ] + } + ], + "result_refs": [ + "s6" + ], + "b3d_volume": 40848.104058581375, + "b3d_volume_gk": 40848.104058581375, + "b3d_default_volume": 40848.10405858136, + "b3d_bbox": [ + -40.0, + -30.0, + -5.0, + 40.0, + 30.0, + 5.0 + ], + "solid_count": 1, + "b3d_status": "ok", + "unsupported": [], + "fixture_provenance": { + "source_stem": "10_filleted_hole", + "build123d_version": "0.13.0", + "source_kernel": "8.0.1.1", + "reference_volume_method": "Gauss-Kronrod, eps=1e-9, use_span=True" + } +} diff --git a/test/build123d_traces/lines_and_arcs.trace.json b/test/build123d_traces/lines_and_arcs.trace.json new file mode 100644 index 00000000..e238548d --- /dev/null +++ b/test/build123d_traces/lines_and_arcs.trace.json @@ -0,0 +1,178 @@ +{ + "schema_version": "1.0", + "steps": [ + { + "id": "s1", + "op": "extrude", + "inputs": [], + "parameters": { + "direction": [ + 0.0, + 0.0, + 10.0 + ] + }, + "source": "Solid.extrude", + "result": { + "volume": 69504.67164903213, + "volume_gk": 69504.67164903213, + "default_volume": 69504.6716490322, + "bbox": [ + 0.0, + 0.0, + 0.0, + 80.0, + 95.31128874149275, + 10.0 + ], + "solid_count": 1 + }, + "profile": { + "kind": "face", + "origin": [ + 40.0, + 43.87364956936529, + 0.0 + ], + "x_dir": [ + 1.0, + 0.0, + -0.0 + ], + "y_dir": [ + -0.0, + 1.0, + 0.0 + ], + "normal": [ + 0.0, + 0.0, + 1.0 + ], + "wires": [ + { + "edges": [ + { + "kind": "line", + "start": [ + 0.0, + 0.0, + 0.0 + ], + "end": [ + 80.0, + 0.0, + 0.0 + ], + "parameter_range": [ + 0.0, + 80.0 + ], + "reversed": false + }, + { + "kind": "line", + "start": [ + 80.0, + 0.0, + 0.0 + ], + "end": [ + 80.0, + 60.0, + 0.0 + ], + "parameter_range": [ + 0.0, + 60.0 + ], + "reversed": false + }, + { + "kind": "arc", + "mid": [ + 40.00000000000001, + 95.31128874149275, + 0.0 + ], + "start": [ + 80.0, + 60.0, + 0.0 + ], + "end": [ + 0.0, + 60.00000000000001, + 0.0 + ], + "parameter_range": [ + 0.0, + 2.89288266449627 + ], + "reversed": false, + "center": [ + 40.0, + 55.0, + 0.0 + ], + "radius": 40.311288741492746, + "normal": [ + 0.0, + 0.0, + 1.0 + ], + "x_dir": [ + 0.9922778767136677, + 0.12403473458920847, + -0.0 + ] + }, + { + "kind": "line", + "start": [ + 0.0, + 60.0, + 0.0 + ], + "end": [ + 0.0, + 0.0, + 0.0 + ], + "parameter_range": [ + 0.0, + 60.0 + ], + "reversed": false + } + ], + "closed": true + } + ] + } + } + ], + "result_refs": [ + "s1" + ], + "b3d_volume": 69504.67164903213, + "b3d_volume_gk": 69504.67164903213, + "b3d_default_volume": 69504.6716490322, + "b3d_bbox": [ + 0.0, + 0.0, + 0.0, + 80.0, + 95.31128874149275, + 10.0 + ], + "solid_count": 1, + "b3d_status": "ok", + "unsupported": [], + "fixture_provenance": { + "source_stem": "04_lines_and_arcs_profile", + "build123d_version": "0.13.0", + "source_kernel": "8.0.1.1", + "reference_volume_method": "Gauss-Kronrod, eps=1e-9, use_span=True" + } +} diff --git a/test/build123d_traces/spring.trace.json b/test/build123d_traces/spring.trace.json new file mode 100644 index 00000000..5341a7db --- /dev/null +++ b/test/build123d_traces/spring.trace.json @@ -0,0 +1,1350 @@ +{ + "schema_version": "1.0", + "steps": [ + { + "id": "s1", + "op": "sweep", + "inputs": [], + "parameters": { + "make_solid": true, + "is_frenet": false, + "mode": null, + "transition": "TRANSFORMED" + }, + "source": "Solid.sweep", + "result": { + "volume": 35.642805167823504, + "volume_gk": 35.642805167823504, + "default_volume": 35.642681367230885, + "bbox": [ + -2.300012799317609, + -2.3000127993174146, + -0.2990547016451006, + 2.3000127993175012, + 2.3000127993174946, + 10.299054701644959 + ], + "solid_count": 1 + }, + "profile": { + "kind": "face", + "origin": [ + 2.0, + 6.652720096874938e-16, + 3.089313816368428e-17 + ], + "x_dir": [ + -1.0, + 1.3153723711470598e-10, + -1.6540802198634257e-09 + ], + "y_dir": [ + -1.6593020853332313e-09, + -0.07932669684361024, + 0.996848672150333 + ], + "normal": [ + -9.000000000000026e-14, + 0.996848672150333, + 0.07932669684361024 + ], + "wires": [ + { + "edges": [ + { + "kind": "circle", + "start": [ + 1.7, + 3.9463258065123386e-11, + -4.962243907043456e-10 + ], + "end": [ + 1.7, + 3.9463263893954504e-11, + -4.962244639515977e-10 + ], + "parameter_range": [ + 0.0, + 6.283185307179586 + ], + "reversed": false, + "center": [ + 2.0, + 6.661338147750939e-16, + 0.0 + ], + "radius": 0.3, + "normal": [ + -8.536480491004258e-14, + 0.996848672150333, + 0.07932669684360946 + ], + "x_dir": [ + -1.0, + 1.3154197310436203e-10, + -1.6540813023478187e-09 + ] + } + ], + "closed": true + } + ] + }, + "path": { + "edges": [ + { + "kind": "bspline", + "start": [ + 2.0, + 6.661338147750939e-16, + 0.0 + ], + "end": [ + 2.0, + -5.047582789074024e-15, + 10.000000000000002 + ], + "parameter_range": [ + 0.0, + 63.62265131567328 + ], + "reversed": false, + "degree": 13, + "poles": [ + [ + 2.0, + 6.661338147750939e-16, + 0.0 + ], + [ + 1.9999999999999483, + 0.6041524333821656, + 0.04807692307685436 + ], + [ + 1.8022915771936825, + 1.208304870905466, + 0.09615384615415443 + ], + [ + 1.4068747452798505, + 1.7418755258527725, + 0.14423076922994169 + ], + [ + 0.8414668189386844, + 2.1342829054726824, + 0.1923076923092902 + ], + [ + 0.16150280081281543, + 2.3270383484976986, + 0.24038461538226746 + ], + [ + -0.555802317397056, + 2.285844591901501, + 0.28846153846423295 + ], + [ + -1.2233246240282272, + 2.009347799270346, + 0.336538461536015 + ], + [ + -1.759664321949602, + 1.5312648706945784, + 0.3846153846171492 + ], + [ + -2.104172809238839, + 0.9141590215986823, + 0.43269230769130473 + ], + [ + -2.226502668984004, + 0.23688132365397352, + 0.4807692307696662 + ], + [ + -2.128813163865841, + -0.4200120279538607, + 0.5288461538460247 + ], + [ + -1.8414138448882833, + -0.9870132798576515, + 0.5769230769230892 + ], + [ + -0.9870132798577862, + -1.841413844888405, + 0.6730769230769338 + ], + [ + -0.42001202795322584, + -2.1288131638652668, + 0.7211538461536989 + ], + [ + 0.2368813236521682, + -2.2265026689856753, + 0.7692307692313213 + ], + [ + 0.9141590216025747, + -2.1041728092354055, + 0.8173076923063703 + ], + [ + 1.531264870687806, + -1.7596643219548758, + 0.8653846153869104 + ], + [ + 2.009347799279881, + -1.223324624021997, + 0.9134615384585185 + ], + [ + 2.2858445918908323, + -0.5558023174028356, + 0.9615384615415711 + ], + [ + 2.327038348507003, + 0.1615028008170587, + 1.0096153846128404 + ], + [ + 2.134282905466487, + 0.8414668189362138, + 1.0576923076939477 + ], + [ + 1.7418755258558447, + 1.40687474528098, + 1.1057692307684408 + ], + [ + 1.2083048709043918, + 1.802291577193289, + 1.153846153846394 + ], + [ + 0.6041524333823806, + 2.0000000000000338, + 1.2019230769230707 + ], + [ + -0.60415243338235, + 1.9999999999999694, + 1.2980769230768512 + ], + [ + -1.2083048709047663, + 1.802291577193527, + 1.346153846154394 + ], + [ + -1.7418755258545406, + 1.406874745280456, + 1.3942307692290825 + ], + [ + -2.1342829054693033, + 0.8414668189370608, + 1.4423076923112175 + ], + [ + -2.3270383485027906, + 0.1615028008159628, + 1.490384615379138 + ], + [ + -2.2858445918954327, + -0.5558023174016837, + 1.5384615384681204 + ], + [ + -2.009347799276032, + -1.2233246240229605, + 1.5865384615321823 + ], + [ + -1.5312648706904137, + -1.7596643219542796, + 1.6346153846201954 + ], + [ + -0.9141590216010391, + -2.1041728092356133, + 1.6826923076893723 + ], + [ + -0.23688132365297554, + -2.2265026689856637, + 1.730769230770599 + ], + [ + 0.4200120279535802, + -2.1288131638652725, + 1.7788461538457223 + ], + [ + 0.9870132798576695, + -1.8414138448883566, + 1.826923076923126 + ], + [ + 1.8414138448884514, + -0.9870132798577959, + 1.9230769230769404 + ], + [ + 2.128813163864945, + -0.4200120279529417, + 1.9711538461535816 + ], + [ + 2.226502668986428, + 0.23688132365119724, + 2.019230769231603 + ], + [ + 2.1041728092342944, + 0.9141590216045443, + 2.067307692305985 + ], + [ + 1.759664321955971, + 1.531264870684877, + 2.1153846153872093 + ], + [ + 1.223324624021276, + 2.009347799283421, + 2.1634615384584075 + ], + [ + 0.5558023174030458, + 2.285844591887129, + 2.2115384615415885 + ], + [ + -0.16150280081684026, + 2.3270383485103614, + 2.2596153846127724 + ], + [ + -0.8414668189366271, + 2.134282905463938, + 2.3076923076941025 + ], + [ + -1.4068747452806216, + 1.741875525857392, + 2.3557692307682663 + ], + [ + -1.802291577193468, + 1.2083048709036788, + 2.4038461538465112 + ], + [ + -1.999999999999993, + 0.6041524333826138, + 2.4519230769230216 + ], + [ + -2.0000000000000036, + -0.60415243338269, + 2.548076923076934 + ], + [ + -1.8022915771934396, + -1.208304870902915, + 2.5961538461539533 + ], + [ + -1.406874745280619, + -1.7418755258598626, + 2.6442307692302807 + ], + [ + -0.8414668189367117, + -2.134282905458664, + 2.692307692309037 + ], + [ + -0.1615028008165532, + -2.327038348518791, + 2.740384615381839 + ], + [ + 0.5558023174023855, + -2.2858445918765837, + 2.7884615384658793 + ], + [ + 1.223324624022344, + -2.009347799293845, + 2.8365384615333173 + ], + [ + 1.7596643219547532, + -1.531264870676808, + 2.8846153846199845 + ], + [ + 2.1041728092353202, + -0.9141590216093117, + 2.932692307689301 + ], + [ + 2.22650266898584, + -0.23688132364910672, + 2.980769230770539 + ], + [ + 2.128813163865153, + 0.42001202795234166, + 3.028846153845933 + ], + [ + 1.841413844888438, + 0.987013279857846, + 3.0769230769229154 + ], + [ + 0.9870132798576133, + 1.8414138448884376, + 3.1730769230774123 + ], + [ + 0.42001202795398135, + 2.128813163864508, + 3.221153846150588 + ], + [ + -0.23688132365419814, + 2.226502668988156, + 3.2692307692407536 + ], + [ + -0.9141590215985614, + 2.1041728092302887, + 3.317307692286612 + ], + [ + -1.5312648706939194, + 1.7596643219626364, + 3.365384615418172 + ], + [ + -2.00934779927267, + 1.2233246240127316, + 3.413461538419854 + ], + [ + -2.2858445918974035, + 0.5558023174118611, + 3.461538461579471 + ], + [ + -2.327038348502379, + -0.16150280082438673, + 3.509615384583322 + ], + [ + -2.134282905468952, + -0.8414668189313265, + 3.557692307712066 + ], + [ + -1.7418755258549337, + -1.4068747452834651, + 3.605769230759894 + ], + [ + -1.2083048709045383, + -1.8022915771924415, + 3.6538461538492917 + ], + [ + -0.6041524333824554, + -2.0000000000001457, + 3.701923076922517 + ], + [ + 0.6041524333825566, + -1.9999999999999754, + 3.798076923077314 + ], + [ + 1.2083048709032667, + -1.8022915771931862, + 3.8461538461525504 + ], + [ + 1.7418755258591465, + -1.4068747452816484, + 3.894230769233875 + ], + [ + 2.1342829054596724, + -0.841466818934496, + 3.942307692302027 + ], + [ + 2.32703834851759, + -0.16150280082005888, + 3.990384615392585 + ], + [ + 2.285844591877996, + 0.555802317406608, + 4.038461538452697 + ], + [ + 2.0093477992920947, + 1.2233246240182585, + 4.0865384615462865 + ], + [ + 1.5312648706787897, + 1.7596643219578538, + 4.134615384609838 + ], + [ + 0.9141590216076623, + 2.1041728092334813, + 4.182692307695468 + ], + [ + 0.23688132365004666, + 2.226502668986602, + 4.230769230767802 + ], + [ + -0.4200120279526195, + 2.128813163865019, + 4.278846153846611 + ], + [ + -0.9870132798578423, + 1.8414138448883532, + 4.326923076923031 + ], + [ + -1.8414138448883082, + 0.9870132798579057, + 4.423076923076879 + ], + [ + -2.1288131638654924, + 0.42001202795204434, + 4.471153846154212 + ], + [ + -2.2265026689851264, + -0.23688132364829764, + 4.519230769229973 + ], + [ + -2.104172809236565, + -0.9141590216110625, + 4.567307692308481 + ], + [ + -1.7596643219528143, + -1.5312648706739498, + 4.615384615384955 + ], + [ + -1.223324624024944, + -2.0093477992974025, + 4.663461538458743 + ], + [ + -0.555802317399491, + -2.2858445918732224, + 4.711538461543929 + ], + [ + 0.16150280081409893, + -2.3270383485212665, + 4.759615384608758 + ], + [ + 0.8414668189382435, + -2.1342829054572143, + 4.807692307697982 + ], + [ + 1.4068747452799755, + -1.741875525860566, + 4.855769230765668 + ], + [ + 1.8022915771936252, + -1.2083048709025832, + 4.903846153847806 + ], + [ + 1.9999999999999802, + -0.6041524333828419, + 4.951923076922511 + ], + [ + 2.0000000000000084, + 0.6041524333829378, + 5.048076923077781 + ], + [ + 1.8022915771935002, + 1.2083048709016495, + 5.096153846149207 + ], + [ + 1.40687474528029, + 1.7418755258633165, + 5.14423076924378 + ], + [ + 0.8414668189379731, + 2.1342829054516783, + 5.1923076922821405 + ], + [ + 0.1615028008136067, + 2.3270383485300115, + 5.24038461542245 + ], + [ + -0.5558023173980627, + 2.2858445918620824, + 5.2884615384179465 + ], + [ + -1.2233246240265405, + 2.0093477993088764, + 5.336538461578303 + ], + [ + -1.759664321951844, + 1.5312648706645449, + 5.384615384586182 + ], + [ + -2.10417280923674, + 0.9141590216169742, + 5.432692307709485 + ], + [ + -2.226502668985374, + 0.23688132364566097, + 5.480769230761262 + ], + [ + -2.128813163865221, + -0.42001202795128517, + 5.528846153848939 + ], + [ + -1.841413844888457, + -0.9870132798580189, + 5.576923076922424 + ], + [ + -0.9870132798575849, + -1.8414138448882669, + 5.6730769230776925 + ], + [ + -0.420012027954111, + -2.128813163865314, + 5.721153846149946 + ], + [ + 0.2368813236545341, + -2.2265026689858023, + 5.7692307692421325 + ], + [ + 0.9141590215976688, + -2.1041728092354037, + 5.817307692284152 + ], + [ + 1.531264870696111, + -1.7596643219539576, + 5.86538461542164 + ], + [ + 2.009347799268774, + -1.2233246240241786, + 5.913461538416068 + ], + [ + 2.2858445919022303, + -0.5558023174003069, + 5.961538461582642 + ], + [ + 2.3270383484981876, + 0.1615028008155162, + 6.0096153845812825 + ], + [ + 2.134282905471544, + 0.8414668189366524, + 6.057692307713076 + ], + [ + 1.7418755258537841, + 1.4068747452810244, + 6.105769230759544 + ], + [ + 1.2083048709048787, + 1.8022915771932297, + 6.153846153849315 + ], + [ + 0.6041524333823935, + 2.000000000000041, + 6.201923076922612 + ], + [ + -0.6041524333825374, + 1.9999999999999893, + 6.298076923077063 + ], + [ + -1.2083048709034343, + 1.8022915771934118, + 6.346153846154186 + ], + [ + -1.741875525858368, + 1.4068747452807753, + 6.394230769229149 + ], + [ + -2.134282905461634, + 0.8414668189366404, + 6.4423076923116795 + ], + [ + -2.327038348514876, + 0.16150280081609175, + 6.4903846153772005 + ], + [ + -2.285844591879941, + -0.5558023174015779, + 6.538461538472675 + ], + [ + -2.0093477992921986, + -1.2233246240226923, + 6.586538461524955 + ], + [ + -1.5312648706769878, + -1.759664321955127, + 6.634615384628441 + ], + [ + -0.9141590216095852, + -2.104172809234644, + 6.682692307682479 + ], + [ + -0.23688132364902123, + -2.22650266898628, + 6.730769230774807 + ], + [ + 0.4200120279523284, + -2.1288131638650727, + 6.778846153843907 + ], + [ + 0.9870132798579115, + -1.8414138448883368, + 6.826923076923632 + ], + [ + 1.8414138448882231, + -0.9870132798579234, + 6.923076923076314 + ], + [ + 2.1288131638659307, + -0.42001202795207304, + 6.971153846156448 + ], + [ + 2.2265026689837657, + 0.23688132364868128, + 7.019230769224817 + ], + [ + 2.104172809239701, + 0.9141590216094687, + 7.067307692316733 + ], + [ + 1.7596643219474462, + 1.531264870677658, + 7.115384615374962 + ], + [ + 1.22332462403152, + 2.009347799291817, + 7.1634615384687494 + ], + [ + 0.5558023173941883, + 2.285844591878955, + 7.211538461535101 + ], + [ + -0.1615028008117135, + 2.327038348517077, + 7.259615384615659 + ], + [ + -0.8414668189383224, + 2.1342829054594192, + 7.307692307693514 + ], + [ + -1.4068747452805237, + 1.7418755258597582, + 7.355769230767805 + ], + [ + -1.8022915771932797, + 1.208304870902799, + 7.403846153847179 + ], + [ + -2.000000000000064, + 0.6041524333828056, + 7.4519230769225455 + ], + [ + -1.9999999999999298, + -0.6041524333828427, + 7.548076923077893 + ], + [ + -1.802291577193757, + -1.2083048709021935, + 7.5961538461480504 + ], + [ + -1.4068747452799983, + -1.741875525861654, + 7.644230769248086 + ], + [ + -0.8414668189377974, + -2.1342829054549712, + 7.692307692271547 + ], + [ + -0.16150280081486876, + -2.32703834852532, + 7.740384615441791 + ], + [ + 0.555802317400229, + -2.2858445918671535, + 7.788461538390869 + ], + [ + 1.2233246240243516, + -2.0093477993044764, + 7.836538461607568 + ], + [ + 1.7596643219533266, + -1.5312648706676915, + 7.884615384561717 + ], + [ + 2.104172809236037, + -0.9141590216151801, + 7.932692307725156 + ], + [ + 2.2265026689855025, + -0.23688132364645745, + 7.980769230753871 + ], + [ + 2.128813163865374, + 0.42001202795148923, + 8.028846153851099 + ], + [ + 1.8414138448882706, + 0.9870132798580143, + 8.076923076922437 + ], + [ + 0.987013279858011, + 1.8414138448882098, + 8.173076923076875 + ], + [ + 0.420012027951521, + 2.128813163865906, + 8.22115384615648 + ], + [ + -0.23688132364686182, + 2.226502668983463, + 8.26923076922096 + ], + [ + -0.9141590216137264, + 2.104172809241313, + 8.31730769233036 + ], + [ + -1.531264870670311, + 1.7596643219434511, + 8.365384615346466 + ], + [ + -2.009347799301379, + 1.2233246240379547, + 8.413461538510797 + ], + [ + -2.2858445918695356, + 0.5558023173866748, + 8.461538461488567 + ], + [ + -2.327038348524108, + -0.16150280080508977, + 8.5096153846554 + ], + [ + -2.134282905455498, + -0.8414668189427993, + 8.557692307667162 + ], + [ + -1.7418755258613183, + -1.4068747452782566, + 8.605769230781075 + ], + [ + -1.20830487090241, + -1.8022915771940946, + 8.653846153842522 + ], + [ + -0.604152433382828, + -1.999999999999907, + 8.701923076923327 + ], + [ + 0.6041524333828305, + -2.0000000000000564, + 8.798076923077526 + ], + [ + 1.208304870902372, + -1.8022915771933377, + 8.846153846148837 + ], + [ + 1.7418755258609533, + -1.4068747452807895, + 8.894230769245528 + ], + [ + 2.134282905456969, + -0.8414668189369883, + 8.942307692278401 + ], + [ + 2.3270383485210635, + -0.16150280081474325, + 8.990384615428868 + ], + [ + 2.285844591873969, + 0.5558023173983309, + 9.038461538408193 + ], + [ + 2.0093477992963296, + 1.2233246240276605, + 9.086538461590958 + ], + [ + 1.5312648706749636, + 1.7596643219501886, + 9.134615384572934 + ], + [ + 0.91415902161026, + 2.1041728092378444, + 9.182692307720423 + ], + [ + 0.2368813236489844, + 2.2265026689849927, + 9.230769230754154 + ], + [ + -0.42001202795247133, + 2.1288131638652503, + 9.278846153852554 + ], + [ + -0.9870132798577462, + 1.8414138448885005, + 9.326923076921004 + ], + [ + -1.8414138448885735, + 0.9870132798574845, + 9.423076923079982 + ], + [ + -2.1288131638639047, + 0.42001202795476394, + 9.471153846136946 + ], + [ + -2.22650266898944, + -0.2368813236564224, + 9.519230769279703 + ], + [ + -2.104172809228791, + -0.9141590215942135, + 9.567307692207855 + ], + [ + -1.7596643219632289, + -1.5312648707004843, + 9.615384615539929 + ], + [ + -1.22332462401359, + -2.0093477992648854, + 9.663461538271 + ], + [ + -0.5558023174105057, + -2.2858445919045973, + 9.711538461725837 + ], + [ + 0.16150280082377924, + -2.3270383484972044, + 9.759615384466972 + ], + [ + 0.8414668189311059, + -2.1342829054719186, + 9.80769230778639 + ], + [ + 1.4068747452839687, + -1.741875525853499, + 9.85576923072261 + ], + [ + 1.8022915771920602, + -1.2083048709051603, + 9.903846153863118 + ], + [ + 2.0000000000003317, + -0.6041524333822196, + 9.951923076919346 + ], + [ + 2.0, + -5.047582789074024e-15, + 10.000000000000002 + ] + ], + "weights": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "knots": [ + 0.0, + 3.97641570722958, + 7.95283141445916, + 11.92924712168874, + 15.90566282891832, + 19.8820785361479, + 23.85849424337748, + 27.83490995060706, + 31.81132565783664, + 35.787741365066225, + 39.7641570722958, + 43.74057277952538, + 47.71698848675496, + 51.693404193984534, + 55.66981990121412, + 59.6462356084437, + 63.62265131567328 + ], + "multiplicities": [ + 14, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 12, + 14 + ], + "periodic": false + } + ], + "closed": false + }, + "inner_wires": [] + } + ], + "result_refs": [ + "s1" + ], + "b3d_volume": 35.642805167823504, + "b3d_volume_gk": 35.642805167823504, + "b3d_default_volume": 35.642681367230885, + "b3d_bbox": [ + -2.300012799317609, + -2.3000127993174146, + -0.2990547016451006, + 2.3000127993175012, + 2.3000127993174946, + 10.299054701644959 + ], + "solid_count": 1, + "b3d_status": "ok", + "unsupported": [], + "fixture_provenance": { + "source_stem": "50_spring", + "build123d_version": "0.13.0", + "source_kernel": "8.0.1.1", + "reference_volume_method": "Gauss-Kronrod, eps=1e-9, use_span=True" + } +} diff --git a/test/test_affine_geometry_isolation.py b/test/test_affine_geometry_isolation.py new file mode 100644 index 00000000..2daea3ef --- /dev/null +++ b/test/test_affine_geometry_isolation.py @@ -0,0 +1,25 @@ +"""Complex shared fillet geometry must survive nonuniform scale without a crash.""" +import json +from pathlib import Path +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools/research/build123dftc")) +from b3ftc.replay import replay_trace_to_ftc +from b3ftc.validation import _worker + + +def test_nonuniform_scale_after_spherical_cut_and_fillet(tmp_path): + trace = json.loads((ROOT / "test/build123d_traces/affine_trimmed_sphere.trace.json").read_text()) + source, meta = replay_trace_to_ftc(trace, stem="affine-shared-geometry") + assert meta["status"] == "ok" + path = tmp_path / "affine.ftc.py" + path.write_text(source) + # A native kernel regression can segfault. Keep the test runner alive and + # report it as a failing feature, exactly like the external acceptance tool. + metrics = _worker(path, tmp_path / "work", 30, None, sys.executable, None) + assert metrics["status"] == "ok", metrics + assert metrics["is_valid"] is True + assert metrics["volume"] == pytest.approx(trace["b3d_volume"], rel=1e-6) diff --git a/test/test_build123d_cli.py b/test/test_build123d_cli.py new file mode 100644 index 00000000..7271c482 --- /dev/null +++ b/test/test_build123d_cli.py @@ -0,0 +1,88 @@ +"""The pure stage-2 boundary and batch input discovery contracts.""" +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +TOOLS = Path(__file__).resolve().parents[1] / "tools/research/build123dftc" +sys.path.insert(0, str(TOOLS)) + + +def _script(name): + spec = importlib.util.spec_from_file_location(name, TOOLS / f"{name}.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_translate_writes_reviewable_source_and_sidecars(tmp_path): + payload = {"schema_version": "1.0", "stem": "box", "b3d_status": "ok", + "unsupported": [], "steps": [{"id": "base", "op": "box", "inputs": [], + "parameters": {"length": 10, "width": 8, "height": 4}}], + "result_refs": ["base"], "b3d_volume": 320, + "b3d_bbox": [0, 0, 0, 10, 8, 4], "solid_count": 1} + trace = tmp_path / "box.trace.json" + trace.write_text(json.dumps(payload)) + stem, source, meta = _script("build123d_to_ftc").translate_trace_file(trace, tmp_path / "out") + assert stem == "box" + assert meta["status"] == "ok" + assert "feature:" in source and "make_box_rsolid" in source + assert (tmp_path / "out/box.ftc.py").exists() + assert (tmp_path / "out/box.meta.json").exists() + assert (tmp_path / "out/box.sketches.json").exists() + + +@pytest.mark.parametrize("stem", ["../escape", "/absolute", "..", "nested/file"]) +def test_trace_cannot_write_outside_output_directory(tmp_path, stem): + trace = tmp_path / "bad.trace.json" + trace.write_text(json.dumps({"schema_version": "1.0", "stem": stem, "steps": []})) + with pytest.raises(ValueError): + _script("build123d_to_ftc").translate_trace_file(trace, tmp_path / "out") + assert not (tmp_path / "out").exists() + + +def test_future_schema_is_not_silently_reinterpreted(tmp_path): + trace = tmp_path / "bad.trace.json" + trace.write_text(json.dumps({"schema_version": "999", "steps": []})) + with pytest.raises(ValueError, match="schema"): + _script("build123d_to_ftc").translate_trace_file(trace, tmp_path / "out") + + +def test_manifest_resolves_only_declared_models(tmp_path): + models = tmp_path / "models" + models.mkdir() + for name in ("one.py", "two.py", "extra.py"): + (models / name).write_text("") + manifest = tmp_path / "manifest.json" + manifest.write_text(json.dumps({"examples": [{"file": "models/one.py"}, {"file": "models/two.py"}]})) + actual = _script("build123d_trace").discover_inputs(manifest=manifest) + assert [p.name for p in actual] == ["one.py", "two.py"] + + +@pytest.mark.parametrize("timeout", ["nan", "inf", "0", "-1"]) +def test_capture_rejects_unbounded_or_invalid_timeouts(timeout): + with pytest.raises(SystemExit) as error: + _script("build123d_trace").main(["--input", "unused.py", "--timeout", timeout]) + assert error.value.code == 2 + + +def test_validator_cli_reports_failure_with_nonzero_exit(tmp_path, monkeypatch): + module = _script("build123d_validate") + report = {"rows": [{"stem": "broken", "quality": "rejected", "build": "build_error", + "reasons": ["build_failed"]}], + "counts": {"accepted": 0, "partial": 0, "rejected": 1}} + monkeypatch.setattr(module, "validate_directory", lambda *a, **kw: report) + output = tmp_path / "report.json" + assert module.main(["--ftc-dir", str(tmp_path), "--traces-dir", str(tmp_path), + "--report", str(output)]) == 1 + assert json.loads(output.read_text())["counts"]["rejected"] == 1 + + +@pytest.mark.parametrize("timeout", ["nan", "inf", "-1", "0"]) +def test_validator_cli_rejects_nonfinite_or_nonpositive_timeout(tmp_path, timeout): + with pytest.raises(SystemExit) as exc: + _script("build123d_validate").main(["--ftc-dir", str(tmp_path), "--traces-dir", str(tmp_path), + "--timeout", timeout]) + assert exc.value.code == 2 diff --git a/test/test_build123d_review_regressions.py b/test/test_build123d_review_regressions.py new file mode 100644 index 00000000..cfe07ca2 --- /dev/null +++ b/test/test_build123d_review_regressions.py @@ -0,0 +1,90 @@ +"""Review reproductions cross the two isolated kernel environments end to end.""" +import json +from pathlib import Path +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools/research/build123dftc")) +from b3ftc.replay import replay_trace_to_ftc +from b3ftc.validation import compare_metrics +from b3ftc._validate_worker import run + + +def capture(source): + python = ROOT.parent / ".venv/bin/python" + if not python.is_file(): + pytest.skip("isolated build123d environment is unavailable") + worker = "import json,sys; sys.path.insert(0,'tools/research/build123dftc'); from b3ftc.tracer import trace_source; print(json.dumps(trace_source(sys.stdin.read())))" + process = subprocess.run([str(python), "-c", worker], input="from build123d import *\n" + source, + text=True, capture_output=True, cwd=ROOT, timeout=30, check=True) + trace = json.loads(process.stdout) + assert trace["b3d_status"] == "ok", trace + return trace + + +@pytest.mark.parametrize("source", [ + "result=Solid.make_torus(5,1,major_angle=120)", + "a=Solid.make_box(10,2,2)\nb=Solid.make_box(2,4,4).moved(Location((4,-1,-1)))\nresult=a.cut(b)", + "a=Solid.make_box(10,2,2)\nb=Solid.make_box(2,4,4).moved(Location((4,-1,-1)))\nresult=a.cut(b).solids()[1].moved(Location((0,5,0)))", + "a=Solid.make_box(10,2,2)\nb=Solid.make_box(2,4,4)\nc=Solid.make_box(2,4,4).moved(Location((8,0,0)))\nresult=a.intersect(Compound([b,c]))", + "a=Solid.make_box(2,2,2)\nb=Solid.make_box(2,2,2).moved(Location((4,0,0)))\nc=Compound([a,b])\nresult=c.fillet(.2, c.edges().filter_by(Axis.Z))", + "a=Solid.make_box(2,2,2)\nb=Solid.make_box(2,2,2).moved(Location((4,0,0)))\nc=Compound([a,b])\nresult=c.fillet(.2, a.edges().filter_by(Axis.Z))", + "a=Solid.make_box(2,2,2)\nresult=a.split(Plane.XY.offset(10), keep=Keep.BOTH)", +]) +def test_review_geometry_reconciles_with_actual_return(tmp_path, source): + trace = capture(source) + generated, meta = replay_trace_to_ftc(trace, stem="review") + assert meta["status"] == "ok", meta + ftc = tmp_path / "review.ftc.py" + ftc.write_text(generated) + stage = tmp_path / "staged" + stage.mkdir() + metrics = run(ftc, stage) + comparison = compare_metrics(trace, metrics, meta) + assert comparison["quality"] == "accepted", comparison + + +@pytest.mark.parametrize("angles", ["start_angle=-30", "end_angle=80", "start_angle=-30,end_angle=80,major_angle=140"]) +def test_partial_torus_tube_is_explicitly_unsupported(angles): + trace = capture(f"result=Solid.make_torus(5,1,{angles})") + generated, meta = replay_trace_to_ftc(trace) + assert meta["status"] == "partial" + assert any("partial torus tube" in str(note) for note in meta["unsupported"]) + assert "feature tree produced no solid" in generated + + +def test_solid_ordering_uses_bounds_when_centroids_differ_between_kernels(tmp_path): + trace = capture("a=Solid.make_box(2,2,2)\nb=a.moved(Location((4,0,0)))\nresult=Compound([a,b])") + for step in trace["steps"]: + for component in step.get("result_components", []): + component["center"] = [1234, 5678, 9000] + generated, meta = replay_trace_to_ftc(trace, stem="bounds") + assert meta["status"] == "ok" + ftc = tmp_path / "bounds.ftc.py" + ftc.write_text(generated) + stage = tmp_path / "stage" + stage.mkdir() + assert compare_metrics(trace, run(ftc, stage), meta)["quality"] == "accepted" + + +@pytest.mark.parametrize("source", [ + "b=Box(1,2,3)+Cylinder(.75,2.5)\nwith BuildPart() as bp:\n insert(b)\n Cylinder(.4,6,mode=Mode.SUBTRACT)\nresult=bp.part", + "a=Box(4,4,4)\nb=Box(4,4,4).moved(Location((2,0,0)))\nresult=Part([a]) & b", + "section=RegularPolygon(1,6).face()\nresult=Solid.extrude_linear_with_rotation(section,(0,0),(0,0,5),72)", + "face=Face(Wire.make_circle(2),[Wire.make_circle(1)])\nresult=Solid.sweep(face,Wire([Edge.make_three_point_arc((0,0,0),(3,0,2),(5,0,5))]))", + "v=[(1,1,1),(1,-1,-1),(-1,1,-1),(-1,-1,1)]\nf=[[0,1,2],[0,3,1],[0,2,3],[1,3,2]]\nresult=Solid(Shell([Face(Wire.make_polygon([v[i] for i in face])) for face in f])).clean()", + "result=ConvexPolyhedron([(0,0,0),(1,0,0),(0,1,0),(0,0,1)])", +]) +def test_additional_fix_candidates_reconcile(tmp_path, source): + trace = capture(source) + generated, meta = replay_trace_to_ftc(trace, stem="candidate") + assert meta["status"] == "ok", meta + ftc = tmp_path / "candidate.ftc.py" + ftc.write_text(generated) + stage = tmp_path / "stage" + stage.mkdir() + comparison = compare_metrics(trace, run(ftc, stage), meta) + assert comparison["quality"] == "accepted", comparison diff --git a/test/test_build123d_trace.py b/test/test_build123d_trace.py new file mode 100644 index 00000000..a1e68f0a --- /dev/null +++ b/test/test_build123d_trace.py @@ -0,0 +1,260 @@ +"""Feature tracing regressions, independent of the SimpleCAD runtime.""" +from pathlib import Path +import copy +import json +import sys + +import pytest + +b = pytest.importorskip("build123d") +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/research/build123dftc")) +from b3ftc.geometry import edge as transcribe_edge +from b3ftc.geometry import fingerprint +from b3ftc.tracer import trace_source + + +def checked(source): + trace = trace_source(source) + assert trace["b3d_status"] == "ok", trace["error"] + assert trace["unsupported"] == [] + known = set() + for step in trace["steps"]: + assert set(step["inputs"]) <= known + known.add(step["id"]) + assert set(trace["result_refs"]) <= known + json.dumps(trace, allow_nan=False) + return trace + + +def test_builder_profile_is_actual_placed_face_with_hole(): + trace = checked("""from build123d import * +with BuildPart() as bp: + with BuildSketch(Plane.XY.offset(7)): + Rectangle(10, 8) + Circle(2, mode=Mode.SUBTRACT) + extrude(amount=3) +result = bp.part +""") + feature = next(s for s in trace["steps"] if s["op"] == "extrude") + assert len(feature["profile"]["wires"]) == 2 + assert feature["profile"]["origin"][2] == pytest.approx(7) + assert trace["b3d_bbox"][2::3] == pytest.approx([7, 10]) + assert trace["b3d_volume"] == pytest.approx((80 - 4 * 3.141592653589793) * 3) + + +def test_algebra_has_boolean_dependencies_and_transform_snapshots(): + trace = checked("""from build123d import * +base = Box(10, 10, 10) +base.move(Location((3, 0, 0))) +result = base - Pos(3, 0, 0) * Cylinder(2, 20) +""") + assert trace["steps"][-1]["op"] == "cut" + assert len(trace["steps"][-1]["inputs"]) == 2 + transform = [s for s in trace["steps"] if s["op"] == "transform"] + assert transform[0]["result"]["bbox"][0] == -5 + assert transform[1]["result"]["bbox"][0] == -2 + assert trace["b3d_volume"] == pytest.approx(1000 - 40 * 3.141592653589793) + + +def test_nested_builder_publication_is_captured_once(): + trace = checked("""from build123d import * +with BuildPart(Plane.XY.offset(10)) as bp: + with Locations((4, 0, 0)): + Box(2, 2, 2) +result = bp.part +""") + assert len([s for s in trace["steps"] if s["op"] == "box"]) == 1 + assert trace["b3d_bbox"] == pytest.approx([3, -1, 9, 5, 1, 11]) + + +def test_deepcopy_mirror_preserves_feature_reference(): + trace = checked("""from build123d import * +import copy +original = Solid.make_box(2, 3, 4) +result = copy.deepcopy(original).mirror(Plane.YZ) +""") + assert [s["op"] for s in trace["steps"]] == ["box", "transform"] + assert trace["b3d_bbox"] == pytest.approx([-2, 0, 0, 0, 3, 4]) + + +def test_multiple_solid_components_and_union_groups(): + trace = checked("""from build123d import * +a = Solid.make_box(1, 1, 1) +b = Solid.make_box(1, 1, 1).moved(Location((3, 0, 0))) +group = a.fuse(b).moved(Location((0, 0, 2))) +result = group.solids()[0].moved(Location((0, 2, 0))) +""") + union = next(s for s in trace["steps"] if s["op"] == "union") + assert union["result"]["solid_count"] == 2 + assert len(union["result_groups"]) == 2 + assert all(group for group in union["result_groups"]) + assert any(s["op"] == "component" for s in trace["steps"]) + assert trace["b3d_volume"] == pytest.approx(1) + + +def test_selection_fingerprints_for_fillet_and_shell(): + trace = checked("""from build123d import * +with BuildPart() as bp: + Box(10, 10, 10) + fillet(bp.edges().filter_by(Axis.Z), 1) + offset(amount=-.5, openings=bp.faces().sort_by(Axis.Z)[-1]) +result=bp.part +""") + fillet = next(s for s in trace["steps"] if s["op"] == "fillet") + shell = next(s for s in trace["steps"] if s["op"] == "offset") + assert len(fillet["selection"]) == 4 + assert shell["selection"][0]["kind"] == "face" + assert "normal" in shell["selection"][0] + + +def test_trimmed_spline_retains_exact_control_points(): + original = b.Edge.make_spline([(0, 0), (1, 2), (3, 1), (4, 0)]) + trimmed = original.trim(.2, .8) + encoded = transcribe_edge(trimmed) + assert encoded["kind"] == "bspline" + assert encoded["start"] == pytest.approx(tuple(trimmed.position_at(0))) + assert encoded["end"] == pytest.approx(tuple(trimmed.position_at(1))) + assert encoded["poles"][0] == pytest.approx(encoded["end"] if encoded["reversed"] else encoded["start"]) + + +def test_absolute_location_assignment_and_exact_conic_trim(): + trace = checked("""from build123d import * +result = Solid.make_box(2,3,4).moved(Location((2,0,0))) +result.location = Location((5,0,0)) +""") + assert trace["b3d_bbox"][0] == pytest.approx(5) + assert trace["steps"][-1]["parameters"]["matrix"][0][3] == pytest.approx(3) + conic = b.Edge.make_ellipse(3, 2, start_angle=10, end_angle=150) + data = transcribe_edge(conic) + assert data["kind"] == "bspline" + assert data["degree"] == 2 + assert any(weight != 1 for weight in data["weights"]) + + +def test_curved_selection_center_is_mass_center_and_no_arbitrary_surface_normal(): + arc = b.Edge.make_three_point_arc((1, 0), (0, 1), (-1, 0)) + data = fingerprint(arc) + assert data["center"] == pytest.approx(tuple(arc.center(b.CenterOf.MASS))) + assert data["center"] != pytest.approx(tuple(arc.center())) + sphere = b.Solid.make_sphere(2) + curved = fingerprint(sphere.faces()[0]) + assert "normal" not in curved + + +def test_scaling_resolves_actual_default_location_center(): + trace = checked("""from build123d import * +placed = Solid.make_box(2,3,4).moved(Location((10,0,0))) +result = placed.scale(2) +""") + assert trace["steps"][-1]["parameters"]["about"] == pytest.approx([10, 0, 0]) + assert trace["b3d_bbox"] == pytest.approx([10, 0, 0, 14, 6, 8]) + + +def test_factory_parameters_partial_angles_and_generator_inputs_are_preserved(): + trace = checked("""from build123d import * +a = Solid.make_cylinder(2,3,angle=120) +b = Solid.make_cone(3,1,4,angle=180).moved(Location((10,0,0))) +c = Solid.make_torus(5,1,start_angle=-30,end_angle=80,major_angle=140).moved(Location((20,0,0))) +result = a.fuse(b,c) +""") + assert next(s for s in trace["steps"] if s["op"] == "cylinder")["parameters"]["angle"] == 120 + torus = next(s for s in trace["steps"] if s["op"] == "torus") + assert torus["parameters"]["major_angle"] == 140 + trace = checked("""from build123d import * +wires = (Wire.make_circle(r, plane=Plane.XY.offset(z)) for r,z in [(2,0),(3,5)]) +result = Solid.make_loft(wires) +""") + assert len(trace["steps"][-1]["profiles"]) == 2 + + +def test_draft_is_transcribed_and_nonplanar_thicken_reports_surface_boundary(): + trace = checked("""from build123d import * +body = Solid.make_box(5,5,5) +side = body.faces().filter_by(Plane.YZ)[-1] +result = body.draft([side], Plane.XY, 5) +""") + draft = trace["steps"][-1] + assert draft["op"] == "draft" + assert len(draft["selection"]) == 1 + assert draft["parameters"]["neutral_plane"]["normal"] == [0, 0, 1] + trace = trace_source("""from build123d import * +face = Solid.make_sphere(2).faces()[0] +result = Solid.thicken(face, .2) +""") + assert trace["unsupported"] + assert any(s["op"] == "thicken" and "geometry_error" in s for s in trace["steps"]) + + +def test_unknown_kernel_solid_is_reported_and_errors_restore_patches(): + original = b.Solid.make_box.__func__ + owned_attributes = set(b.Solid.__dict__) + trace = trace_source("""from build123d import Solid +from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox +result = Solid(BRepPrimAPI_MakeBox(2, 3, 4).Solid()) +""") + assert trace["unsupported"] + assert trace["steps"][-1]["op"] == "unsupported" + assert b.Solid.make_box.__func__ is original + trace = trace_source("from build123d import *\nresult=Box(1,2,3)\nraise RuntimeError('expected')") + assert trace["b3d_status"] == "error" + assert "expected" in trace["error"] + assert b.Solid.make_box.__func__ is original + assert set(b.Solid.__dict__) == owned_attributes + + +def test_rational_circle_volume_uses_span_aware_adaptive_integration(): + trace = checked("""from build123d import * +from math import sqrt +points = [(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1),(1,-1),(1,0)] +weights = [1,sqrt(.5),1,sqrt(.5),1,sqrt(.5),1,sqrt(.5),1] +curve = Edge.make_bspline(points,[0,0,0,1,1,2,2,3,3,4,4,4],2,weights) +result = Solid.extrude(Face(Wire([curve])), (0,0,1)) +""") + assert trace["b3d_volume"] == pytest.approx(3.141592653589793, rel=1e-9) + assert trace["b3d_area"] == pytest.approx(4 * 3.141592653589793, rel=1e-9) + assert "b3d_default_volume" in trace + assert trace["steps"][-1]["profile"]["wires"][0]["edges"][0]["kind"] == "bspline" + + +def test_multiple_intersection_results_reference_individual_components(): + trace = checked("""from build123d import * +base = Solid.make_box(10,2,2) +left = Solid.make_box(2,4,4) +right = Solid.make_box(2,4,4).moved(Location((8,0,0))) +intersection = base.intersect(left.fuse(right)) +result = intersection[0].moved(Location((0,10,0))) +""") + assert any(s["op"] == "component" for s in trace["steps"]) + assert trace["solid_count"] == 1 + assert trace["b3d_volume"] == pytest.approx(8) + + +@pytest.mark.parametrize("result", ["a-a", "[]", "()", "None", "Compound([])"]) +def test_explicit_empty_result_never_promotes_old_solid(result): + trace = trace_source(f"from build123d import *\na=Box(2,2,2)\nresult={result}") + assert trace["b3d_status"] == "error" + assert trace["result_refs"] == [] + assert trace["solid_count"] == 0 + + +@pytest.mark.parametrize("z", [-10, 10]) +def test_both_split_with_empty_side_is_serializable(z): + trace = checked(f"""from build123d import * +a = Solid.make_box(2,2,2) +result = a.split(Plane.XY.offset({z}), keep=Keep.BOTH) +""") + assert trace["solid_count"] == 1 + assert trace["b3d_valid"] + assert trace["b3d_volume"] == pytest.approx(8) + + +@pytest.mark.parametrize("op", ["revolve", "sweep"]) +def test_face_section_ignores_explicit_inner_wires(op): + call = ("Solid.revolve(section, 360, Axis((0,0,0),(0,1,0)), inner_wires=[hole])" + if op == "revolve" else "Solid.sweep(section, Wire.make_polygon([(3,0,0),(3,0,5)]), inner_wires=[hole])") + trace = checked(f"""from build123d import * +section = Face(Wire.make_circle(1, plane=Plane((3,0,0)))) +hole = Wire.make_circle(.2, plane=Plane((3,0,0))) +result = {call} +""") + assert not trace["steps"][-1].get("inner_wires") diff --git a/test/test_build123d_translation_output.py b/test/test_build123d_translation_output.py new file mode 100644 index 00000000..701996e3 --- /dev/null +++ b/test/test_build123d_translation_output.py @@ -0,0 +1,285 @@ +"""Format and executable geometry tests for build123d FTC translation. + +Fixtures model the trace protocol without depending on build123d itself. +""" +from __future__ import annotations +import importlib.util +import math +import re +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'tools/research/build123dftc')) +from b3ftc.replay import replay_trace_to_ftc + + +def rectangle(x=10, y=6, z=0): + points = [(0, 0, z), (x, 0, z), (x, y, z), (0, y, z)] + return {'origin': (0, 0, z), 'u': (1, 0, 0), 'v': (0, 1, 0), + 'normal': (0, 0, 1), 'wires': [{'edges': [ + {'kind': 'line', 'start': a, 'end': b} + for a, b in zip(points, points[1:] + points[:1])]}]} + + +def circle(radius, z=0): + return {'origin': (0, 0, z), 'u': (1, 0, 0), 'v': (0, 1, 0), + 'normal': (0, 0, 1), 'wires': [{'edges': [ + {'kind': 'circle', 'center': (0, 0, z), 'radius': radius, + 'normal': (0, 0, 1), 'x_dir': (1, 0, 0)}]}]} + + +def trace(*steps): + return {'schema_version': '1.0', 'steps': list(steps), + 'result_refs': [steps[-1]['id']], 'unsupported': []} + + +class FormatTests(unittest.TestCase): + def test_vertex_loft_sections_use_keyword_arguments(self): + source, meta = replay_trace_to_ftc(trace({'id': 's1', 'op': 'loft', + 'profiles': [circle(2), {'kind': 'vertex', 'point': (0, 0, 5)}], + 'parameters': {'ruled': True}}), stem='pointed') + self.assertEqual(meta['status'], 'ok', meta) + self.assertIn('scad.make_point_rvertex(x=0, y=0, z=5)', source) + + def test_real_recorded_fixtures_need_no_build123d_dependency(self): + for path in sorted((ROOT / 'test/build123d_traces').glob('*.trace.json')): + with self.subTest(fixture=path.name): + import json + source, meta = replay_trace_to_ftc(json.loads(path.read_text()), stem=path.stem) + self.assertEqual(meta['status'], 'ok', meta['unsupported']) + compile(source, str(path), 'exec') + self.assertNotIn('import build123d', source) + self.assertNotIn('load_brep', source) + def test_sketch_extrusion_and_explicit_module_contract(self): + source, meta = replay_trace_to_ftc(trace({'id': 's1', 'op': 'extrude', + 'profile': rectangle(), 'parameters': {'direction': (0, 0, 1), 'distance': 4}}), stem='plate') + self.assertEqual(meta['status'], 'ok', meta) + self.assertIn('scad.make_sketch_rsketch(', source) + self.assertIn('scad.make_face_from_sketch_rface(', source) + self.assertIn('scad.extrude_rsolid(', source) + self.assertIn("project_root=Path(__file__).parent", source) + self.assertIn('BODIES = list(bodies)', source) + self.assertRegex(source, r'feature: extrude \(build, profile=geometry\)') + for text in ('import build123d', 'import OCP', 'load_brep', '.get_edges()', '.get_faces()'): + self.assertNotIn(text, source) + compile(source, '', 'exec') + + def test_unknown_feature_blocks_dependents(self): + source, meta = replay_trace_to_ftc(trace({'id': 's1', 'op': 'unknown'}, + {'id': 's2', 'op': 'translate', 'inputs': ['s1'], 'parameters': {'vector': (1, 0, 0)}}), stem='unknown') + self.assertEqual(meta['status'], 'partial') + self.assertTrue(any('unknown' in str(x) for x in meta['unsupported'])) + self.assertIn('feature tree produced no solid', source) + + def test_modifiers_use_individual_fingerprints(self): + payload = trace({'id': 's1', 'op': 'box', 'parameters': {'length': 10, 'width': 10, 'height': 10}}, + {'id': 's2', 'op': 'fillet', 'inputs': ['s1'], 'parameters': {'radius': 1}, + 'selection': [{'kind': 'edge', 'geom_type': 'LINE', 'center': [0, 0, 5], 'length': 10, 'area': 0}]}) + source, meta = replay_trace_to_ftc(payload, stem='filleted') + self.assertEqual(meta['status'], 'ok', meta) + self.assertIn('ql.edges()', source) + self.assertIn('.take(1).exactly(1)', source) + self.assertIn("ql.prop('geom.length'", source) + self.assertIn('scad.fillet_rsolid(', source) + self.assertNotIn('geom.area', source) + + def test_no_final_snapshot_fallback(self): + source, meta = replay_trace_to_ftc({'steps': [], 'final_brep': 'ignored'}, stem='empty') + self.assertEqual(meta['status'], 'partial') + self.assertNotIn('ignored', source) + self.assertNotIn('load_brep', source) + + def test_explicit_empty_results_do_not_promote_unused_tool(self): + payload = trace({'id': 's1', 'op': 'box', 'parameters': {'length': 1, 'width': 2, 'height': 3}}) + payload['result_refs'] = [] + source, meta = replay_trace_to_ftc(payload) + self.assertEqual(meta['status'], 'partial') + self.assertIn('feature tree produced no solid', source) + + def test_draft_uses_face_predicate_and_neutral_plane(self): + payload = trace({'id': 's1', 'op': 'box', 'parameters': {'length': 2, 'width': 2, 'height': 3}}, + {'id': 's2', 'op': 'draft', 'inputs': ['s1'], 'parameters': {'angle': 5, + 'neutral_plane': {'origin': (0, 0, 0), 'normal': (0, 0, 1)}}, + 'selection': [{'kind': 'face', 'geom_type': 'PLANE', 'center': (2, 1, 1.5), 'area': 6, 'length': 0}]}) + source, meta = replay_trace_to_ftc(payload) + self.assertEqual(meta['status'], 'ok', meta) + self.assertIn('scad.draft_rsolid(', source) + self.assertIn('faces_to_draft=ql.faces()', source) + self.assertNotIn('geom.length', source) + compile(source, '', 'exec') + + +class GeometryTests(unittest.TestCase): + def test_many_holes_use_small_sketches_and_preserve_all_loops(self): + profile = rectangle(500, 600) + for i in range(25): + for j in range(25): + x, y = 5 + 10 * i, 5 + 10 * j + points = [(x, y, 0), (x + 1, y, 0), (x + 1, y + 1, 0), (x, y + 1, 0)] + profile['wires'].append({'edges': [{'kind': 'line', 'start': a, 'end': b} + for a, b in zip(points, points[1:] + points[:1])]}) + payload = trace({'id': 's1', 'op': 'extrude', 'profile': profile, + 'parameters': {'direction': (0, 0, 1), 'distance': 1}}) + source, _ = replay_trace_to_ftc(payload) + self.assertIn('scad.make_face_from_wires_rface(', source) + self.build(payload, 500 * 600 - 625) + + def test_real_recorded_fixtures_reconcile(self): + import json + for path in sorted((ROOT / 'test/build123d_traces').glob('*.trace.json')): + with self.subTest(fixture=path.name): + payload = json.loads(path.read_text()) + self.build(payload, payload['b3d_volume']) + def build(self, payload, expected, *, constraints_mode=False): + source, meta = replay_trace_to_ftc(payload, stem='geometry', constraints_mode=constraints_mode) + self.assertEqual(meta['status'], 'ok', meta) + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / 'model.ftc.py' + path.write_text(source) + spec = importlib.util.spec_from_file_location('ftc_geometry_' + path.parent.name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.build() + self.assertEqual(len(module.BODIES), payload.get('solid_count', 1)) + from OCP.BRepCheck import BRepCheck_Analyzer + self.assertTrue(all(BRepCheck_Analyzer(body.wrapped).IsValid() for body in module.BODIES)) + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + def accurate_volume(body): + props = GProp_GProps() + BRepGProp.VolumePropertiesGK_s(body.wrapped, props, 1e-9, True, True) + return float(props.Mass()) + volume = sum(accurate_volume(body) for body in module.BODIES) + self.assertAlmostEqual(volume, expected, delta=max(abs(expected), 1) * 1e-6) + + def test_extruded_rectangle(self): + self.build(trace({'id': 's1', 'op': 'extrude', 'profile': rectangle(), + 'parameters': {'direction': (0, 0, 1), 'distance': 4}}), 240) + + def test_recovered_constraints_preserve_rectangle_geometry(self): + payload = trace({'id': 's1', 'op': 'extrude', 'profile': rectangle(), + 'parameters': {'direction': (0, 0, 1), 'distance': 4}}) + self.build(payload, 240, constraints_mode=True) + source, meta = replay_trace_to_ftc(payload, constraints_mode=True) + self.assertTrue(meta['sketches']) + self.assertEqual(meta['constraint_recovery'][0]['status'], 'riveted') + self.assertIn('scad.constrain_', source) + + def test_face_with_circle_hole(self): + profile = circle(5) + profile['wires'].extend(circle(2)['wires']) + self.build(trace({'id': 's1', 'op': 'extrude', 'profile': profile, + 'parameters': {'direction': (0, 0, -1), 'distance': 3}}), math.pi * 21 * 3) + + def test_clockwise_polygon_hole_is_subtractive(self): + profile = circle(5) + points = [(2, -1, 0), (-2, -1, 0), (-2, 1, 0), (2, 1, 0)] + profile['wires'].append({'edges': [{'kind': 'line', 'start': a, 'end': b} + for a, b in zip(points, points[1:] + points[:1])]}) + self.build(trace({'id': 's1', 'op': 'extrude', 'profile': profile, + 'parameters': {'direction': (0, 0, 1), 'distance': 3}}), (math.pi * 25 - 8) * 3) + + def test_explicit_boolean_cut(self): + self.build(trace({'id': 's1', 'op': 'box', 'parameters': {'length': 10, 'width': 10, 'height': 5}}, + {'id': 's2', 'op': 'cylinder', 'parameters': {'radius': 2, 'height': 5, 'origin': (5, 5, 0)}}, + {'id': 's3', 'op': 'cut', 'inputs': ['s1', 's2']}), 500 - math.pi * 20) + + def test_loft_circle_sections(self): + self.build(trace({'id': 's1', 'op': 'loft', 'profiles': [circle(2), circle(4, 6)], + 'parameters': {'ruled': True}}), math.pi * 6 * (4 + 8 + 16) / 3) + + def test_native_multisection_sweep(self): + self.build(trace({'id': 's1', 'op': 'sweep_multi', 'profiles': [circle(2), circle(4, 6)], + 'path': {'edges': [{'kind': 'line', 'start': (0, 0, 0), 'end': (0, 0, 6)}]}, + 'parameters': {'is_frenet': False}}), 56 * math.pi) + + def test_native_tapered_extrusion(self): + top_radius = 4 - 6 * math.tan(math.radians(15)) + self.build(trace({'id': 's1', 'op': 'extrude_taper', 'profile': circle(4), + 'parameters': {'direction': (0, 0, 6), 'taper': 15}}), + math.pi * 6 * (16 + 4 * top_radius + top_radius ** 2) / 3) + + def test_native_until_extrusion(self): + self.build(trace({'id': 's1', 'op': 'box', + 'parameters': {'length': 10, 'width': 10, 'height': 3, 'origin': (-5, -5, 5)}}, + {'id': 's2', 'op': 'extrude_until', 'inputs': ['s1'], 'profile': circle(1), + 'parameters': {'direction': (0, 0, 1), 'until': 'NEXT'}}), 5 * math.pi) + + def test_native_twisted_extrusion(self): + self.build(trace({'id': 's1', 'op': 'extrude_twist', 'profile': rectangle(), + 'parameters': {'normal': (0, 0, 4), 'center': (0, 0, 0), 'angle': 90}}), 240) + + def test_affine_shear_preserves_volume(self): + self.build(trace({'id': 's1', 'op': 'box', 'parameters': {'length': 10, 'width': 6, 'height': 4}}, + {'id': 's2', 'op': 'transform', 'inputs': ['s1'], 'parameters': {'matrix': [ + [1, .5, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}}), 240) + + def test_both_split_preserves_original_component_order(self): + self.build(trace({'id': 's1', 'op': 'box', + 'parameters': {'length': 2, 'width': 3, 'height': 8}, + 'result': {'bbox': [0, 0, 0, 2, 3, 8], 'solid_count': 1}}, + {'id': 's2', 'op': 'split', 'inputs': ['s1'], + 'parameters': {'tool': {'origin': (0, 0, 2), 'normal': (0, 0, 1)}, 'keep': 'BOTH'}, + 'result_component_sides': ['BOTTOM', 'TOP']}, + {'id': 's3', 'op': 'component', 'inputs': ['s2'], 'parameters': {'index': 0}}), 12) + + def test_exact_ellipse_sketch(self): + profile = circle(2) + profile['wires'][0]['edges'] = [{'kind': 'ellipse', 'center': (0, 0, 0), + 'major_radius': 4, 'minor_radius': 2, 'x_dir': (1, 0, 0), 'y_dir': (0, 1, 0)}] + self.build(trace({'id': 's1', 'op': 'extrude', 'profile': profile, + 'parameters': {'direction': (0, 0, 1), 'distance': 3}}), math.pi * 8 * 3) + + def test_translate_preserves_volume(self): + self.build(trace({'id': 's1', 'op': 'sphere', 'parameters': {'radius': 3}}, + {'id': 's2', 'op': 'translate', 'inputs': ['s1'], 'parameters': {'vector': (5, -8, 2)}}), 36 * math.pi) + + def test_partial_cylinder(self): + self.build(trace({'id': 's1', 'op': 'cylinder', + 'parameters': {'radius': 3, 'height': 4, 'angle': 180}}), 18 * math.pi) + + def test_partial_cone(self): + self.build(trace({'id': 's1', 'op': 'cone', + 'parameters': {'base_radius': 3, 'top_radius': 0, 'height': 4, 'angle': 90}}), 3 * math.pi) + + def test_partial_sphere(self): + self.build(trace({'id': 's1', 'op': 'sphere', + 'parameters': {'radius': 3, 'angle1': 0, 'angle2': 90, 'angle3': 360}}), 18 * math.pi) + + def test_closed_rational_spline_profile(self): + profile = circle(1) + poles = [(1, 0, 0), (1, 1, 0), (0, 1, 0), (-1, 1, 0), (-1, 0, 0), + (-1, -1, 0), (0, -1, 0), (1, -1, 0), (1, 0, 0)] + profile['wires'][0]['edges'] = [{'kind': 'bspline', 'poles': poles, 'degree': 2, + 'knots': [0, .25, .5, .75, 1], 'multiplicities': [3, 2, 2, 2, 3], + 'weights': [1, math.sqrt(.5), 1, math.sqrt(.5), 1, math.sqrt(.5), 1, math.sqrt(.5), 1], + 'start': poles[0], 'end': poles[-1]}] + self.build(trace({'id': 's1', 'op': 'extrude', 'profile': profile, + 'parameters': {'direction': [0, 0, 1], 'distance': 2}}), 2 * math.pi) + + def test_reversed_cubic_basis_preserves_entire_curve(self): + profile = circle(1) + poles = [(0, 0, 0), (0, 3, 0), (2, 4, 0), (4, 0, 0)] + profile['wires'][0]['edges'] = [{'kind': 'bezier', 'poles': poles, 'degree': 3, + 'start': poles[-1], 'end': poles[0], 'reversed': True}, + {'kind': 'line', 'start': poles[0], 'end': poles[-1]}] + # Exact Green-integral area of this cubic and its closing chord is 7.5. + self.build(trace({'id': 's1', 'op': 'extrude', 'profile': profile, + 'parameters': {'direction': [0, 0, 1], 'distance': 2}}), 15) + + def test_multi_body_is_complete_assembly(self): + payload = trace({'id': 's1', 'op': 'box', 'parameters': {'length': 2, 'width': 3, 'height': 4}}, + {'id': 's2', 'op': 'box', 'parameters': {'length': 4, 'width': 2, 'height': 3, 'origin': [20, 0, 0]}}, + {'id': 's3', 'op': 'compound', 'inputs': ['s1', 's2']}) + payload['solid_count'] = 2 + self.build(payload, 48) + source, meta = replay_trace_to_ftc(payload) + self.assertEqual(meta['artifact_type'], 'assembly') + self.assertIn('@scad.assemble(', source) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_build123d_unknown_api.py b/test/test_build123d_unknown_api.py new file mode 100644 index 00000000..3d77ccf6 --- /dev/null +++ b/test/test_build123d_unknown_api.py @@ -0,0 +1,39 @@ +"""Unknown construction must remain visible instead of aliasing an old solid.""" +from pathlib import Path +import sys + +import pytest + +pytest.importorskip("build123d") +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools/research/build123dftc")) +from b3ftc.replay import replay_trace_to_ftc +from b3ftc.tracer import trace_source +from b3ftc.validation import compare_metrics + + +@pytest.mark.parametrize("statement", [ + "result = Box(2,3,4).to_splines()", + "from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox\nresult = Solid(BRepPrimAPI_MakeBox(2,3,4).Solid())", +]) +def test_uninstrumented_api_cannot_silently_accept(statement): + trace = trace_source("from build123d import *\n" + statement) + assert trace["b3d_status"] == "ok", trace["error"] + assert trace["unsupported"], trace + assert any(step["op"] == "unsupported" for step in trace["steps"]) + _, metadata = replay_trace_to_ftc(trace) + assert metadata["status"] == "partial", metadata + actual = {"status": "ok", "volume": trace["b3d_volume"], + "bbox": trace["b3d_bbox"], "solid_count": trace["solid_count"], + "body_count": trace["solid_count"], "is_valid": True} + assert compare_metrics(trace, actual, metadata)["quality"] == "rejected" + + +def test_failed_source_rejects_even_after_valid_feature_was_created(): + trace = trace_source("from build123d import *\nresult=Box(2,3,4)\nraise RuntimeError('unknown operation')") + _, metadata = replay_trace_to_ftc(trace) + actual = {"status": "ok", "volume": 24, "bbox": [-1,-1.5,-2,1,1.5,2], + "solid_count": 1, "body_count": 1, "is_valid": True} + result = compare_metrics(trace, actual, metadata) + assert result["quality"] == "rejected" + assert "source_failed" in result["reasons"] diff --git a/test/test_build123d_validation.py b/test/test_build123d_validation.py new file mode 100644 index 00000000..94c55faf --- /dev/null +++ b/test/test_build123d_validation.py @@ -0,0 +1,244 @@ +"""Acceptance must reject missing evidence and geometric false positives.""" +from __future__ import annotations + +import json +import math +from pathlib import Path +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools/research/build123dftc")) +from b3ftc.validation import compare_metrics, validate_directory, validate_row + + +def expected(**changes): + trace = {"b3d_status": "ok", "b3d_volume": 1000.0, + "b3d_bbox": [-5, -5, 0, 5, 5, 10], "solid_count": 1, + "result_refs": ["s1"], "unsupported": []} + trace.update(changes) + return trace + + +def actual(**changes): + metrics = {"status": "ok", "volume": 1000.0, + "bbox": [-5, -5, 0, 5, 5, 10], "solid_count": 1, + "body_count": 1, "is_valid": True} + metrics.update(changes) + return metrics + + +META = {"status": "ok", "unsupported": []} + + +def test_matching_metrics_accepted(): + result = compare_metrics(expected(), actual(), META) + assert result["quality"] == "accepted", result + assert result["bbox_error"] == 0 + + +def test_reference_failure_reports_basic_geometry_separately(tmp_path, monkeypatch): + import b3ftc.validation as validation + ftc = tmp_path / "sample.ftc.py" + ftc.write_text("# worker is isolated by this test") + trace = tmp_path / "sample.trace.json" + trace.write_text(json.dumps(expected())) + ftc.with_name("sample.meta.json").write_text(json.dumps(META)) + monkeypatch.setattr(validation, "_worker", lambda *args, **kwargs: + actual(reference_status="error", reference_error="kernel comparison failed")) + row = validate_row(ftc, trace, reference_step=tmp_path / "reference.step") + assert row["quality"] == "rejected" + assert row["metrics_quality"] == "accepted" + assert row["metrics_reasons"] == [] + assert row["reasons"] == ["reference_error"] + + +def test_rational_circle_cylinder_has_mathematical_area_four_pi(): + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire, BRepBuilderAPI_MakeFace + from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism + from OCP.Geom import Geom_Circle + from OCP.GeomConvert import GeomConvert + from OCP.gp import gp_Ax2, gp_Pnt, gp_Dir, gp_Vec + from b3ftc._validate_worker import _area, _volume + + circle = Geom_Circle(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), 1) + spline = GeomConvert.CurveToBSplineCurve_s(circle) + assert spline.IsRational() + edge = BRepBuilderAPI_MakeEdge(spline).Edge() + wire = BRepBuilderAPI_MakeWire(edge).Wire() + face = BRepBuilderAPI_MakeFace(wire, True).Face() + solid = BRepPrimAPI_MakePrism(face, gp_Vec(0, 0, 1)).Shape() + # OCCT 7.9's continuity split adds about 7.7e-9 relative integration + # error here; this precision still detects the original 0.0028 error. + assert _area(solid) == pytest.approx(4 * math.pi, rel=1e-8) + assert _volume(solid) == pytest.approx(math.pi, rel=1e-9) + + +def test_reference_export_cannot_change_geometry_to_obtain_acceptance(): + metrics = actual(reference_status="ok", reference_volume=1000, + symmetric_difference_volume=0, roundtrip_actual_volume=900, + roundtrip_actual_bbox=[-5, -5, 0, 5, 5, 10], + roundtrip_actual_solid_count=1, roundtrip_actual_valid=True) + row = compare_metrics(expected(), metrics, META, require_reference=True) + assert row["quality"] == "rejected" + assert "reference_roundtrip_changed_geometry" in row["reasons"] + + +@pytest.mark.parametrize("changes", [ + {"b3d_status": "error"}, {"b3d_volume": None}, {"b3d_bbox": None}, + {"solid_count": None}, {"result_refs": []}, {"b3d_volume": math.nan}, + {"b3d_bbox": [0, 0, 0]}, {"unsupported": ["unsupported sweep"]}, +]) +def test_source_failure_or_missing_evidence_rejected(changes): + assert compare_metrics(expected(**changes), actual(), META)["quality"] == "rejected" + + +@pytest.mark.parametrize("metadata", [None, {}, {"status": "partial"}, + {"status": "ok", "unsupported": ["torus"]}, + {"status": "ok", "unknown": ["operation"]}, + {"status": "ok", "unknown_operations": 1}]) +def test_incomplete_conversion_never_accepted(metadata): + assert compare_metrics(expected(), actual(), metadata)["quality"] == "rejected" + + +@pytest.mark.parametrize("changes", [ + {"body_count": 0}, {"solid_count": 2}, {"is_valid": False}, + {"volume": None}, {"bbox": None}, {"volume": math.inf}, + {"bbox": [0, -5, 0, 10, 5, 10]}, +]) +def test_actual_failure_including_rigid_translation_rejected(changes): + assert compare_metrics(expected(), actual(**changes), META)["quality"] == "rejected" + + +def write_case(root: Path, stem: str, source: str, trace=None, metadata=None): + ftc_dir, traces_dir = root / "ftc", root / "traces" + ftc_dir.mkdir(exist_ok=True) + traces_dir.mkdir(exist_ok=True) + ftc = ftc_dir / (stem + ".ftc.py") + ftc.write_text(source) + trace_path = traces_dir / (stem + ".trace.json") + trace_path.write_text(json.dumps(trace or expected())) + ftc.with_name(stem + ".meta.json").write_text(json.dumps(metadata or META)) + return ftc, trace_path + + +def box_source(center_x=0): + return f'''from pathlib import Path +import simplecadapi as scad +BODIES = None +@scad.part(id='validation-box', project_root=Path(__file__).parent) +def build(): + global BODIES + body = scad.make_box_rsolid(10, 10, 10, bottom_face_center=({center_x}, 0, 0)) + BODIES = [body] + return body +''' + + +def test_worker_builds_real_topology_and_checks_position(tmp_path): + ftc, trace = write_case(tmp_path, "box", box_source()) + row = validate_row(ftc, trace, timeout=30) + assert row["quality"] == "accepted", row + assert row["ftc_solid_count"] == 1 + ftc.write_text(box_source(10)) + row = validate_row(ftc, trace, timeout=30) + assert row["quality"] == "rejected", row + assert "bbox_mismatch" in row["reasons"] + + +def test_no_bodies_cannot_fall_back_to_returned_part(tmp_path): + source = box_source().replace("BODIES = [body]", "BODIES = []") + ftc, trace = write_case(tmp_path, "empty", source) + row = validate_row(ftc, trace, timeout=30) + assert row["quality"] == "rejected", row + assert "empty_bodies" in row["reasons"] + + +def test_timeout_and_build_failure_do_not_stop_batch(tmp_path): + write_case(tmp_path, "a_timeout", "import time\ntime.sleep(100)\n") + write_case(tmp_path, "b_failure", "raise RuntimeError('model failure')\n") + write_case(tmp_path, "c_good", box_source()) + report = validate_directory(tmp_path / "ftc", tmp_path / "traces", timeout=8) + assert [row["quality"] for row in report["rows"]] == ["rejected", "rejected", "accepted"], report + assert report["rows"][0]["build"] == "timeout" + assert "model failure" in report["rows"][1]["error"] + + +def test_missing_trace_rejected_but_other_cases_continue(tmp_path): + ftc, trace = write_case(tmp_path, "a_missing", box_source()) + trace.unlink() + write_case(tmp_path, "b_good", box_source()) + report = validate_directory(ftc.parent, tmp_path / "traces", timeout=30) + assert report["counts"] == {"accepted": 1, "partial": 0, "rejected": 1} + + +def test_reference_step_detects_equal_volume_equal_bbox_different_holes(tmp_path): + from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut + from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder + from OCP.gp import gp_Pnt, gp_Ax2, gp_Dir + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + from OCP.IFSelect import IFSelect_RetDone + + box = BRepPrimAPI_MakeBox(gp_Pnt(-5, -5, 0), 10, 10, 10).Shape() + hole = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(2, 0, 0), gp_Dir(0, 0, 1)), 1, 10).Shape() + reference = BRepAlgoAPI_Cut(box, hole).Shape() + step = tmp_path / "reference.step" + writer = STEPControl_Writer() + assert writer.Transfer(reference, STEPControl_AsIs) == IFSelect_RetDone + assert writer.Write(str(step)) == IFSelect_RetDone + source = box_source().replace("BODIES = [body]", "body = scad.cut_rsolid(body, scad.make_cylinder_rsolid(1, 10, bottom_face_center=(-2, 0, 0)))\n BODIES = [body]") + ftc, trace = write_case(tmp_path, "wrong_hole", source, + expected(b3d_volume=1000 - math.pi * 10)) + coarse = validate_row(ftc, trace, timeout=30) + assert coarse["quality"] == "accepted", coarse + strict = validate_row(ftc, trace, timeout=30, reference_step=step) + assert strict["quality"] == "rejected", strict + assert "symmetric_difference_mismatch" in strict["reasons"] + assert strict["symmetric_difference_volume"] > 60 + independent = validate_row(ftc, trace, timeout=30, reference_step=step, + reference_python=sys.executable) + assert independent["quality"] == "rejected", independent + assert independent["reference_kernel"] + assert "symmetric_difference_mismatch" in independent["reasons"] + ftc.write_text(source.replace("bottom_face_center=(-2, 0, 0)", "bottom_face_center=(2, 0, 0)")) + identical = validate_row(ftc, trace, timeout=30, reference_step=step, + reference_python=sys.executable) + assert identical["quality"] == "accepted", identical + assert identical["symmetric_difference_volume"] == pytest.approx(0) + + +def test_requested_missing_reference_rejected(tmp_path): + ftc, trace = write_case(tmp_path, "box", box_source()) + row = validate_row(ftc, trace, timeout=30, reference_step=tmp_path / "absent.step") + assert row["quality"] == "rejected", row + assert "reference_error" in row["reasons"] + + +@pytest.mark.parametrize("returned", ["None", "scad.make_box_rsolid(1, 1, 1)", + "scad.translate_shape(body, (10, 0, 0))"]) +def test_actual_build_return_cannot_hide_behind_matching_bodies(tmp_path, returned): + source = box_source().replace("return body", f"return {returned}") + # A plain build allows None so the worker itself must reject it. + source = "\n".join(line for line in source.splitlines() if not line.startswith("@scad.part")) + ftc, trace = write_case(tmp_path, "wrong_return", source) + row = validate_row(ftc, trace, timeout=30) + assert row["quality"] == "rejected", row + + +def test_returned_assembly_is_checked_in_world_coordinates(tmp_path): + source = """import simplecadapi as scad +BODIES = None +def build(): + global BODIES + body = scad.make_box_rsolid(10, 10, 10) + BODIES = [body] + part = scad.make_part_rpart(part_id='box', body=body) + assembly = scad.make_assembly_rassembly(assembly_id='placed') + return scad.add_component_rassembly(assembly=assembly, item=part, + component_id='box', placement=scad.make_placement_rplacement(origin=(10,0,0))) +""" + ftc, trace = write_case(tmp_path, "placed", source) + row = validate_row(ftc, trace, timeout=30) + assert row["quality"] == "rejected", row + assert "does not match" in row["error"] diff --git a/test/test_deformation_features.py b/test/test_deformation_features.py new file mode 100644 index 00000000..88088d40 --- /dev/null +++ b/test/test_deformation_features.py @@ -0,0 +1,97 @@ +"""Geometric and graph-replay contracts needed by source translators.""" +import math + +import pytest +import simplecadapi as scad + + +def test_nonuniform_scale_volume_and_replay(): + with scad.GraphSession(graph_id="scale-test") as session: + original = scad.make_box_rsolid(width=10, height=12, depth=14) + scaled = scad.scale_rsolid(solid=original, factors=(2, 3, 0.5), center=(5, 6, 7)) + session.capture_result(value=scaled) + assert scaled.get_volume() == pytest.approx(original.get_volume() * 3) + assert original.get_volume() == pytest.approx(1680) + rebuilt = scad.replay_model_json(scad.export_model_json(session=session), strict=True)[0] + assert rebuilt.get_volume() == pytest.approx(scaled.get_volume()) + + +@pytest.mark.parametrize("factors", [(0, 1, 1), (math.nan, 1, 1), (1, 2), (math.inf, 1, 1)]) +def test_invalid_scale_factors_are_rejected(factors): + original = scad.make_box_rsolid(width=10, height=12, depth=14) + with pytest.raises(ValueError): + scad.scale_rsolid(solid=original, factors=factors) + + +@pytest.mark.parametrize("amount", [-1.0, 1.0]) +def test_signed_open_shell_replays(amount): + with scad.GraphSession(graph_id="offset-test") as session: + original = scad.make_box_rsolid(width=10, height=12, depth=14) + openings = scad.ql.faces().where(scad.ql.prop("geom.normal.z", ">", 0.9)).exactly(1) + hollow = scad.offset_rsolid(solid=original, amount=amount, openings=openings) + session.capture_result(value=hollow) + assert 0 < hollow.get_volume() < original.get_volume() + rebuilt = scad.replay_model_json(scad.export_model_json(session=session), strict=True)[0] + assert rebuilt.get_volume() == pytest.approx(hollow.get_volume(), rel=1e-9) + + +def test_closed_outer_offset_increases_volume(): + original = scad.make_box_rsolid(width=10, height=12, depth=14) + expanded = scad.offset_rsolid(solid=original, amount=1.0) + assert expanded.get_volume() > original.get_volume() + + +def test_zero_offset_rejected(): + original = scad.make_box_rsolid(width=10, height=12, depth=14) + with pytest.raises(ValueError): + scad.offset_rsolid(solid=original, amount=0) + + +def test_affine_shear_is_recorded_and_replayed(): + with scad.GraphSession(graph_id="shear") as session: + original = scad.make_box_rsolid(width=10, height=12, depth=14) + transformed = scad.transform_rsolid(solid=original, + matrix=[[1, 0.3, 0, 5], [0, 1, 0, 2], [0, 0, 1, 0], [0, 0, 0, 1]]) + session.capture_result(value=transformed) + assert transformed.get_volume() == pytest.approx(original.get_volume()) + rebuilt = scad.replay_model_json(scad.export_model_json(session=session), strict=True)[0] + assert rebuilt.get_volume() == pytest.approx(transformed.get_volume()) + + +def test_affine_singular_matrix_rejected(): + original = scad.make_box_rsolid(width=10, height=12, depth=14) + with pytest.raises(ValueError): + scad.transform_rsolid(solid=original, matrix=[[0]*4 for _ in range(4)]) + + +def test_set_boolean_keeps_all_output_slots_and_part_replay(): + with scad.GraphSession(graph_id="cut-pieces") as session: + base = scad.make_box_rsolid(10, 2, 2) + tool = scad.make_box_rsolid(2, 4, 4, bottom_face_center=(0, 0, -1)) + pieces = scad.boolean_rsolidlist(left=[base], right=[tool], operation="cut") + assert len(pieces) == 2 + final = scad.make_part_rpart(part_id="second", body=pieces[1]) + session.capture_result(value=final) + replayed = scad.replay_model_json(scad.export_model_json(session=session), strict=True)[0] + from simplecadapi.kernel.ocp_properties import center_of_mass + assert tuple(center_of_mass(replayed.body.wrapped)) == pytest.approx(tuple(center_of_mass(pieces[1].wrapped))) + assert replayed.body.get_volume() == pytest.approx(16) + + +@pytest.mark.parametrize("operation", ["cut", "intersect", "union"]) +def test_set_boolean_compound_members_and_graph_replay(operation): + with scad.GraphSession(graph_id="set-boolean") as session: + left = [scad.make_box_rsolid(2, 2, 2, bottom_face_center=(x, 0, 0)) for x in (-4, 4)] + right = [scad.make_box_rsolid(1, 4, 4, bottom_face_center=(x, 0, -1)) for x in (-4, 4)] + result = scad.boolean_rsolidlist(left=left, right=right, operation=operation) + session.capture_result(value=result) + expected = {"cut": 8, "intersect": 8, "union": 40}[operation] + assert sum(body.get_volume() for body in result) == pytest.approx(expected) + replayed = scad.replay_model_json(scad.export_model_json(session=session), strict=True) + assert len(replayed) == len(result) + assert sum(body.get_volume() for body in replayed) == pytest.approx(expected) + + +def test_set_boolean_complete_removal_returns_no_solid(): + base = scad.make_box_rsolid(2, 2, 2) + assert scad.boolean_rsolidlist(left=[base], right=[base], operation="cut") == [] diff --git a/test/test_draft.py b/test/test_draft.py new file mode 100644 index 00000000..68ba2828 --- /dev/null +++ b/test/test_draft.py @@ -0,0 +1,60 @@ +"""Recorded draft preserves its selected faces and neutral-plane semantics.""" +import math + +import pytest +from OCP.BRepCheck import BRepCheck_Analyzer + +import simplecadapi as s +from simplecadapi import ql +from simplecadapi.operators.draft import draft_rsolid +from simplecadapi.recording.graph import GraphSession + + +def _right_face(body): + return next(face for face in body._iter_faces() if face.get_normal_at().x > .9) + + +@pytest.mark.parametrize("angle", [10, -10, 0]) +def test_single_planar_face_uses_neutral_plane_and_signed_degrees(angle): + body = s.make_box_rsolid(10, 10, 10) + result = draft_rsolid(body, [_right_face(body)], angle=angle) + assert result.get_volume() == pytest.approx(1000 - 500 * math.tan(math.radians(angle)), rel=1e-9) + assert body.get_volume() == pytest.approx(1000) + assert BRepCheck_Analyzer(result.wrapped).IsValid() + + +def test_query_selection_has_same_geometry_as_explicit_faces(): + body = s.make_box_rsolid(10, 10, 10) + selector = ql.faces().where(ql.prop("geom.normal.x", ">", .9)).exactly(1) + selected = draft_rsolid(body, selector, 5, plane_origin=(0, 0, 5)) + explicit = draft_rsolid(body, [_right_face(body)], 5, plane_origin=(0, 0, 5)) + assert selected.get_volume() == pytest.approx(explicit.get_volume(), rel=1e-9) + assert selected.get_volume() == pytest.approx(1000) + + +def test_draft_face_selection_and_plane_are_replayable(): + with GraphSession() as session: + body = s.make_box_rsolid(10, 10, 10) + result = draft_rsolid(body, [_right_face(body)], 10) + session.capture_result(value=result) + node = next(node for node in session.graph.nodes if node.op == "make_draft_rsolid") + assert node.params["angle"] == 10 + assert node.params["selected_face_count"] == 1 + assert len(node.params["selected_face_node_ids"]) == 1 + assert len(node.inputs) == 2 + replayed = s.replay_model_json(s.export_model_json(session))[0] + assert replayed.get_volume() == pytest.approx(result.get_volume(), rel=1e-9) + + +def test_invalid_selection_plane_and_angle_are_rejected(): + body = s.make_box_rsolid(10, 10, 10) + faces = [_right_face(body)] + other = s.make_box_rsolid(1, 1, 1, bottom_face_center=(20, 0, 0)) + with pytest.raises(ValueError, match="belong"): + draft_rsolid(body, [_right_face(other)], 5) + with pytest.raises(ValueError, match="at least one"): + draft_rsolid(body, [], 5) + with pytest.raises(ValueError, match="nonzero"): + draft_rsolid(body, faces, 5, plane_normal=(0, 0, 0)) + with pytest.raises(ValueError, match="angle"): + draft_rsolid(body, faces, 90) diff --git a/test/test_drawing_lazy_pymupdf.py b/test/test_drawing_lazy_pymupdf.py new file mode 100644 index 00000000..fd3866b7 --- /dev/null +++ b/test/test_drawing_lazy_pymupdf.py @@ -0,0 +1,34 @@ +"""pymupdf stays off the package import path; drawing functions name the extra.""" + +import builtins +import subprocess +import sys + +import pytest + +from simplecadapi.inspect.drawing._pymupdf import _LazyPymupdf + + +def test_package_import_does_not_pull_pymupdf() -> None: + code = ( + "import sys, simplecadapi; " + "assert 'pymupdf' not in sys.modules, 'pymupdf imported eagerly'" + ) + subprocess.run([sys.executable, "-c", code], check=True) + + +def test_missing_pymupdf_names_the_drawing_extra(monkeypatch: pytest.MonkeyPatch) -> None: + real_import = builtins.__import__ + + def poisoned(name, *args, **kwargs): + if name == "pymupdf": + raise ImportError(f"No module named '{name}'") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", poisoned) + with pytest.raises(ImportError, match=r"simplecadapi\[drawing\]"): + _LazyPymupdf().Rect + + +def test_present_pymupdf_passes_through() -> None: + assert callable(_LazyPymupdf().open) diff --git a/test/test_editor.py b/test/test_editor.py new file mode 100644 index 00000000..f0021c03 --- /dev/null +++ b/test/test_editor.py @@ -0,0 +1,825 @@ +"""The editor's server-side logic and templates (no kernel is started). + +An end-to-end run against a real marimo kernel is opt-in: +``SCA_EDITOR_E2E=1 pytest test/test_editor.py``. +""" + +from __future__ import annotations + +import json +import os +import tempfile +import time +import unittest +import urllib.request +from pathlib import Path + +import pytest + +pytest.importorskip("starlette") + +from simplecadapi.editor.server import WorkspaceState, _is_function_cell, _module_code, pick_notebook +from simplecadapi.editor.templates import create_notebook, render_notebook +from simplecadapi.runtime import run_notebook +from simplecadapi.runtime.params import ParamSpec, parameters_in_cell + + +def specs_of(code: str) -> dict[str, ParamSpec]: + return {spec.name: spec for spec in parameters_in_cell(code)} + + +class TemplatesTest(unittest.TestCase): + def test_new_part_runs_and_offers_sliders(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = create_notebook(Path(tmp), "bracket", "part") + run = run_notebook(path) + self.assertEqual(run.definition.definition_kind, "single_solid") + self.assertEqual(run.definition.definition_id, "bracket") + from simplecadapi.runtime.params import list_parameters + + controls = {spec.name: spec.control for spec in list_parameters(path)} + self.assertEqual(controls, {"RADIUS": "slider", "HEIGHT": "slider"}) + + def test_new_assembly_uses_the_workspace_parts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + with self.assertRaisesRegex(ValueError, "create a part first"): + create_notebook(root, "unit", "assembly") + create_notebook(root, "bracket", "part") + create_notebook(root, "pin-a", "part") + path = create_notebook(root, "unit", "assembly") + run = run_notebook(path) + self.assertEqual(run.definition.definition_kind, "assembly") + self.assertEqual(run.definition.definition_id, "unit") + + def test_names_and_kinds_are_checked(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + for bad in ("", "1abc", "a b", "../x"): + with self.assertRaises(ValueError): + create_notebook(Path(tmp), bad) + with self.assertRaises(ValueError): + render_notebook("x", "gadget") + create_notebook(Path(tmp), "once") + with self.assertRaises(FileExistsError): + create_notebook(Path(tmp), "once") + + +class PickNotebookTest(unittest.TestCase): + def test_prefers_the_asked_one_then_the_top_assembly(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self.assertIsNone(pick_notebook(root)) + create_notebook(root, "a-part", "part") + create_notebook(root, "b-part", "part") + self.assertEqual(pick_notebook(root), (root / "a-part.py").resolve()) + create_notebook(root, "top", "assembly") + self.assertEqual(pick_notebook(root), (root / "top.py").resolve()) + self.assertEqual(pick_notebook(root, root / "b-part.py"), (root / "b-part.py").resolve()) + + +class ParamCleaningTest(unittest.TestCase): + def state(self) -> WorkspaceState: + state = WorkspaceState(Path(tempfile.gettempdir())) + state.specs = specs_of( + "# ---- params ----\n" + "W = 20.0 # ui: slider(10, 40)\n" + "N = 4 # ui: slider(2, 8)\n" + "F = False\n" + ) + return state + + def test_values_are_clamped_and_typed(self) -> None: + clean = self.state()._clean_params({"W": 99, "N": 3.6, "F": 1, "NOPE": 1}) + self.assertEqual(clean, {"W": 40.0, "N": 4, "F": True}) + self.assertIsInstance(clean["N"], int) + + def test_wrong_kinds_are_dropped(self) -> None: + self.assertEqual(self.state()._clean_params({"W": "7", "F": "yes"}), {}) + self.assertEqual(self.state()._clean_params({"N": True}), {}) + + +class ModuleCodeTest(unittest.TestCase): + def test_top_level_return_is_stripped_for_code_mode(self) -> None: + code = "x = 1\nif x:\n return\nreturn (x,)" + self.assertEqual(_module_code(code), "x = 1\nif x:\n pass\n(x,)") + + def test_returns_inside_functions_and_classes_are_code(self) -> None: + code = ( + "def f(x):\n if x:\n return 1\n return\n" + "class C:\n def g(self):\n return self\n" + "g = lambda: 1\n" + ) + self.assertEqual(_module_code(code), code.rstrip("\n")) + + def test_only_the_cells_own_return_goes_and_comments_stay(self) -> None: + code = "def f():\n return 2 # keep\ny = f() # call\nreturn (y,) # export" + self.assertEqual( + _module_code(code), + "def f():\n return 2 # keep\ny = f() # call\n(y,) # export", + ) + + +class FunctionCellTest(unittest.TestCase): + def test_a_function_cell_is_one_def_or_class(self) -> None: + self.assertTrue(_is_function_cell("def f(x):\n return x")) + self.assertTrue(_is_function_cell("class C:\n pass")) + self.assertFalse(_is_function_cell("def f():\n pass\nf()")) + self.assertFalse(_is_function_cell("x = 1")) + self.assertFalse(_is_function_cell("def broken(")) + + def test_editing_one_is_refused_and_unchanged_code_is_not(self) -> None: + state = WorkspaceState(Path(tempfile.gettempdir())) + state.kernel = type("K", (), {"run_cells": lambda self, ids: {}, "wait_idle": lambda self: True})() # type: ignore[assignment] + state.cells = [{"id": "f1", "code": "def f():\n return 1"}] + state.schedule_settle = lambda: None # type: ignore[method-assign] + refused = state.dispatch_command({"type": "code.edit", "cell": "f1", "code": "def f():\n return 2"}) + self.assertEqual(refused["type"], "error") + self.assertIn("@app.function", refused["message"]) + accepted = state.dispatch_command({"type": "code.edit", "cell": "f1", "code": "def f():\n return 1"}) + self.assertEqual(accepted["type"], "ack") + + +@unittest.skipUnless(os.environ.get("SCA_EDITOR_E2E"), "set SCA_EDITOR_E2E=1 to boot a marimo kernel") +class EditorEndToEndTest(unittest.TestCase): + def test_slider_rebuilds_the_part_and_commit_writes_the_source(self) -> None: + import threading + + import uvicorn + + from simplecadapi.editor.server import create_app + from simplecadapi.runtime.params import read_saved_params + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + path = create_notebook(root, "bracket", "part") + state = WorkspaceState(root) + state.port = 17731 + app = create_app(state) + server = uvicorn.Server(uvicorn.Config(app, port=state.port, log_level="error")) + threading.Thread(target=server.run, daemon=True).start() + state.hub.attach_loop(__import__("asyncio").new_event_loop()) + try: + state.start_kernel(path) + self.assertIn("RADIUS", state.specs) + first = state.latest_preview_bytes + self.assertTrue(first) + state.dispatch_command({"type": "param.set", "values": {"RADIUS": 40}}) + deadline = time.monotonic() + 90 + while state.latest_preview_bytes == first and time.monotonic() < deadline: + time.sleep(0.5) + self.assertNotEqual(state.latest_preview_bytes, first) + self.assertEqual(read_saved_params(state.config), {"RADIUS": 40.0}) + state.dispatch_command({"type": "param.commit", "names": ["RADIUS"]}) + deadline = time.monotonic() + 60 + while "40.0" not in path.read_text() and time.monotonic() < deadline: + time.sleep(0.5) + self.assertIn('scad.var("radius", 40.0', path.read_text()) + self.assertEqual(read_saved_params(state.config), {}) + with urllib.request.urlopen(f"http://127.0.0.1:{state.port}/api/health") as response: + self.assertTrue(json.load(response)["kernel"]) + finally: + if state.kernel is not None: + state.kernel.stop() + server.should_exit = True + + +if __name__ == "__main__": + unittest.main() + + +# --------------------------------------------------------------------------- +# Selection as notebook code, the step titles, the agent snapshot +# --------------------------------------------------------------------------- + +from simplecadapi.editor.selection import ( + MARKER, + selection_cell_code, + selector_expression, + variable_names, +) +from simplecadapi.editor.server import cell_title + +FACE = {"centroid": [1.0, 2.0, 3.0], "area": 100.0} +EDGE = {"centroid": [0.0, 0.0, 5.0], "length": 12.5} + + +class SelectionCodeTest(unittest.TestCase): + def test_a_face_is_selected_by_type_place_and_size(self) -> None: + expression = selector_expression("face", FACE, {"type": "plane"}) + assert expression is not None + self.assertTrue(expression.startswith("scad.ql.faces().where(scad.ql.and_(")) + self.assertTrue(expression.endswith(".exactly(1)")) + for part in ('"geom.type", "==", "PLANE"', '"geom.center.x", ">=", 0.9', '"geom.area", "<=", 101'): + self.assertIn(part, expression) + + def test_a_solid_is_selected_by_volume_only(self) -> None: + expression = selector_expression("solid", {"centroid": [1, 2, 3], "volume": 50.0}, {"type": "brep_solid"}) + assert expression is not None + self.assertIn("geom.volume", expression) + self.assertNotIn("geom.center", expression) + + def test_nothing_to_select_by_gives_none(self) -> None: + self.assertIsNone(selector_expression("face", {}, {})) + self.assertIsNone(selector_expression("gadget", FACE, {})) + + def test_one_item_per_kind_is_named_by_its_kind(self) -> None: + code = selection_cell_code([{"kind": "face", "properties": FACE, "geometry": {"type": "plane"}}]) + self.assertTrue(code.startswith(MARKER)) + self.assertIn("selected_face = scad.ql.faces()", code) + self.assertTrue(code.endswith("selected_item = [selected_face]")) + compile(code, "", "exec") + + def test_several_items_are_numbered_per_kind_and_listed(self) -> None: + items = [ + {"kind": "edge", "properties": EDGE, "geometry": {"type": "line"}}, + {"kind": "face", "properties": FACE, "geometry": {"type": "plane"}}, + {"kind": "edge", "properties": EDGE, "geometry": {"type": "circle"}}, + ] + self.assertEqual(variable_names(items), ["selected_edge_1", "selected_face", "selected_edge_2"]) + code = selection_cell_code(items) + self.assertTrue(code.endswith("selected_item = [selected_edge_1, selected_face, selected_edge_2]")) + compile(code, "", "exec") + + def test_an_empty_selection_is_an_empty_list(self) -> None: + self.assertEqual(selection_cell_code([]), f"{MARKER}\nselected_item = []") + + def test_a_component_is_its_node_id(self) -> None: + code = selection_cell_code([{"kind": "component", "node_id": "node/asm/shell"}]) + self.assertIn("selected_component = 'node/asm/shell'", code) + + def test_the_generated_selectors_find_the_picked_entities(self) -> None: + import simplecadapi as scad + + body = scad.make_box_rsolid(10.0, 20.0, 30.0) + for kind, entity in ( + ("solid", {"properties": {"volume": body.get_volume()}, "geometry": {"type": "brep_solid"}}), + ("face", {"properties": {"centroid": [0.0, 0.0, 0.0], "area": 200.0}, "geometry": {"type": "plane"}}), + ("vertex", {"properties": {"position": [-5.0, -10.0, 0.0]}, "geometry": {"type": "point"}}), + ): + expression = selector_expression(kind, entity["properties"], entity["geometry"]) + assert expression is not None + found = eval(expression, {"scad": scad}).resolve(body) # noqa: S307 + self.assertEqual(len(found), 1, kind) + + +class CellTitleTest(unittest.TestCase): + def test_the_name_comes_from_the_most_specific_source(self) -> None: + self.assertEqual(cell_title("x = 1", "base-plate"), "base-plate") + self.assertEqual(cell_title(f"{MARKER}\nselected_item = []", None), "selection") + self.assertEqual(cell_title("# ---- params ----\nA = 1", None), "params") + self.assertEqual(cell_title("# ---- params: disc ----\nA = 1", None), "params: disc") + self.assertEqual(cell_title("_t = 1\nbody = _t", None), "body") + self.assertEqual(cell_title("def mount(x):\n return x", None), "mount") + self.assertEqual(cell_title("# only a comment", None), "only a comment") + self.assertEqual(cell_title("print(1)", None), "cell") + + +class SnapshotTest(unittest.TestCase): + def test_the_snapshot_is_written_and_read_back_by_the_cli(self) -> None: + from simplecadapi import cli as sca_cli + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + state = WorkspaceState(root) + state.cells = [{"id": "a", "code": "x = 1", "title": "x", "status": "idle", "errors": []}] + state.selection_items = [{"kind": "face", "entity_id": "e/1", "node_id": "n/1", "definition_id": "d"}] + state.write_snapshot() + data = json.loads((root / ".sca-editor" / "snapshot.json").read_text()) + self.assertEqual(data["selection"]["items"][0]["variable"], "selected_face") + self.assertEqual(data["cells"][0]["title"], "x") + report, code, _printer = sca_cli.run(["snapshot", str(root)]) + self.assertEqual((code, report["selection"]["items"][0]["entity_id"]), (0, "e/1")) + png = b"\x89PNG\r\n\x1a\nrest" + state.save_viewport(png) + self.assertEqual((root / ".sca-editor" / "viewport.png").read_bytes(), png) + self.assertTrue(state.snapshot()["viewport_png"].endswith("viewport.png")) + with self.assertRaises(ValueError): + sca_cli.run(["snapshot", str(root / "nowhere")]) + + +class RunAboveTest(unittest.TestCase): + def test_run_above_runs_the_cells_before_this_one_in_order(self) -> None: + ran: list[list[str]] = [] + state = WorkspaceState(Path(tempfile.gettempdir())) + state.kernel = type( # type: ignore[assignment] + "K", (), {"run_cells": lambda self, ids: ran.append(list(ids)) or {}, "wait_idle": lambda self: True} + )() + state.cells = [{"id": name, "code": ""} for name in ("a", "b", "c")] + state.schedule_settle = lambda: None # type: ignore[method-assign] + for _ in range(2): + state.dispatch_command({"type": "rerun.request", "cell": "c", "above": True}) + state.dispatch_command({"type": "rerun.request", "cell": "a", "above": True}) + deadline = time.monotonic() + 3 + while len(ran) < 2 and time.monotonic() < deadline: + time.sleep(0.05) + self.assertEqual(ran[:2], [["a", "b"], ["a", "b"]]) + self.assertEqual(len(ran), 2) # nothing is above the first cell + + +class CellLinesTest(unittest.TestCase): + NOTEBOOK = ( + "import marimo\n\napp = marimo.App()\n\nwith app.setup:\n import simplecadapi as scad\n\n\n" + "@app.cell\ndef _():\n a = 1\n b = 2\n return a, b\n\n\n" + "@app.cell\ndef _(a):\n c = a\n return (c,)\n" + ) + + def test_cells_are_located_in_the_file_by_order(self) -> None: + from simplecadapi.editor.server import file_line_ranges + + lines = self.NOTEBOOK.splitlines() + ranges = file_line_ranges(self.NOTEBOOK, ["a = 1\nb = 2", "c = a"]) + self.assertEqual(lines[ranges[0][0] - 1].strip(), "a = 1") + self.assertEqual(lines[ranges[0][1] - 1].strip(), "b = 2") + self.assertEqual(lines[ranges[1][0] - 1].strip(), "c = a") + + def test_an_unsaved_cell_has_no_location(self) -> None: + from simplecadapi.editor.server import file_line_ranges + + self.assertEqual(file_line_ranges(self.NOTEBOOK, ["a = 1\nb = 2", "c = 99"])[1], None) + self.assertEqual(file_line_ranges("def broken(", ["x"]), [None]) + + +class ModulesTest(unittest.TestCase): + def _workspace(self, root: Path) -> None: + (root / "dims.py").write_text( + "from dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass D:\n L: float = 80.0\n\nDIMS = D()\n" + ) + (root / "helper.py").write_text("def helper():\n return 1\n") + notebook = create_notebook(root, "part", "part") + notebook.write_text( + notebook.read_text().replace( + "with app.setup:\n import simplecadapi as scad\n", + "with app.setup:\n import simplecadapi as scad\n from dims import DIMS\n from helper import helper\n", + ) + ) + + def test_the_workspace_lists_the_modules_notebooks_import(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self._workspace(root) + state = WorkspaceState(root) + modules = {m["path"]: m for m in state.modules()} + self.assertEqual(sorted(modules), ["dims.py", "helper.py"]) + self.assertEqual(modules["dims.py"]["used_by"], ["part.py"]) + + def test_a_module_opens_with_its_dataclass_fields_as_parameters(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self._workspace(root) + state = WorkspaceState(root) + seen: list[dict] = [] + state.emit = seen.append # type: ignore[method-assign] + state.open_module("dims.py") + envelope = seen[-1] + self.assertEqual((envelope["type"], envelope["origin"], envelope["used_by"]), ("module", "open", ["part.py"])) + self.assertEqual([p["id"] for p in envelope["params"]], ["D.L"]) + with self.assertRaises(ValueError): + state.open_module("../outside.py") + + def test_a_parameter_edit_is_written_to_the_module_and_propagated(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self._workspace(root) + state = WorkspaceState(root) + state.emit = lambda envelope: None # type: ignore[method-assign] + changed: list[set[Path]] = [] + state._files_changed = lambda paths, announce=True: changed.append(paths) # type: ignore[method-assign] + state.open_module("dims.py") + state.set_module_params("dims.py", {"D.L": 120}) + self.assertIn("L: float = 120.0", (root / "dims.py").read_text()) + self.assertEqual(changed, [{(root / "dims.py").resolve()}]) + state.save_module("dims.py", "X = 1\n") + self.assertEqual((root / "dims.py").read_text(), "X = 1\n") + self.assertEqual(len(changed), 2) + + +class SelectionLatencyTest(unittest.TestCase): + """A pick shows up at once and the kernel only ever writes the newest selection.""" + + def test_the_cell_is_pushed_before_the_kernel_writes_and_writes_coalesce(self) -> None: + import threading + + from simplecadapi.editor.server import PENDING_SELECTION_ID + + gate = threading.Event() + written: list[str] = [] + + class Kernel: + def put_cell(self, code: str, *, cell_id: str | None, run: bool = True) -> str: + gate.wait(5) # the kernel is busy with something long + written.append(code) + return cell_id or "real" + + def wait_idle(self, timeout: float = 0) -> bool: + return True + + state = WorkspaceState(Path(tempfile.gettempdir())) + kernel = Kernel() + state.kernel = kernel # type: ignore[assignment] + sent: list[dict] = [] + state.emit = sent.append # type: ignore[method-assign] + state.push_cells = lambda: None # type: ignore[method-assign] + state.write_snapshot = lambda: None # type: ignore[method-assign] + face = {"kind": "face", "properties": FACE, "geometry": {"type": "plane"}} + worker = threading.Thread(target=state._set_selection, args=(kernel, [face])) + worker.start() + deadline = time.monotonic() + 3 + while not any(m["type"] == "notebook.cells" for m in sent) and time.monotonic() < deadline: + time.sleep(0.01) + cells = next(m for m in sent if m["type"] == "notebook.cells")["cells"] + self.assertEqual(cells[-1]["id"], PENDING_SELECTION_ID) # shown while the kernel is still busy + self.assertEqual(written, []) + # two more picks while the first write is stuck: only the newest is written afterwards + edge = {"kind": "edge", "properties": EDGE, "geometry": {"type": "line"}} + state._set_selection(kernel, [face, edge]) + state._set_selection(kernel, [edge]) + gate.set() + worker.join(5) + self.assertEqual(len(written), 2) # the first, then the newest + self.assertIn("selected_edge = ", written[-1]) + self.assertNotIn("selected_face", written[-1]) + + +@unittest.skipUnless(os.environ.get("SCA_EDITOR_E2E"), "set SCA_EDITOR_E2E=1 to boot a marimo kernel") +class KernelCacheTest(unittest.TestCase): + """The editor kernel shares the cell cache: a changed module recomputes only what reads it.""" + + NOTEBOOK = ( + "# /// script\n# [tool.simplecadapi]\n# id = \"two\"\n# ///\n" + "import marimo\n\napp = marimo.App()\n\nwith app.setup:\n import simplecadapi as scad\n\n\n" + "@app.cell\ndef _():\n from lib.alpha import A\n" + " open('/tmp/_sca_cache_log.txt', 'a').write('A')\n return (A,)\n\n\n" + "@app.cell\ndef _():\n from lib.beta import B\n" + " open('/tmp/_sca_cache_log.txt', 'a').write('B')\n return (B,)\n\n\n" + "@app.cell\ndef _(A, B):\n part = scad.Part(part_id='two', body=scad.make_box_rsolid(A, B, 1.0))\n" + " open('/tmp/_sca_cache_log.txt', 'a').write('P')\n return (part,)\n" + ) + + def test_only_the_cells_that_read_a_changed_module_run_again(self) -> None: + import threading + + import uvicorn + + from simplecadapi.editor.server import create_app + + log = Path("/tmp/_sca_cache_log.txt") + log.unlink(missing_ok=True) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + (root / "lib").mkdir() + (root / "lib" / "alpha.py").write_text("A = 10.0\n") + (root / "lib" / "beta.py").write_text("B = 6.0\n") + path = root / "two.py" + path.write_text(self.NOTEBOOK) + state = WorkspaceState(root) + state.port = 17733 + app = create_app(state) + server = uvicorn.Server(uvicorn.Config(app, port=state.port, log_level="error")) + threading.Thread(target=server.run, daemon=True).start() + state.hub.attach_loop(__import__("asyncio").new_event_loop()) + try: + state.start_kernel(path) + first = log.read_text() + self.assertEqual(sorted(first), ["A", "B", "P"]) # everything ran once + (root / "lib" / "alpha.py").write_text("A = 12.0\n") + kernel = state.kernel + assert kernel is not None + kernel.reload_modules(["lib", "lib.alpha", "lib.beta"]) + kernel.wait_idle(120) + again = log.read_text()[len(first):] + # alpha changed: the cell that reads it and the part rerun; beta's cell is restored. + self.assertEqual(sorted(again), ["A", "P"]) + # going back is restored for both + (root / "lib" / "alpha.py").write_text("A = 10.0\n") + before = log.read_text() + kernel.reload_modules(["lib", "lib.alpha", "lib.beta"]) + kernel.wait_idle(120) + self.assertEqual(log.read_text()[len(before):], "") + finally: + if state.kernel is not None: + state.kernel.stop() + server.should_exit = True + log.unlink(missing_ok=True) + + +class WatchedFilesTest(unittest.TestCase): + """What the editor watches: modules, child notebooks, and the data files a header lists.""" + + def test_input_files_of_the_notebook_and_its_children_are_watched(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + (root / "data").mkdir() + (root / "data" / "profile.dxf").write_text("0\nEOF\n") + (root / "data" / "child.csv").write_text("1,2\n") + header = '# /// script\n# [tool.simplecadapi]\n# id = "{id}"\n# inputs = ["{inputs}"]\n# ///\n' + body = "import marimo\n\napp = marimo.App()\n\nwith app.setup:\n import simplecadapi as scad\n" + (root / "child.py").write_text(header.format(id="child", inputs="data/child.csv") + body) + (root / "top.py").write_text( + header.format(id="top", inputs="data/profile.dxf") + + body + + '\n\n@app.cell\ndef _():\n c = scad.use("child.py")\n return (c,)\n' + ) + state = WorkspaceState(root, root / "top.py") + mtimes, _own, inputs = state._watched_files() + self.assertEqual(inputs, {root / "data" / "profile.dxf"}) + self.assertIn(root / "data" / "profile.dxf", mtimes) + self.assertIn(root / "data" / "child.csv", mtimes) # a child's data: its cells rerun + self.assertIn(root / "child.py", mtimes) + + +@unittest.skipUnless(os.environ.get("SCA_EDITOR_E2E"), "set SCA_EDITOR_E2E=1 to boot a marimo kernel") +class LongCellTest(unittest.TestCase): + """A cell that runs longer than the bridge's probes must not be interrupted by them.""" + + def test_a_long_cell_survives_the_bridge_polling_the_kernel(self) -> None: + import threading + + import uvicorn + + from simplecadapi.editor.server import create_app + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + path = root / "slow.py" + path.write_text( + "# /// script\n# [tool.simplecadapi]\n# id = \"slow\"\n# ///\n" + "import marimo\n\napp = marimo.App()\n\nwith app.setup:\n import simplecadapi as scad\n\n\n" + "@app.cell\ndef _():\n import time\n time.sleep(22)\n done = 1\n return (done,)\n" + ) + state = WorkspaceState(root) + state.port = 17735 + app = create_app(state) + server = uvicorn.Server(uvicorn.Config(app, port=state.port, log_level="error")) + threading.Thread(target=server.run, daemon=True).start() + state.hub.attach_loop(__import__("asyncio").new_event_loop()) + try: + # start_kernel waits for the notebook to go idle with scratch probes while the + # cell sleeps: they used to time out after 15 s and interrupt the cell. + state.start_kernel(path) + kernel = state.kernel + assert kernel is not None + self.assertTrue(kernel.session_exists()) + cell = next(c for c in kernel.list_cells() if "time.sleep" in c["code"]) + self.assertEqual(cell["status"], "idle") + self.assertEqual(cell["errors"], []) + self.assertIn("DONE 1", kernel.execute("print('DONE', done)").get("log", "").upper()) + finally: + if state.kernel is not None: + state.kernel.stop() + server.should_exit = True + + +class RestoreTest(unittest.TestCase): + """Switching back to a notebook shows its last model at once, if nothing it depends on changed.""" + + def _workspace(self, root: Path) -> Path: + (root / "dims.py").write_text("L = 10.0\n") + path = create_notebook(root, "part", "part") + path.write_text(path.read_text().replace("import simplecadapi as scad\n", "import simplecadapi as scad\n import dims\n", 1)) + return path + + def test_the_signature_follows_what_the_model_depends_on(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + path = self._workspace(root) + state = WorkspaceState(root, path) + first = state._signature(path) + self.assertEqual(first, state._signature(path)) + time.sleep(2.1) # file digests are remembered per (mtime, size) once settled + (root / "dims.py").write_text("L = 11.0\n") + changed = state._signature(path) + self.assertNotEqual(first, changed) # a module it imports + path.write_text(path.read_text() + "\n# edited\n") + self.assertNotEqual(changed, state._signature(path)) # the notebook itself + + def test_a_package_is_recalled_from_memory_and_from_disk_for_its_signature_only(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + path = self._workspace(root) + state = WorkspaceState(root, path) + state.remember_model(path, "sig-1", b"PKG") + self.assertEqual(state._recall_model(path, "sig-1"), b"PKG") + self.assertIsNone(state._recall_model(path, "sig-2")) + fresh = WorkspaceState(root, path) # a new server: only the disk remembers + self.assertEqual(fresh._recall_model(path, "sig-1"), b"PKG") + self.assertIsNone(fresh._recall_model(path, "sig-2")) + + def test_restoring_shows_the_cells_and_the_model_before_any_kernel(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + path = self._workspace(root) + state = WorkspaceState(root, path) + sent: list[dict] = [] + state.emit = sent.append # type: ignore[method-assign] + state.hub.broadcast = lambda message: None # type: ignore[method-assign] + self.assertFalse(state._restore(path)) # nothing published yet + state.remember_model(path, state._signature(path), b"PKG") + self.assertTrue(state._restore(path)) + package = next(m for m in sent if m["type"] == "preview") + self.assertEqual((package["cached"], package["size"]), (True, 3)) + self.assertEqual(state.latest_preview_bytes, b"PKG") + cells = next(m for m in sent if m["type"] == "notebook.cells")["cells"] + self.assertEqual([c["title"] for c in cells], ["params", "base", "part"]) + self.assertTrue(all(c["readonly"] for c in cells)) # not editable until a kernel has them + self.assertIn("RADIUS", state.specs) + + def test_static_cells_leave_out_the_setup_cell(self) -> None: + from simplecadapi.editor.server import _static_cells + + with tempfile.TemporaryDirectory() as tmp: + path = create_notebook(Path(tmp), "part", "part") + names = [cell["name"] for cell in _static_cells(path)] + self.assertNotIn("setup", names) + self.assertEqual(len(names), 3) + + +class SwitchOrderTest(unittest.TestCase): + """Opening one notebook after another: the last one asked for is the one that ends up open.""" + + def test_the_last_open_wins_however_the_threads_are_scheduled(self) -> None: + import simplecadapi.editor.server as server + + booted: list[str] = [] + + class FakeKernel: + marimo_url = "http://127.0.0.1:1" + alive = True + + def __init__(self, notebook, **_kw) -> None: + self.notebook = notebook + self._stop = __import__("threading").Event() + + def start(self) -> None: + time.sleep(0.2) + if self._stop.is_set(): + raise RuntimeError("stopped while booting") + booted.append(self.notebook.stem) + + def stop(self) -> None: + self._stop.set() + + def wait_instantiated(self, *_a, **_k) -> bool: + return True + + def wait_idle(self, *_a, **_k) -> bool: + return True + + original = server.MarimoKernel + server.MarimoKernel = FakeKernel # type: ignore[misc] + try: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + a = create_notebook(root, "alpha", "part") + b = create_notebook(root, "beta", "part") + state = WorkspaceState(root, a) + sent: list[dict] = [] + state.hub.broadcast = sent.append # type: ignore[method-assign] + state.settle = lambda: None # type: ignore[method-assign] + state._watch_files = lambda kernel: None # type: ignore[method-assign] + first = state.reserve_switch() + second = state.reserve_switch() + # the later request's thread runs first, the earlier one after it + state.open_notebook("beta.py", second) + state.open_notebook("alpha.py", first) + self.assertEqual(state.notebook, b) + self.assertEqual(booted, ["beta"]) + self.assertEqual([m for m in sent if m.get("error")], []) + finally: + server.MarimoKernel = original # type: ignore[misc] + + def test_an_open_superseded_while_booting_ends_quietly(self) -> None: + import threading + + import simplecadapi.editor.server as server + + class SlowKernel: + marimo_url = "http://127.0.0.1:1" + alive = True + + def __init__(self, notebook, **_kw) -> None: + self._stop = threading.Event() + + def start(self) -> None: + self._stop.wait(5) + raise RuntimeError("marimo exited with code -15") + + def stop(self) -> None: + self._stop.set() + + def wait_instantiated(self, *_a, **_k) -> bool: + return True + + def wait_idle(self, *_a, **_k) -> bool: + return True + + original = server.MarimoKernel + server.MarimoKernel = SlowKernel # type: ignore[misc] + try: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + create_notebook(root, "alpha", "part") + b = create_notebook(root, "beta", "part") + state = WorkspaceState(root, None) + state.hub.broadcast = lambda message: None # type: ignore[method-assign] + errors: list[BaseException] = [] + + def first() -> None: + try: + state.open_notebook("alpha.py") + except BaseException as exc: # noqa: BLE001 + errors.append(exc) + + thread = threading.Thread(target=first) + thread.start() + time.sleep(0.3) # alpha is booting + state.reserve_switch() + state._begin_switch(b, state._generation) # beta is asked for: alpha's kernel is stopped + thread.join(5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(state.notebook, b) + finally: + server.MarimoKernel = original # type: ignore[misc] + + +class InterruptTest(unittest.TestCase): + def test_the_stop_command_interrupts_the_kernel(self) -> None: + state = WorkspaceState(Path(tempfile.gettempdir())) + stopped: list[bool] = [] + state.kernel = type("K", (), {"interrupt": lambda self: stopped.append(True)})() # type: ignore[assignment] + self.assertEqual(state.dispatch_command({"type": "interrupt.request"})["type"], "ack") + deadline = time.monotonic() + 2 + while not stopped and time.monotonic() < deadline: + time.sleep(0.01) + self.assertEqual(stopped, [True]) + + def test_cell_states_from_marimo_reach_the_webuis(self) -> None: + from simplecadapi.editor.kernel import MarimoKernel + + seen: list[tuple[str, str]] = [] + kernel = MarimoKernel(Path(tempfile.gettempdir()) / "x.py", server_port=40000, on_cell_status=lambda i, s: seen.append((i, s))) + kernel._on_message(json.dumps({"op": "cell-op", "data": {"cell_id": "Hbol", "status": "running"}})) + kernel._on_message(json.dumps({"op": "cell-op", "data": {"cell_id": "Hbol", "status": None}})) + kernel._on_message("not json") + # the setup cell and the bridge's own scratch snippets are not cells of the notebook + kernel._on_message(json.dumps({"op": "cell-op", "data": {"cell_id": "setup", "status": "running"}})) + kernel._on_message(json.dumps({"op": "cell-op", "data": {"cell_id": "__scratch__", "status": "running"}})) + self.assertEqual(seen, [("Hbol", "running")]) + state = WorkspaceState(Path(tempfile.gettempdir())) + sent: list[dict] = [] + state.hub.broadcast = sent.append # type: ignore[method-assign] + state.cells = [{"id": "Hbol", "code": "", "status": "idle", "title": "base"}] + state.cell_status("Hbol", "queued") + self.assertEqual(state.cells[0]["status"], "queued") + self.assertEqual(sent, [{"type": "cell.status", "id": "Hbol", "status": "queued", "title": "base"}]) + + +@unittest.skipUnless(os.environ.get("SCA_EDITOR_E2E"), "set SCA_EDITOR_E2E=1 to boot a marimo kernel") +class InterruptEndToEndTest(unittest.TestCase): + def test_a_running_cell_is_stopped_and_the_kernel_stays_usable(self) -> None: + import threading + + import uvicorn + + from simplecadapi.editor.server import create_app + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + path = root / "slow.py" + path.write_text( + '# /// script\n# [tool.simplecadapi]\n# id = "slow"\n# ///\n' + "import marimo\n\napp = marimo.App()\n\nwith app.setup:\n import simplecadapi as scad\n\n\n" + "@app.cell\ndef _():\n value = 1\n return (value,)\n\n\n" + "@app.cell\ndef _(value):\n import time\n time.sleep(60)\n slow = value\n return (slow,)\n" + ) + state = WorkspaceState(root) + state.port = 17737 + app = create_app(state) + server = uvicorn.Server(uvicorn.Config(app, port=state.port, log_level="error")) + threading.Thread(target=server.run, daemon=True).start() + state.hub.attach_loop(__import__("asyncio").new_event_loop()) + statuses: list[tuple[str, str]] = [] + try: + # boot without running the 60 s cell: start the kernel directly and interrupt the run + from simplecadapi.editor.kernel import MarimoKernel + + kernel = MarimoKernel(path, server_port=state.port, on_cell_status=lambda i, s: statuses.append((i, s))) + state.kernel = kernel + state.notebook = path + threading.Thread(target=kernel.start, daemon=True).start() + deadline = time.monotonic() + 60 + while not any(s == "running" for _, s in statuses) and time.monotonic() < deadline: + time.sleep(0.2) + self.assertTrue(any(s == "running" for _, s in statuses), "the slow cell never started") + started = time.monotonic() + kernel.interrupt() + deadline = time.monotonic() + 20 + while not any(s == "idle" for _, s in statuses[-3:]) and time.monotonic() < deadline: + time.sleep(0.2) + self.assertLess(time.monotonic() - started, 15) # stopped, not 60 s later + self.assertIn("OK", kernel.execute("print('ok')").get("log", "").upper()) # still usable + finally: + kernel = state.kernel + if kernel is not None: + kernel.stop() + server.should_exit = True diff --git a/test/test_error_diagnostics_blend_ql.py b/test/test_error_diagnostics_blend_ql.py index eb63e8b1..d6a1efee 100644 --- a/test/test_error_diagnostics_blend_ql.py +++ b/test/test_error_diagnostics_blend_ql.py @@ -132,8 +132,7 @@ def test_fillet_bore_mouth_boundary_room(): def test_fillet_silent_garbage_output_caught_at_exit(): # Mechanism E: r=5 on all 12 edges of a 10-cube fails loudly, but r=6 - # makes the kernel *succeed* and hand back a solid with volume 1036.35 - # — more material than the 1000 it started with. Geometrically + # makes the kernel *succeed* and hand back a degenerate solid. Geometrically # impossible; the exit check must name it instead of returning it. box = _box() edges = _box_edges(box) @@ -141,17 +140,23 @@ def test_fillet_silent_garbage_output_caught_at_exit(): scad.fillet_rsolid(box, edges, 6) payload = _payload(ctx.value) + what = payload["what_happened"] # The volume violation leads what_happened so the agent can tell - # "kernel silently lied" apart from "kernel refused" (r=5 path). The - # garbage volume itself is platform-dependent OCC behavior (macOS - # reports ~1036, Linux ~1039) — the contract is only that the kernel - # handed back MORE material than the 1000 it was given. - garbage = re.search(r"result volume ([0-9.]+)", payload["what_happened"]) - assert garbage, payload["what_happened"] - assert float(garbage.group(1)) > 1000 - assert "exceeds the input volume 1000" in payload["what_happened"] + # "kernel silently lied" apart from "kernel refused" (r=5 path). How the + # garbage solid measures depends on the volume integrator: the fixed-order + # one reported phantom extra material (~1036 on macOS, ~1039 on Linux, more + # than the 1000 it was given); the knot-span (Gauss-Kronrod) one sees an + # empty solid (volume 0). Either way the exit check must name it. + garbage = re.search(r"result volume (?:is )?([0-9.]+)", what) + assert garbage, what + volume = float(garbage.group(1)) + if volume > 1000: + assert "exceeds the input volume 1000" in what + else: + assert volume == 0.0 + assert "empty/degenerate solid" in what # Probe explanation follows as the suspected root cause. - assert "probes indicate" in payload["what_happened"] + assert "probes indicate" in what assert "BlendVolumeViolation" in (payload["technical_details"] or "") diff --git a/test/test_example_cli.py b/test/test_example_cli.py new file mode 100644 index 00000000..1e5dd923 --- /dev/null +++ b/test/test_example_cli.py @@ -0,0 +1,46 @@ +"""``sca example``: a bundled example workspace copied out for the editor.""" +from pathlib import Path + +import pytest + +from simplecadapi.cli import main, run +from simplecadapi.runtime.workspace import scan_workspace + + +def test_without_a_name_the_examples_are_listed(): + report, code, printer = run(["example"]) + assert code == 0 and "u_link" in report["examples"] + printer(report) + + +def test_copy_makes_a_workspace_the_editor_can_open(tmp_path, capsys): + target = tmp_path / "demo" + report, code, printer = run(["example", "u_link", str(target)]) + assert code == 0 and Path(report["path"]) == target + assert report["edit"].startswith("sca edit ") and str(target) in report["edit"] + entries = {entry.path: entry for entry in scan_workspace(target)} + assert set(entries) == {"shell.py", "u_link.py", "u_link_motor_mount.py"} + assert entries["u_link_motor_mount.py"].kind == "assembly" + assert set(entries["u_link_motor_mount.py"].uses) == {"u_link.py", "shell.py"} + printer(report) + assert "open it:" in capsys.readouterr().out + + +def test_an_existing_directory_is_never_overwritten(tmp_path, capsys): + target = tmp_path / "mine" + target.mkdir() + (target / "notes.txt").write_text("keep me") + assert main(["example", "u_link", str(target)]) == 2 + assert "already exists" in capsys.readouterr().err + assert [path.name for path in target.iterdir()] == ["notes.txt"] + + +def test_the_default_directory_is_the_examples_own_name(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + report, _, _ = run(["example", "u_link"]) + assert Path(report["path"]) == tmp_path / "u_link_motor_mount" + + +def test_an_unknown_example_is_refused(): + with pytest.raises(SystemExit): + run(["example", "nope"]) diff --git a/test/test_limited_extrusion.py b/test/test_limited_extrusion.py new file mode 100644 index 00000000..4b1047cd --- /dev/null +++ b/test/test_limited_extrusion.py @@ -0,0 +1,95 @@ +"""Target-limited extrusions must terminate on actual target surfaces.""" +from __future__ import annotations + +import json +import pytest +from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut + +import simplecadapi as s +from simplecadapi.kernel.ocp_booleans import solids_of +from simplecadapi.kernel.ocp_properties import center_of_mass, volume +from simplecadapi.operators.limited_extrusion import extrude_until_rsolid +from simplecadapi.recording.graph import GraphSession + + +@pytest.mark.parametrize("until,height", [("NEXT", 5), ("LAST", 7)]) +def test_forward_stops_at_near_or_far_planar_surface(until, height): + face = s.make_rectangle_rface(2, 2) + target = s.make_box_rsolid(10, 10, 2, bottom_face_center=(0, 0, 5)) + output = extrude_until_rsolid(face, target, until=until) + assert output.get_volume() == pytest.approx(4 * height) + + +@pytest.mark.parametrize("until,height", [("PREVIOUS", 8), ("FIRST", 10)]) +def test_reverse_modes_invert_the_direction(until, height): + face = s.make_rectangle_rface(2, 2, center=(0, 0, 10)) + target = s.make_box_rsolid(10, 10, 2) + output = extrude_until_rsolid(face, target, until=until) + assert output.get_volume() == pytest.approx(4 * height) + + +def test_curved_termination_matches_an_independent_boolean_reference(): + face = s.make_rectangle_rface(2, 2) + sphere = s.make_sphere_rsolid(5, center=(0, 0, 10)) + output = extrude_until_rsolid(face, sphere) + long_prism = s.extrude_rsolid(face, direction=(0, 0, 1), distance=20) + cut = BRepAlgoAPI_Cut(long_prism.wrapped, sphere.wrapped) + cut.Build() + candidates = solids_of(cut.Shape()) + lower = min(candidates, key=lambda part: center_of_mass(part).z) + assert output.get_volume() == pytest.approx(volume(lower), rel=1e-8) + # A sphere's contact surface bows away from its bounding box near the axis. + assert output.get_volume() > 4 * 5 + .1 + + +def test_ordered_multi_body_targets_choose_first_and_last_surface(): + face = s.make_rectangle_rface(2, 2) + lower = s.make_box_rsolid(10, 10, 2, bottom_face_center=(0, 0, 5)) + upper = s.make_box_rsolid(10, 10, 2, bottom_face_center=(0, 0, 10)) + assert extrude_until_rsolid(face, [upper, lower], until="NEXT").get_volume() == pytest.approx(20) + assert extrude_until_rsolid(face, [upper, lower], until="LAST").get_volume() == pytest.approx(48) + + +def test_rejects_invalid_parameters_and_non_intersecting_targets(): + face = s.make_rectangle_rface(2, 2) + far = s.make_box_rsolid(2, 2, 2, bottom_face_center=(20, 0, 5)) + with pytest.raises(ValueError, match="intersect|contact"): + extrude_until_rsolid(face, far) + with pytest.raises(ValueError, match="nonzero"): + extrude_until_rsolid(face, far, direction=(0, 0, 0)) + with pytest.raises(ValueError, match="NEXT"): + extrude_until_rsolid(face, far, until="UNKNOWN") + with pytest.raises(ValueError, match="at least one"): + extrude_until_rsolid(face, []) + + +def test_operation_retains_profile_and_target_as_replay_inputs(): + with GraphSession() as session: + face = s.make_rectangle_rface(2, 2) + target = s.make_box_rsolid(10, 10, 2, bottom_face_center=(0, 0, 5)) + output = extrude_until_rsolid(face, target) + session.capture_result(value=output) + node = next(n for n in session.graph.nodes if n.op == "make_extrude_until_rsolid") + assert len(node.inputs) == 2 + assert {key: value for key, value in node.params.items() if key != "input_refs"} == { + "direction": (0, 0, 1), "until": "NEXT", "target_count": 1} + assert [ref["node_id"] for ref in node.params["input_refs"]] == [n.node_id for n in node.inputs] + assert [ref["output_slot"] for ref in node.params["input_refs"]] == [0, 0] + replayed = s.replay_model_json(s.export_model_json(session))[0] + assert replayed.get_volume() == pytest.approx(output.get_volume(), rel=1e-9) + assert replayed.get_volume() == pytest.approx(20) + + +@pytest.mark.parametrize("until,height", [("NEXT", 5), ("LAST", 7)]) +def test_matching_target_footprint_keeps_distinct_termination_faces(until, height): + profile = s.make_rectangle_rface(2, 2) + target = s.make_box_rsolid(2, 2, 2, bottom_face_center=(0, 0, 5)) + result = extrude_until_rsolid(profile, target, until=until) + assert result.get_volume() == pytest.approx(4 * height) + + +def test_partial_target_cannot_silently_leave_the_temporary_prism(): + profile = s.make_rectangle_rface(4, 4) + target = s.make_box_rsolid(2, 2, 2, bottom_face_center=(0, 0, 5)) + with pytest.raises(ValueError, match="cover|termination"): + extrude_until_rsolid(profile, target) diff --git a/test/test_notebook_params.py b/test/test_notebook_params.py new file mode 100644 index 00000000..42a2e973 --- /dev/null +++ b/test/test_notebook_params.py @@ -0,0 +1,278 @@ +"""Notebook parameters, features and workspace scanning (read from files).""" + +from __future__ import annotations + +import json +import tempfile +import textwrap +import unittest +from pathlib import Path + +import simplecadapi as scad +from simplecadapi.runtime.config import read_notebook_config +from simplecadapi.runtime.params import ( + apply_saved_params, + commit_value, + list_parameters, + parameters_in_cell, + read_saved_params, + write_saved_params, +) +from simplecadapi.runtime.workspace import ( + features_in_cell, + list_features, + scan_workspace, +) + +HEADER = '# /// script\n# [tool.simplecadapi]\n# id = "{id}"\n# ///\n' + + +def notebook(id: str, *cells: str) -> str: + body = "".join( + f"@app.cell\ndef _():\n{textwrap.indent(textwrap.dedent(c).strip(), ' ')}\n return\n\n\n" + for c in cells + ) + return HEADER.format(id=id) + "import marimo\n\napp = marimo.App()\n\n\n" + body + + +class ParameterCommentTest(unittest.TestCase): + def test_header_forms_and_inference(self) -> None: + for header in ("# ---- params ----", "# ---- params: disc ----"): + specs = parameters_in_cell(f"{header}\nA = 1.5\nN = 4\nF = True\nS = 'x'\n_t = 3\n") + self.assertEqual([s.name for s in specs], ["A", "N", "F"]) + self.assertEqual([s.kind for s in specs], ["float", "int", "bool"]) + self.assertEqual([s.control for s in specs], ["number", "number", "toggle"]) + self.assertEqual(parameters_in_cell("# ---- params: disc ----\nA = 1\n")[0].group, "disc") + self.assertEqual(parameters_in_cell("# ---- params ----\nA = 1\n")[0].group, "") + + def test_plain_constants_outside_a_params_cell_are_not_parameters(self) -> None: + self.assertEqual(parameters_in_cell("X = 3.0\n"), []) + + def test_ui_comment_makes_a_parameter_anywhere(self) -> None: + (spec,) = parameters_in_cell('W = 20.0 # ui: slider(10, 40, step=2, unit="mm", label="Width")\n') + self.assertEqual( + (spec.control, spec.min, spec.max, spec.step, spec.unit, spec.label), + ("slider", 10, 40, 2, "mm", "Width"), + ) + self.assertEqual(spec.issues, ()) + + def test_scad_var_carries_unit_and_comment(self) -> None: + (spec,) = parameters_in_cell( + '# ---- params ----\nOD = scad.var("od", 100.0, comment="outer", unit="mm") # ui: slider(50, 200)\n' + ) + self.assertEqual((spec.declared, spec.unit, spec.comment), ("var", "mm", "outer")) + self.assertEqual((spec.min, spec.max), (50, 200)) + + def test_bad_ui_comments_fall_back_and_say_why(self) -> None: + cases = { + "A = 1.0 # ui: slider(5, 1)": "min 5 is above max 1", + "A = 1.0 # ui: slider()": "slider needs min and max", + "A = 1.0 # ui: dial(1, 2)": "must be one of", + "A = 1 # ui: toggle()": "toggle needs a bool", + "A = True # ui: slider(1, 2)": "needs a number", + "A = 1.0 # ui: number(step=0)": "step must be positive", + "A = 1.0 # ui: number(speed=1)": "unknown ui option", + } + for code, message in cases.items(): + (spec,) = parameters_in_cell(code + "\n") + self.assertTrue(any(message in issue for issue in spec.issues), (code, spec.issues)) + self.assertIn(spec.control, ("number", "toggle"), code) + + def test_list_parameters_reads_the_file(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "m.py" + path.write_text( + notebook("m", "# ---- params ----\nW = 20.0 # ui: slider(10, 40)\nFLAG = False"), + encoding="utf-8", + ) + specs = list_parameters(path) + self.assertEqual([s.name for s in specs], ["W", "FLAG"]) + self.assertTrue(specs[0].cell.startswith("c")) + + +class CommitValueTest(unittest.TestCase): + CODE = ( + "# ---- params ----\n" + 'W = 20.0 # ui: slider(10, 40)\n' + "N = 4\n" + "F = False\n" + 'OD = scad.var("od", 100.0, comment="é", unit="mm") # 外径\n' + "T = scad.var(\n 't', 3.0)\n" + ) + + def test_literals_keep_the_rest_of_the_line(self) -> None: + out = commit_value(self.CODE, "W", 25) + assert out is not None + self.assertIn("W = 25.0 # ui: slider(10, 40)", out) + self.assertIn("N = 7", commit_value(self.CODE, "N", 7.0) or "") + self.assertIn("F = True", commit_value(self.CODE, "F", True) or "") + + def test_var_default_is_replaced_not_the_name(self) -> None: + out = commit_value(self.CODE, "OD", 120) + assert out is not None + self.assertIn('OD = scad.var("od", 120.0, comment="é", unit="mm") # 外径', out) + + def test_refuses_what_does_not_fit(self) -> None: + self.assertIsNone(commit_value(self.CODE, "MISSING", 1)) + self.assertIsNone(commit_value(self.CODE, "F", 1.0)) + self.assertIsNone(commit_value(self.CODE, "W", True)) + + def test_a_declaration_spanning_lines_is_edited_where_the_value_is(self) -> None: + self.assertIn("'t', 4.0)", commit_value(self.CODE, "T", 4.0) or "") + + +class FeatureHeaderTest(unittest.TestCase): + def test_header_parts(self) -> None: + (feature,) = features_in_cell( + "# ---- feature: base-plate (build, profile=sketch) ----\nx = 1\n", "c1" + ) + self.assertEqual( + (feature.slug, feature.role, feature.tags, feature.cell), + ("base-plate", "build", (("profile", "sketch"),), "c1"), + ) + (bare,) = features_in_cell("\n# ---- feature: boss ----\n") + self.assertEqual((bare.slug, bare.role, bare.line), ("boss", "", 2)) + + def test_list_features_in_cell_order(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "m.py" + path.write_text( + notebook("m", "# ---- feature: a (build) ----\na = 1", "# ---- feature: b (add) ----\nb = 2"), + encoding="utf-8", + ) + self.assertEqual([f.slug for f in list_features(path)], ["a", "b"]) + + +class SavedParamsTest(unittest.TestCase): + def setUp(self) -> None: + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + path = Path(self._tmp.name) / "m.py" + path.write_text(notebook("m", "# ---- params ----\nW = 1.0"), encoding="utf-8") + config = read_notebook_config(path) + assert config is not None + self.config = config + + def test_round_trip_and_clear(self) -> None: + self.assertEqual(read_saved_params(self.config), {}) + write_saved_params(self.config, {"W": 2.5, "N": 3, "F": True}) + self.assertEqual(read_saved_params(self.config), {"W": 2.5, "N": 3, "F": True}) + write_saved_params(self.config, {"W": 9.0}) + self.assertEqual(read_saved_params(self.config), {"W": 9.0}) + write_saved_params(self.config, {}) + self.assertEqual(read_saved_params(self.config), {}) + + def test_corrupt_file_is_ignored(self) -> None: + write_saved_params(self.config, {"W": 2.0}) + from simplecadapi.runtime.params import saved_params_file + + saved_params_file(self.config).write_text("{not json", encoding="utf-8") + self.assertEqual(read_saved_params(self.config), {}) + + def test_apply_keeps_type_and_var_declaration(self) -> None: + var = scad.var("od", 100.0, comment="outer", unit="mm") + glbls = {"OD": var, "N": 6, "F": False, "S": 1.5, "OTHER": 1.0} + write_saved_params(self.config, {"OD": 120, "N": 8.0, "F": True, "S": True, "OTHER": 9.0}) + applied = apply_saved_params(self.config, {"OD", "N", "F", "S"}, glbls) + self.assertEqual(sorted(applied), ["F", "N", "OD"]) # S: a bool for a number is refused + self.assertEqual(glbls["OD"].default, 120.0) + self.assertEqual((glbls["OD"].name, glbls["OD"].unit, glbls["OD"].comment), + ("od", var.unit, "outer")) + self.assertEqual((glbls["N"], type(glbls["N"])), (8, int)) + self.assertIs(glbls["F"], True) + self.assertEqual(glbls["S"], 1.5) + self.assertEqual(glbls["OTHER"], 1.0) # not defined by the cell + + +class ScratchCellTest(unittest.TestCase): + def test_scratch_cells_are_not_recorded(self) -> None: + from simplecadapi.runtime.cells import NotebookCells + + cells = NotebookCells(Path("m.py"), []) + self.assertNotIn("__scratch__", cells) + + +class WorkspaceScanTest(unittest.TestCase): + def test_scan_lists_only_simplecad_notebooks(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "parts").mkdir() + (root / "parts" / "bar.py").write_text(notebook("bar", "b = 1"), encoding="utf-8") + (root / "parts" / "dims.py").write_text("X = 1\n", encoding="utf-8") + (root / "product.py").write_text( + notebook("product", 'p = scad.use("parts/bar.py", id="a")\np2 = scad.use("parts/gone.py")'), + encoding="utf-8", + ) + (root / "__marimo__").mkdir() + (root / "__marimo__" / "x.py").write_text(notebook("x", "b = 1"), encoding="utf-8") + (root / "plain.py").write_text("import marimo\n", encoding="utf-8") + entries = {entry.path: entry for entry in scan_workspace(root)} + self.assertEqual(sorted(entries), ["parts/bar.py", "product.py"]) + self.assertEqual(entries["parts/bar.py"].kind, "part") + product = entries["product.py"] + self.assertEqual(product.kind, "assembly") + self.assertEqual(product.uses, ("parts/bar.py", "parts/gone.py")) + json.dumps(product.to_dict()) + + def test_examples_are_a_workspace_without_changes(self) -> None: + examples = Path(__file__).resolve().parents[1] / "examples" + entries = {entry.path: entry for entry in scan_workspace(examples)} + link = entries["four_bar_linkage/link_bar.py"] + self.assertEqual((link.id, link.kind), ("link_bar", "part")) + assembly = entries["four_bar_linkage/four_bar_linkage.py"] + self.assertEqual(assembly.kind, "assembly") + self.assertIn("four_bar_linkage/link_bar.py", assembly.uses) + names = [spec.name for spec in list_parameters(examples / "flange_plate" / "flange_plate.py")] + self.assertIn("FLANGE_OD", names) + + +if __name__ == "__main__": + unittest.main() + + +class ModuleParametersTest(unittest.TestCase): + CODE = ( + "from dataclasses import dataclass\n\n" + 'TAG = "x"\n' + "WALL = 2.0 # ui: slider(1, 5, step=0.5, unit=\"mm\")\n" + "TABLE = 3\n\n\n" + "@dataclass(frozen=True)\n" + "class Dims:\n" + " L: float = 80.0 # arm span\n" + " N: int = 4 # ui: slider(2, 8)\n" + " name: str = 'a'\n\n" + " @property\n" + " def r(self) -> float:\n" + " return self.L / 2\n\n\n" + "class Plain:\n" + " K: float = 1.0\n" + ) + + def test_dataclass_fields_and_commented_constants_are_parameters(self) -> None: + from simplecadapi.runtime.params import parameters_in_module + + specs = {spec.name: spec for spec in parameters_in_module(self.CODE)} + self.assertEqual(sorted(specs), ["Dims.L", "Dims.N", "WALL"]) + self.assertEqual((specs["WALL"].control, specs["WALL"].unit, specs["WALL"].group), ("slider", "mm", "module")) + self.assertEqual((specs["Dims.N"].kind, specs["Dims.N"].control), ("int", "slider")) + self.assertEqual(specs["Dims.L"].to_dict()["label"], "L") + + def test_values_are_written_where_they_are_declared(self) -> None: + from simplecadapi.runtime.params import commit_field_value + + out = commit_field_value(self.CODE, "Dims", "L", 100) + assert out is not None + self.assertIn("L: float = 100.0 # arm span", out) + self.assertIsNone(commit_field_value(self.CODE, "Plain", "K", 2)) # not a dataclass + self.assertIsNone(commit_field_value(self.CODE, "Dims", "name", 2)) + self.assertIn("WALL = 3.5 # ui:", commit_value(self.CODE, "WALL", 3.5) or "") + + +class VertexScopeTest(unittest.TestCase): + def test_vertices_resolve_through_a_solid_and_a_face(self) -> None: + import simplecadapi as scad + + solid = scad.make_box_rsolid(10.0, 20.0, 30.0) + self.assertEqual(len(scad.ql.vertices().resolve(solid)), 8) + face = scad.ql.faces().where(scad.ql.prop("geom.center.z", "<=", 0.01)).exactly(1).resolve(solid)[0] + self.assertEqual(len(scad.ql.vertices().resolve(face)), 4) diff --git a/test/test_notebook_runtime.py b/test/test_notebook_runtime.py index 16d1bd2d..563b172f 100644 --- a/test/test_notebook_runtime.py +++ b/test/test_notebook_runtime.py @@ -348,11 +348,15 @@ def test_a_changed_local_module_invalidates_the_cache(self) -> None: first = run_notebook(self.bracket) self.write("lib/dims.py", "HOLE_RADIUS = 2.0\n") changed = run_notebook(self.bracket) - self.assertEqual(self.statuses(changed), ["ran"] * 4) + # `width` reads no module: it is restored; the cell that imports the + # module, and the part made from its result, are computed again. + self.assertEqual(self.statuses(changed), ["ran", "cached", "ran", "ran"]) self.assertNotEqual(first.definition.content_hash, changed.definition.content_hash) self.assertEqual(self.statuses(run_notebook(self.bracket))[1:], ["cached"] * 3) - # Only the cache for the current dependencies is kept. - self.assertEqual(len(list((state_dir(first.config) / "cache").iterdir())), 1) + # The caches of the current dependencies and of the previous run are kept. + self.assertEqual( + len([d for d in (state_dir(first.config) / "cache").iterdir() if d.is_dir()]), 2 + ) def test_sources_are_positions_in_the_notebook_file(self) -> None: lines = BRACKET.splitlines() @@ -560,10 +564,83 @@ def test_a_changed_child_invalidates_the_parent(self) -> None: first = run_notebook(self.rig) self.write("bracket.py", BRACKET.replace("10.0, 5.0)", "12.0, 5.0)")) changed = run_notebook(self.rig) - self.assertEqual(self.statuses(changed), ["ran"] * 4) + # The pin does not depend on the child: it is restored. + self.assertEqual(self.statuses(changed), ["ran", "ran", "cached", "ran"]) self.assertNotEqual(first.definition.content_hash, changed.definition.content_hash) +TWO_MODULES = notebook_text( + cell("from lib.alpha import A", returns="A"), + cell("from lib.beta import B", returns="B"), + cell("box = scad.make_box_rsolid(A, 4.0, 4.0)", reads="A", returns="box"), + cell("plate = scad.make_box_rsolid(B, B, 1.0)", reads="B", returns="plate"), + cell("part = scad.Part(part_id='two', body=scad.union_rsolid(box, plate))", reads="box, plate", returns="part"), + table='id = "two"', +) + + +class TestCellLevelDependencies(NotebookTestCase): + """A changed file recomputes the cells that read it, and nothing stale survives.""" + + def setUp(self) -> None: + super().setUp() + self.write("lib/alpha.py", "A = 10.0\n") + self.write("lib/beta.py", "B = 6.0\n") + self.two = self.write("two.py", TWO_MODULES) + + def test_only_the_cells_that_read_a_module_and_their_dependents_rerun(self) -> None: + first = run_notebook(self.two) + self.assertEqual(self.statuses(first), ["ran"] * 6) + self.write("lib/alpha.py", "A = 12.0\n") + changed = run_notebook(self.two) + # setup, A (reads alpha), B (beta: untouched), box (made from A), + # plate (made from B: untouched), part (reads both products). + self.assertEqual(self.statuses(changed), ["ran", "ran", "cached", "ran", "cached", "ran"]) + + def test_the_result_is_what_a_fresh_run_gives_after_any_change(self) -> None: + run_notebook(self.two) + for name, text in ( + ("lib/alpha.py", "A = 12.0\n"), + ("lib/beta.py", "B = 8.0\n"), + ("lib/alpha.py", "A = 10.0\n"), # back to an earlier state + ("lib/beta.py", "B = 6.0\n"), + ): + self.write(name, text) + cached = run_notebook(self.two) + fresh = run_notebook(self.two, cache=False) + self.assertEqual(cached.definition.content_hash, fresh.definition.content_hash, name) + + def test_going_back_to_an_earlier_value_is_restored(self) -> None: + run_notebook(self.two) + self.write("lib/alpha.py", "A = 12.0\n") + run_notebook(self.two) + self.write("lib/alpha.py", "A = 10.0\n") + back = run_notebook(self.two) + self.assertEqual(self.statuses(back), ["ran", "cached", "cached", "cached", "cached", "cached"]) + + def test_a_function_defined_in_the_notebook_carries_the_dependency(self) -> None: + self.write("lib/size.py", "SIZE = 3.0\n") + text = notebook_text( + cell("side = 5.0", returns="side"), + cell("block = scad.make_box_rsolid(edge(), side, side)", reads="side", returns="block"), + cell("part = scad.Part(part_id='f', body=block)", reads="block", returns="part"), + table='id = "f"', + ).replace( + "with app.setup:\n import simplecadapi as scad\n", + "with app.setup:\n import simplecadapi as scad\n from lib.size import SIZE\n\n\n" + " def edge():\n return SIZE\n", + ) + path = self.write("f.py", text) + run_notebook(path) + self.write("lib/size.py", "SIZE = 4.0\n") + changed = run_notebook(path) + # `side` reads nothing; the block calls edge(), which reads the imported SIZE. + self.assertEqual(self.statuses(changed), ["ran", "cached", "ran", "ran"]) + self.assertEqual( + changed.definition.content_hash, run_notebook(path, cache=False).definition.content_hash + ) + + LINK = notebook_text( cell("length = 40.0", returns="length"), cell("bar = scad.make_box_rsolid(length, 6.0, 3.0)", reads="length", returns="bar"), diff --git a/test/test_ql.py b/test/test_ql.py index 56328ce6..2d6c9955 100644 --- a/test/test_ql.py +++ b/test/test_ql.py @@ -6,6 +6,14 @@ class TestQLTagPredicates(unittest.TestCase): + def test_solid_world_bounds_distinguish_equal_volume_components(self): + first = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(3, 0, 4)) + second = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(8, 0, 4)) + selector = Q.solids().where(Q.and_( + Q.prop("geom.bbox.min.x", "==", 7), + Q.prop("geom.bbox.max.z", "==", 6))).exactly(1) + self.assertEqual(selector.resolve([first, second]), [second]) + def test_tag_exact_and_wildcard(self): box = scad.make_box_rsolid(1.0, 1.0, 1.0) scad.apply_tag(box, "role.mounting_surface") diff --git a/test/test_rational_volume_precision.py b/test/test_rational_volume_precision.py new file mode 100644 index 00000000..86bc7634 --- /dev/null +++ b/test/test_rational_volume_precision.py @@ -0,0 +1,17 @@ +"""Rational NURBS mass properties must match analytic geometry.""" +import math + +import pytest +import simplecadapi as s + + +def test_nurbs_circle_extrusion_uses_span_aware_volume_integration(): + sketch = s.make_sketch_rsketch(name="rational-circle", plane="XY") + sketch = s.add_point_rsketch(sketch=sketch, point_id="seam", x=1, y=0) + poles = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), (1, 0)] + sketch = s.add_bspline_rsketch(sketch=sketch, entity_id="circle", start="seam", end="seam", + control_points=poles, degree=2, knots=[0, .25, .5, .75, 1], + multiplicities=[3, 2, 2, 2, 3], weights=[1, math.sqrt(.5), 1, math.sqrt(.5), 1, math.sqrt(.5), 1, math.sqrt(.5), 1]) + profile = s.make_face_from_sketch_rface(sketch=sketch) + body = s.extrude_rsolid(profile=profile, direction=(0, 0, 1), distance=2) + assert body.get_volume() == pytest.approx(2 * math.pi, rel=1e-9) diff --git a/test/test_scene_preview.py b/test/test_scene_preview.py new file mode 100644 index 00000000..4bb00dda --- /dev/null +++ b/test/test_scene_preview.py @@ -0,0 +1,207 @@ +"""The preview: a product's geometry straight from its solids, for a viewer under edit.""" + +from __future__ import annotations + +import json +import math +import struct +import unittest + +import numpy as np + +import simplecadapi as scad +from simplecadapi.scene import preview as pv + + +def box_part(part_id: str, size=(10.0, 20.0, 30.0)) -> scad.Part: + return scad.Part(part_id=part_id, body=scad.make_box_rsolid(*size)) + + +class PreviewGeometryTest(unittest.TestCase): + def setUp(self) -> None: + pv.clear_preview_cache() + + def test_a_part_previews_with_groups_that_tile_the_triangles(self) -> None: + scene = pv.compile_preview(box_part("box")) + (part,) = scene.parts.values() + self.assertEqual(part.triangle_count, 12) # two per face + self.assertEqual(len(part.face_groups), 6) + self.assertEqual(sum(count for _, _, count in part.face_groups), len(part.indices)) + self.assertEqual(len(part.edge_groups), 12) + self.assertEqual(len(part.vertex_ids), 8) + self.assertEqual([n.node_id for n in scene.nodes], ["node/box"]) + positions = part.positions.astype(np.float64) * 1000.0 # glTF metres -> mm (x, z, -y) + self.assertAlmostEqual(float(positions[:, 0].max() - positions[:, 0].min()), 10.0, places=3) + self.assertAlmostEqual(float(positions[:, 2].max() - positions[:, 2].min()), 20.0, places=3) # glTF z is -y + self.assertAlmostEqual(float(positions[:, 1].max() - positions[:, 1].min()), 30.0, places=3) # glTF y is z + + def test_triangle_areas_add_up_to_the_surface(self) -> None: + part = box_part("box") + (preview,) = pv.compile_preview(part).parts.values() + total = 0.0 + for _, first, count in preview.face_groups: + tri = preview.indices[first : first + count].reshape(-1, 3) + a, b, c = (preview.positions[tri[:, k]].astype(np.float64) * 1000.0 for k in range(3)) + total += 0.5 * float(np.linalg.norm(np.cross(b - a, c - a), axis=1).sum()) + self.assertAlmostEqual(total, 2 * (10 * 20 + 10 * 30 + 20 * 30), places=2) + + def test_the_same_content_is_remembered_even_when_the_object_is_rebuilt(self) -> None: + first = pv.compile_preview(box_part("a")) + second = pv.compile_preview(box_part("b")) # another object, the same box + self.assertIs(next(iter(first.parts.values())), next(iter(second.parts.values()))) + other = pv.compile_preview(box_part("c", (11.0, 20.0, 30.0))) + self.assertIsNot(next(iter(first.parts.values())), next(iter(other.parts.values()))) + + def test_a_tolerance_is_part_of_the_key(self) -> None: + part = scad.Part(part_id="cyl", body=scad.make_cylinder_rsolid(5.0, 10.0)) + fine = next(iter(pv.compile_preview(part, linear_tolerance=0.01, angular_tolerance=0.05).parts.values())) + coarse = next(iter(pv.compile_preview(part, linear_tolerance=0.5, angular_tolerance=0.5).parts.values())) + self.assertGreater(fine.triangle_count, coarse.triangle_count) + + +class PreviewAssemblyTest(unittest.TestCase): + def setUp(self) -> None: + pv.clear_preview_cache() + + def nested(self) -> scad.Assembly: + leaf = scad.make_assembly_rassembly(assembly_id="leaf") + leaf = scad.add_component_rassembly( + assembly=leaf, item=box_part("pin", (2.0, 2.0, 8.0)), component_id="pin", + placement=scad.make_placement_rplacement(origin=(5.0, 0.0, 0.0)), + ) + top = scad.make_assembly_rassembly(assembly_id="top") + top = scad.add_component_rassembly( + assembly=top, item=box_part("base"), component_id="base", + placement=scad.identity_placement_rplacement(), + ) + # rotated 90 degrees about z, then moved: x -> y + return scad.add_component_rassembly( + assembly=top, item=leaf, component_id="arm", + placement=scad.make_placement_rplacement(origin=(100.0, 0.0, 0.0), x_axis=(0.0, 1.0, 0.0), y_axis=(-1.0, 0.0, 0.0)), + ) + + def test_nested_placements_are_composed_and_parts_are_not_repeated(self) -> None: + asm = self.nested() + asm = scad.add_component_rassembly( + assembly=asm, item=box_part("pin", (2.0, 2.0, 8.0)), component_id="spare", + placement=scad.make_placement_rplacement(origin=(0.0, 50.0, 0.0)), + ) + scene = pv.compile_preview(asm) + by_id = {n.node_id: n for n in scene.nodes} + self.assertEqual(sorted(by_id), ["node/top/arm/pin", "node/top/base", "node/top/spare"]) + pin = by_id["node/top/arm/pin"].placement + # the pin sits at x=5 in the arm; the arm is turned x -> y and moved to x=100 + self.assertTrue(np.allclose(pin.origin, (100.0, 5.0, 0.0))) + self.assertTrue(np.allclose(pin.x_axis, (0.0, 1.0, 0.0))) + self.assertEqual(len(scene.parts), 2) # "pin" is one geometry, two instances + self.assertEqual(by_id["node/top/arm/pin"].part_key, by_id["node/top/spare"].part_key) + + def test_an_empty_assembly_cannot_be_previewed(self) -> None: + with self.assertRaises(pv.PreviewError): + pv.compile_preview(scad.make_assembly_rassembly(assembly_id="none")) + + +class PreviewWireTest(unittest.TestCase): + def test_the_encoding_round_trips_through_its_header(self) -> None: + scene = pv.compile_preview(box_part("box")) + data = pv.encode_preview(scene) + self.assertTrue(data.startswith(pv.MAGIC)) + header = pv.decode_preview_header(data) + self.assertEqual(header["schema"], pv.PREVIEW_SCHEMA) + (part,) = header["parts"] + (length,) = struct.unpack_from(" None: + pv.clear_preview_cache() + self.scene = pv.compile_preview(box_part("box")) + (self.part,) = self.scene.parts.values() + + def test_a_face_reports_what_the_package_sidecar_would(self) -> None: + topo, _, _ = self.part.face_groups[0] + info = pv.entity_info(self.scene, f"entity/box/face/{topo}") + assert info is not None + self.assertEqual(info["kind"], "face") + self.assertEqual(info["geometry"]["type"], "plane") + self.assertIn(round(info["properties"]["area"], 6), (200.0, 300.0, 600.0)) + self.assertEqual(len(info["properties"]["centroid"]), 3) + + def test_an_edge_a_vertex_and_the_solid(self) -> None: + edge = pv.entity_info(self.scene, f"entity/box/edge/{self.part.edge_groups[0][0]}") + assert edge is not None + self.assertEqual(edge["geometry"]["type"], "line") + self.assertIn(round(edge["properties"]["length"], 6), (10.0, 20.0, 30.0)) + vertex = pv.entity_info(self.scene, f"entity/box/vertex/{self.part.vertex_ids[0]}") + assert vertex is not None + self.assertEqual(vertex["geometry"]["type"], "point") + solid = pv.entity_info(self.scene, f"entity/box/solid/{self.part.solid_topo_id}") + assert solid is not None + self.assertTrue(math.isclose(solid["properties"]["volume"], 6000.0, rel_tol=1e-9)) + + def test_unknown_entities_have_no_info(self) -> None: + self.assertIsNone(pv.entity_info(self.scene, "entity/box/face/face_999")) + self.assertIsNone(pv.entity_info(self.scene, "entity/other/face/face_0")) + self.assertIsNone(pv.entity_info(self.scene, "entity/box/gadget/x")) + + +if __name__ == "__main__": + unittest.main() + + +class AgainstThePackageTest(unittest.TestCase): + """The preview names and describes entities exactly as the package's sidecar does.""" + + def test_a_real_part_has_the_packages_entity_ids_and_properties(self) -> None: + import io + import zipfile + from pathlib import Path + + from simplecadapi.product.packages import build_product_package, encode_product_package + from simplecadapi.runtime import run_notebook + + run = run_notebook(Path(__file__).resolve().parents[1] / "examples" / "flange_plate" / "flange_plate.py") + scene = pv.compile_preview(run.product) + archive = zipfile.ZipFile(io.BytesIO(encode_product_package(build_product_package(run.definition, validate=False)))) + sidecar = None + for name in archive.namelist(): + blob = archive.read(name) + if blob[:1] == b"{" and b'"face_groups"' in blob: + sidecar = json.loads(blob) + assert sidecar is not None + reference = {e["entity_id"]: e for e in sidecar["entities"]} + header = pv.decode_preview_header(pv.encode_preview(scene)) + (part,) = header["parts"] + mine = ( + {g["entity_id"] for g in part["face_groups"]} + | {g["entity_id"] for g in part["edge_groups"]} + | set(part["vertex_ids"]) + | {part["solid_entity_id"]} + ) + self.assertEqual(mine, set(reference)) + + def close(x, y) -> bool: + if isinstance(x, (int, float)) and isinstance(y, (int, float)): + return math.isclose(x, y, rel_tol=1e-6, abs_tol=1e-6) + if isinstance(x, dict) and isinstance(y, dict): + return x.keys() == y.keys() and all(close(x[k], y[k]) for k in x) + if isinstance(x, list) and isinstance(y, list): + return len(x) == len(y) and all(close(p, q) for p, q in zip(x, y)) + return x == y + + for entity_id in sorted(mine)[::5]: + info = pv.entity_info(scene, entity_id) + assert info is not None, entity_id + for field in ("properties", "geometry", "tags"): + self.assertTrue(close(info[field], reference[entity_id][field]), (entity_id, field)) diff --git a/test/test_section_features.py b/test/test_section_features.py new file mode 100644 index 00000000..1ce5caa3 --- /dev/null +++ b/test/test_section_features.py @@ -0,0 +1,105 @@ +"""Native section features: strict geometry and graph replay.""" +import math +import pytest +import simplecadapi as s + + +def _rectangle(z, width=10, height=6): + sketch = s.make_sketch_rsketch(name=f"section{z}", plane={"origin": (0, 0, z), "x_axis": (1, 0, 0), "y_axis": (0, 1, 0)}) + for name, point in zip("abcd", [(0, 0), (width, 0), (width, height), (0, height)]): + sketch = s.add_point_rsketch(sketch=sketch, point_id=name, x=point[0], y=point[1]) + for name, first, second in [("ab", "a", "b"), ("bc", "b", "c"), ("cd", "c", "d"), ("da", "d", "a")]: + sketch = s.add_line_rsketch(sketch=sketch, entity_id=name, start=first, end=second) + return s.make_face_from_sketch_rface(sketch=sketch) + + +def test_tapered_rectangle_and_graph_replay(): + with s.GraphSession(graph_id="taper") as session: + profile = _rectangle(0) + body = s.extrude_taper_rsolid(profile=profile, vector=(0, 0, 4), taper=10) + session.capture_result(value=body) + slope = math.tan(math.radians(10)) + expected = 60 * 4 - (10 + 6) * slope * 4**2 + 4 * slope**2 * 4**3 / 3 + assert body.get_volume() == pytest.approx(expected, rel=1e-8) + replayed = s.replay_model_json(s.export_model_json(session=session), strict=True)[0] + assert replayed.get_volume() == pytest.approx(expected, rel=1e-8) + + +def test_multisection_straight_sweep_and_replay(): + with s.GraphSession(graph_id="multi") as session: + sections = [_rectangle(0), _rectangle(3), _rectangle(6)] + path = s.make_segment_rwire(start=(5, 3, 0), end=(5, 3, 6)) + body = s.sweep_multisection_rsolid(profiles=sections, path=path) + session.capture_result(value=body) + assert body.get_volume() == pytest.approx(360, rel=1e-6) + replayed = s.replay_model_json(s.export_model_json(session=session), strict=True)[0] + assert replayed.get_volume() == pytest.approx(360, rel=1e-6) + + +def test_multisection_needs_two_sections(): + with pytest.raises(ValueError): + s.sweep_multisection_rsolid(profiles=[_rectangle(0)], path=s.make_segment_rwire(start=(0, 0, 0), end=(0, 0, 3))) + + +def test_invalid_taper_is_rejected(): + with pytest.raises(ValueError): + s.extrude_taper_rsolid(profile=_rectangle(0), vector=(0, 0, 4), taper=math.nan) + + +@pytest.mark.parametrize("taper,vector", [(-10, (0, 0, 4)), (10, (0, 0, -4))]) +def test_taper_fallback_keeps_exact_section_area(taper, vector): + body = s.extrude_taper_rsolid(profile=_rectangle(0), vector=vector, taper=taper) + slope = math.tan(math.radians(taper)) + expected = 60 * 4 - 16 * slope * 16 + 4 * slope**2 * 64 / 3 + assert body.get_volume() == pytest.approx(expected, rel=1e-7) + + +def test_taper_with_hole_tapers_inner_loop_oppositely(): + sketch = s.make_sketch_rsketch(name="annulus", plane="XY") + sketch = s.add_point_rsketch(sketch=sketch, point_id="center", x=0, y=0) + sketch = s.add_circle_rsketch(sketch=sketch, entity_id="outer", center="center", radius=10) + sketch = s.add_circle_rsketch(sketch=sketch, entity_id="inner", center="center", radius=2) + face = s.make_face_from_sketch_rface(sketch=sketch, profile="outer", inner_profiles=["inner"]) + body = s.extrude_taper_rsolid(profile=face, vector=(0, 0, 3), taper=5) + slope = math.tan(math.radians(5)) + expected = math.pi * ((100 - 4) * 3 - (10 + 2) * slope * 9) + assert body.get_volume() == pytest.approx(expected, rel=1e-6) + + +def test_taper_ql_face_is_recorded_as_feature_input(): + with s.GraphSession(graph_id="selected-taper") as session: + base = s.make_box_rsolid(10, 6, 2) + profile = s.ql.faces().where(s.ql.prop("geom.normal.z", ">", .9)).exactly(1).resolve(base)[0] + result = s.extrude_taper_rsolid(profile=profile, vector=(0, 0, 4), taper=10) + session.capture_result(value=result) + replayed = s.replay_model_json(s.export_model_json(session=session), strict=True)[0] + assert replayed.get_volume() == pytest.approx(result.get_volume(), rel=1e-8) + from simplecadapi.kernel.ocp_properties import center_of_mass + assert tuple(center_of_mass(replayed.wrapped)) == pytest.approx(tuple(center_of_mass(result.wrapped))) + + +def test_multisection_uses_ordered_slots_from_one_boundary_node(): + with s.GraphSession(graph_id="boundary-sweep") as session: + first = s.make_circle_rwire(center=(0, 0, 0), radius=2) + last = s.make_circle_rwire(center=(0, 0, 6), radius=3) + shell = s.loft_rshell(sections=[first, last]) + boundaries = s.free_boundaries_rwirelist(shell) + assert len(boundaries) == 2 + path = s.make_segment_rwire(start=(0, 0, 0), end=(0, 0, 6)) + body = s.sweep_multisection_rsolid(profiles=list(reversed(boundaries)), path=path) + session.capture_result(value=body) + replayed = s.replay_model_json(s.export_model_json(session=session), strict=True)[0] + assert replayed.get_volume() == pytest.approx(body.get_volume(), rel=1e-7) + + +def test_taper_ql_face_from_second_boolean_output_replays_in_place(): + with s.GraphSession(graph_id="second-profile") as session: + base = s.make_box_rsolid(10, 2, 2) + tool = s.make_box_rsolid(2, 4, 4, bottom_face_center=(0, 0, -1)) + pieces = s.boolean_rsolidlist(left=[base], right=[tool], operation="cut") + profile = s.ql.faces().where(s.ql.prop("geom.normal.z", ">", .9)).exactly(1).resolve(pieces[1])[0] + body = s.extrude_taper_rsolid(profile=profile, vector=(0, 0, 1), taper=5) + session.capture_result(value=body) + replayed = s.replay_model_json(s.export_model_json(session=session), strict=True)[0] + from simplecadapi.kernel.ocp_properties import center_of_mass + assert tuple(center_of_mass(replayed.wrapped)) == pytest.approx(tuple(center_of_mass(body.wrapped))) diff --git a/test/test_serialization.py b/test/test_serialization.py index 880d4269..ccb5be6a 100644 --- a/test/test_serialization.py +++ b/test/test_serialization.py @@ -357,6 +357,14 @@ def test_canonical_core_op_set_is_exact_contract(self): "make_twisted_sweep_rsolid", "make_translate_rshape", "make_rotate_rshape", + "make_scale_rsolid", + "make_transform_rsolid", + "make_offset_rsolid", + "make_extrude_taper_rsolid", + "make_sweep_multisection_rsolid", + "make_extrude_until_rsolid", + "make_draft_rsolid", + "make_boolean_rsolidlist", "make_mirror_rshape", "make_cut_rsolid", "make_union_rsolid", diff --git a/test/test_sketch_closed_bspline.py b/test/test_sketch_closed_bspline.py new file mode 100644 index 00000000..cacb7d73 --- /dev/null +++ b/test/test_sketch_closed_bspline.py @@ -0,0 +1,58 @@ +"""Closed spline seams are topology endpoints, including periodic NURBS.""" +import math +import pytest +import simplecadapi as scad +from OCP.BRepGProp import BRepGProp +from OCP.GProp import GProp_GProps + + +def accurate_mass(shape, *, volume=False): + props = GProp_GProps() + if volume: + BRepGProp.VolumePropertiesGK_s(shape.wrapped, props, 1e-10, True, True) + else: + BRepGProp.SurfaceProperties_s(shape.wrapped, props, 1e-10) + return props.Mass() + + +def rational_circle(): + poles = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), (1, 0)] + weights = [1, math.sqrt(.5), 1, math.sqrt(.5), 1, math.sqrt(.5), 1, math.sqrt(.5), 1] + return poles, weights + + +def test_closed_rational_bspline_same_endpoint_promotes_exact_face(): + poles, weights = rational_circle() + sketch = scad.make_sketch_rsketch(name='closed-rational') + sketch = scad.add_point_rsketch(sketch, 'seam', 1, 0) + sketch = scad.add_bspline_rsketch(sketch, 'boundary', 'seam', 'seam', poles, + degree=2, knots=[0, .25, .5, .75, 1], multiplicities=[3, 2, 2, 2, 3], weights=weights) + face = scad.make_face_from_sketch_rface(sketch=sketch) + assert accurate_mass(face) == pytest.approx(math.pi, rel=1e-9) + with scad.GraphSession() as session: + sketch2 = scad.make_sketch_rsketch(name='replayed-closed-rational') + sketch2 = scad.add_point_rsketch(sketch2, 'seam', 1, 0) + sketch2 = scad.add_bspline_rsketch(sketch2, 'boundary', 'seam', 'seam', poles, + degree=2, knots=[0, .25, .5, .75, 1], multiplicities=[3, 2, 2, 2, 3], weights=weights) + face2 = scad.make_face_from_sketch_rface(sketch=sketch2) + solid = scad.extrude_rsolid(face2, (0, 0, 1), 2) + session.capture_result(value=solid) + from simplecadapi.recording.serializer import replay_graph + rebuilt = replay_graph(session.graph) + assert accurate_mass(rebuilt[0], volume=True) == pytest.approx(math.pi * 2, rel=1e-9) + + +def test_periodic_bspline_seam_does_not_replace_control_poles(): + poles = [(2, 0), (0, 2), (-2, 0), (0, -2)] + # The seam lies midway between poles for this periodic quadratic basis. + native = scad.make_spline_redge(control_points=[(x, y, 0) for x, y in poles], degree=2, periodic=True) + from OCP.BRepAdaptor import BRepAdaptor_Curve + adaptor = BRepAdaptor_Curve(native.wrapped) + seam = adaptor.Value(adaptor.FirstParameter()) + wire = scad.make_wire_from_edges_rwire([native]) + reference = scad.make_face_from_wire_rface(wire) + sketch = scad.make_sketch_rsketch(name='periodic') + sketch = scad.add_point_rsketch(sketch, 'seam', seam.X(), seam.Y()) + sketch = scad.add_bspline_rsketch(sketch, 'boundary', 'seam', 'seam', poles, degree=2, periodic=True) + face = scad.make_face_from_sketch_rface(sketch=sketch) + assert face.get_area() == pytest.approx(reference.get_area(), rel=1e-9) diff --git a/test/test_skill_cli.py b/test/test_skill_cli.py index 36abf4ad..0e0f9574 100644 --- a/test/test_skill_cli.py +++ b/test/test_skill_cli.py @@ -124,3 +124,27 @@ def test_main_reports_unknown_target_without_traceback(project, tmp_path, capsys error = capsys.readouterr().err assert "unknown target" in error and "Traceback" not in error assert not (tmp_path / "s").exists() + + +def test_where_reports_the_destination_without_writing(project, tmp_path): + report, code, printer = run(["skill", "where"]) + expected = tmp_path / "home" / ".agents" / "skills" / "demo" + assert code == 0 and Path(report["where"]) == expected and report["exists"] is False + assert not expected.parent.exists() # nothing was created + printer(report) + run(install_args()) + assert run(["skill", "where"])[0]["exists"] is True + elsewhere, _, _ = run(["skill", "where", "--skills-dir", str(tmp_path / "other")]) + assert Path(elsewhere["where"]) == tmp_path / "other" / "demo" + + +def test_where_reports_the_destination_without_writing(project, tmp_path): + report, code, printer = run(["skill", "where"]) + expected = tmp_path / "home" / ".agents" / "skills" / "demo" + assert code == 0 and Path(report["where"]) == expected and report["exists"] is False + assert not expected.parent.exists() # nothing was created + printer(report) + run(install_args()) + assert run(["skill", "where"])[0]["exists"] is True + elsewhere, _, _ = run(["skill", "where", "--skills-dir", str(tmp_path / "other")]) + assert Path(elsewhere["where"]) == tmp_path / "other" / "demo" diff --git a/test/test_surface_api.py b/test/test_surface_api.py index 8ba9842e..4a7fddd2 100644 --- a/test/test_surface_api.py +++ b/test/test_surface_api.py @@ -431,7 +431,9 @@ def point_on(face, u, v): solid = scad.make_solid_from_shell_rsolid(shell) self.assertIsInstance(solid, scad.Solid) - self.assertAlmostEqual(solid.get_volume(), 512019.199, delta=2.0) + # The sewn solid is an 80 mm cube; the knot-span volume integral is exact + # (the fixed-order one overshot it by ~19 mm3 on these rational patches). + self.assertAlmostEqual(solid.get_volume(), 80.0**3, delta=0.1) self.assertTrue(all(face.get_area() > 4 * side * side for face in solid._iter_faces())) def test_trim_surface_with_hole_and_replay(self): diff --git a/tools/research/build123dftc/API_RESEARCH.md b/tools/research/build123dftc/API_RESEARCH.md new file mode 100644 index 00000000..1e635d55 --- /dev/null +++ b/tools/research/build123dftc/API_RESEARCH.md @@ -0,0 +1,124 @@ +# build123d 0.13 API 与测试集调研 + +本调研针对工作区 `.venv` 中实际安装的 build123d 0.13.0,以及上层 `tests/manifest.json` 固定的 `v0.13.0`、提交 `38effd343e1508964673faf59300a5cc68fbedc1`。2026 年 10 月 7 日已联网核查对应版本的官方文档。清单是测试范围和实现检查表,不能当作已通过转换的声明。 + +## 完整公开入口清单 + +`api_coverage.json` 从实际安装包生成,包含以下范围: + +| 范围 | 数量 | 含义 | +| --- | ---: | --- | +| 包 `__all__` 导出 | 203 | 官方顶层公开入口 | +| 公开成员定义 | 540 | 按定义所属类去重,含可达继承方法、属性与建模运算符 | +| 类接口 | 138 | 导出类,加上 Shape、几何维度 mixin、Builder、BaseObjectMeta | +| 工作区模型 | 50 | manifest 中全部模型,逐项记录调用位置统计 | +| 模型使用的顶层调用 | 50 种 | 已排除 `__main__` 导出代码和恒为 False 的文档生成分支 | +| 顶层调用位置 | 454 | 源码静态位置数量,循环次数和运行时内部调用未展开 | + +顶层入口分类:32 个枚举,15 个常量或类型别名,3 个 builder,4 个位置阵列上下文,27 个曲线对象入口、15 个草图对象入口、11 个实体对象入口(均包含 Base 类),6 个图纸对象入口,23 个建模操作,12 个几何类入口,20 个直接拓扑入口,5 个装配关节,9 个导出入口,6 个导入入口,以及 15 个其他工具入口。 + +成员分类:40 个 `make_*` 等工厂、68 个直接特征方法、18 个变换方法、41 个代数运算符定义、4 个上下文生命周期方法、122 个属性,以及 247 个查询或工具方法。分类采用方法名和所属模块,不能替代逐方法的行为测试。底层 OCP/Python 标准类型的成员和下划线内部实现不算公开 API;专门列入了建模运算符。 + +重新生成: + +```bash +.venv/bin/python simplecadapi/tools/research/build123dftc/research/inspect_api.py \ + --manifest tests/manifest.json \ + --output simplecadapi/tools/research/build123dftc/api_coverage.json +``` + +清单内每条入口有签名、定义文件与行号,类接口指向去重后的成员定义。`conversion_status: unverified` 表示尚未通过该接口的转换验证。后续覆盖报告应区分“识别入口”“真正生成 SimpleCAD 特征”“几何验证通过”,不要把成功执行源脚本等同于转换成功。 + +生成器也接受 `--validation-report` 和 `--traces-dir`。指定严格验收报告后,`verified_sampled_usage` 仅表示列出的模型中执行过该入口且模型验收通过。矩阵保留模型名单与报告 SHA256;其他参数、模式和未执行分支仍未验证。方法证据使用 trace 的实际 `source`,不会仅凭同名方法就把整个继承接口标为通过。最终验收报告更新后需重新生成矩阵。 + +## 捕获结构与可行的扩展方案 + +builder 与 algebra 是建模方式,Direct API 是两者共用的拓扑层;顶层入口捕获和底层特征捕获可以互补。[官方 Direct API 说明](https://build123d.readthedocs.io/en/v0.13.0/direct_api_reference.html) + +当前实现选择 Direct API 层记录实体构造图,并在实体特征的输入边界转录草图轮廓。这覆盖两种建模方式,不需要改变 builder 的 Python frame。下文第一层是未来扩展原始对象/草图操作记录的方案,不是当前已实现的声明。`project / offset / make_hull` 等草图变化目前进入求解后的精确轮廓;不会逐项保留原草图操作。非平面 Face/Shell 曲面工厂、Vertex 终点放样以及清单中其他未验证 API 仍需单独检查,未支持构造必须显式报告。 + +### 第一层:可读的原建模操作 + +安装源码中 `build_common.BaseObjectMeta.__call__` 是全部内置 `BaseObject` 子类的统一入口。它能保留 `Box`、`Circle`、`Spline`、`Hole` 等原参数,也能接收自定义继承类;外层调用期间,内部建模需要防止被重复计入。23 个公开 operations 应全部登记。记录原参数以后,应展开 locations、默认参数以及隐式 pending faces/edges。 + +### 第二层:Direct API 特征与几何 + +工厂需要覆盖以下定义;方法会被继承,不能只包装 `Solid` 一种类: + +- `Edge.make_bezier / make_circle / make_constrained_arcs / make_constrained_lines / make_ellipse / make_parabola / make_hyperbola / make_helix / make_line / make_mid_way / make_spline / make_bspline / make_spline_approx / make_tangent_arc / make_three_point_arc`。 +- `Wire.make_circle / make_convex_hull / make_ellipse / make_polygon / make_rect`。 +- `Face.make_bezier_surface / make_gordon_surface / make_rect / make_surface / make_surface_from_array_of_points / make_surface_from_curves / make_surface_patch / make_holes`。 +- `Shell.make_loft`。 +- `Solid.make_box / make_cone / make_cylinder / make_loft / make_sphere / make_torus / make_wedge`。 +- `Compound.make_text / make_triad`。 +- `Shape / Mixin1D / Mixin2D / Mixin3D / Edge / Wire / Face / Shell / Solid / Compound` 各自声明的 `extrude`;其中部分重载返回不同维度,不能假定都是 Solid。 +- 3D 的 `extrude_taper / extrude_until / extrude_linear_with_rotation / revolve / sweep / sweep_multi / thicken / draft / fillet / chamfer / hollow / offset_3d / dprism`。 +- 2D/1D 的 `offset / offset_2d / fillet_2d / chamfer_2d / trim* / project* / split* / close / stitch / combine / wrap*`。 + +对于没有 SimpleCAD 同名原语的曲线构造(如受约束圆弧、Airfoil、BlendCurve),可把求解后的单条曲线分解为解析参数或精确 BSpline 定义,再以 SimpleCAD 曲线原语重建。需保留 poles、degree、weights、knots、multiplicities、periodic,以及区间/方向。这个方法保留草图和后续特征,而不是把最终实体导入当作转换。 + +### 第三层:组合、位置与查询 + +布尔定义分布在 `Shape`、`Compound` 与 `Mixin1D`。1D 的加法可能得到 Wire/Curve,不能按 3D union 发射。`Shape` 的 `fuse / cut / intersect` 是 builder 和 algebra 实际共用的接口,应按几何维度分派。 + +`Location.__mul__` 对 Shape 实际调用 `other.moved(self)`,`Plane.__mul__` 也进入位置变换。因此包装 `Shape.moved` 后已经能捕获大多数 algebra placement 和 builder 发布,不必再重复包装 `*`。`move / locate / location.setter` 是原位变更;`moved / located` 返回副本。原位变更应建立新节点,保留变更前的输入节点。`translate / rotate / scale / mirror / transformed / transform_shape / transform_geometry` 也需覆盖。 + +查询 API 大多数不用发射新的 FTC 操作,但查询结果必须用于源脚本继续执行。边/面选择器的结果应关联已有特征,再记录指纹或索引;不能把选中边的 repr 当作可重放参数。曲线的 `@ / % / ^` 分别参与求值和位置计算,记录最终 Vector/Location 参数即可。枚举、单位、字体和颜色也应有稳定的序列化。 + +## 0.13 builder 语义的两个关键陷阱 + +所有 builder 先在自身 XY 局部坐标中构造。`output_placements` 只在输出或退出后统一应用。对象构造时使用的 `Locations` 属于局部位置;builder 发布时另有外层 `publication_locations`。两种位置不能重复应用,尤其是嵌套 BuildPart、BuildSketch、BuildLine 和多个 face placement。[官方 builder 位置说明](https://build123d.readthedocs.io/en/v0.13.0/key_concepts_builder.html) + +本地源码 `Builder.__enter__` 用 Python frame 判断当前 builder 与父 builder 是否同一作用域。直接加 `__enter__` 或 `__init__` 包装函数会改变 frame,可能把嵌套 builder 误判为无父级。优先捕获 `_add_to_context`、`_PublicationService.place/publish`;如必须包装 enter/init,要保持原同作用域判定的含义,并测试函数内返回私有草图这种有意隔离的情况。 + +`Builder._add_to_context` 中拿到的 shapes 是 builder 的局部坐标,不能称为全局坐标。此处同时维护累计几何、pending faces/edges 和 Select.LAST/NEW 的变化历史。对截面依赖引用、用于扫掠的独立路径、多个离散实体,需要保留输入依赖图。 + +## 50 个模型真正考验的能力 + +最常出现的静态调用:BuildSketch 57,BuildPart 55,extrude 40,Circle 29,Rectangle 25,Locations 23,Box/Line 各 18,BuildLine 16,fillet 15,RegularPolygon 14,GridLocations 11,Plane 10。完整逐模型统计见 JSON。 + +以下模型需要独立回归,避免简单盒子通过造成覆盖错觉: + +| 模型 | 关键行为 | +| --- | --- | +| 09、10、11、18、29、45、46、47 | 边/面选择、3D 圆角与倒角、修改后的选择指纹 | +| 12、30、40、46、47 | 样条、Bezier、端点切向量、非直线轮廓及扫掠 | +| 13、41、49 | 普通孔、沉孔、锥孔、负向深度、自动贯穿深度、多位置 | +| 14、39、40、46、50 | pending 路径/截面、扫掠参数、曲线起点坐标系 | +| 16、17、20、21、22、24、27、37、38、43 | 不同 workplane、嵌套位置、旋转、偏移与 builder 发布 | +| 19、31、32、33、42、48 | 循环/函数生成阵列,以及源查询决定下一操作参数 | +| 23、29、44、46、47 | 旋转、放样、壁厚偏置、开口选择、多截面 | +| 34 | Text 的字体形状与字内孔,不能只重建外轮廓 | +| 36 | `extrude(until=Until.*)`,限界对象与方向 | +| 45 | section、面内孔与比例缩放 | +| 49 | 四个彼此独立的 BuildPart 输出组合为 Compound | +| 50 | 完全 algebra 模型:Helix、`path ^ 0`、位置相乘、显式 sweep 参数 | + +源码中被禁用的 SVG 文档导出和 `__main__` STEP 输出不属于建模转换测试,但导入/导出公开入口仍列在 API 清单。清单中的矩阵、关节、图纸类和未被这 50 个模型使用的几何操作,应通过额外小型行为用例逐项验证;50 个模型不等于全部 API 覆盖。 + +## SimpleCAD 对应关系与几何验证 + +现有目标原语已提供:线/圆/椭圆/三点圆弧/角度圆弧/精确样条/插值样条/螺旋、wire/face 构造、草图 2D 布尔、3D 原语、3D 布尔、拉伸/旋转/放样/扫掠、圆角/倒角/抽壳,以及曲面构造。可优先复用 `operators/geometry.py`、`features.py`、`boolean.py`、`transform.py` 和 cqftc 的选择指纹与重放模式。 + +每个测试应同时检查操作结构与几何:FTC 内必须存在实际构造特征和输入依赖,执行生成脚本时不依赖 build123d;体积、包围盒、实体数量和有效性应该与源模型一致。只有体积相同无法证明孔位置和姿态正确,建议再比较差集体积或对称差集,并在拓扑变化时用几何指纹选择。基准来源是执行源脚本所得结果或 manifest 的 STEP,不能把待检 FTC 的结果本身作为基准。 + +验收工具已实现独立进程与超时隔离,缺少指标、源执行失败、空 BODIES、无效实体、转换元数据中的 unsupported/unknown 都会拒绝。包围盒比较六个世界坐标而非仅比较尺寸。体积与 STEP 差集使用 `VolumePropertiesGK_s(..., Eps=1e-9, OnlyClosed=True, IsUseSpan=True)`;有理 BSpline 使用默认积分或普通自适应积分可能产生明显误差,所以源与目标必须用同一定义。 + +模型 28 揭示了内核差异:OCP 7.9 对实际形状与基准 STEP 的共同内部点判为 IN,却把 Common 计算为空、双向 Cut 留下全部体积。标准 ShapeFix 和微小 fuzzy 未解决。实际形状重新导出 STEP 后,独立 OCP 8.0.1.1 求得 Common 等于完整体积、双向 Cut 都为空。可选 `--reference-python` 因此让 FTC 在 SimpleCAD 环境中构建,同时在指定独立内核进程中对 STEP 做差集。实际 STEP 的读回体积、包围盒、有效性与实体数量也必须与 FTC 真实 BODIES 一致,验收门槛不变,报告记录两个内核与积分方法。 + +面积计算在独立的几何深拷贝上先按 C1 连续性切分积分区间,再用明确 `Eps` keyword 的自适应积分;不修改实际 FTC 或 STEP。半径 1、高度 1 的精确有理 BSpline 圆柱有面积 `4π`、体积 `π` 的独立数学测试。OCP 7.9 的测量区间切分在该例有约 `7.7e-9` 相对误差,数学测试精度为 `1e-8`,整模型验收门槛保持原值。 + +参考 STEP 的临时实际导出使用 trace `source_metadata.export_options` 中声明的参数曲线与精度模式;没有声明时用标准 `True / AVERAGE`。差集布尔在首次 Build 前统一使用输入实际 BRep 顶点/边容差与内核 `Precision.Confusion` 的最大值,记录该值,不逐级尝试容差。差集必须有效并通过原体积门槛。模型 46 的当前记录表明即使匹配 `False / GREATEST` 并使用该固定规则,跨内核差集仍可能无效;这种情况会报告 `reference_error`,不会将基础指标通过冒充严格 STEP 匹配通过。 + +验收 CLI 有任何 rejected/partial 或没有可验收案例时返回退出码 1,所有案例 accepted 才返回 0;NaN、Inf 与非正 timeout 会作为参数错误拒绝,报告仍保存每例拒绝原因。 + +确实不能转换的入口应报出明确 API 名、源位置与原因。允许用户显式选择近似曲线时,应报告近似误差并保持后续特征可修改。最终 BRep/STEP 导入只能作为明确标记的外部几何来源,不能用它把未覆盖操作伪装成成功 FTC 转换。 + +## 官方核查来源 + +- [Objects](https://build123d.readthedocs.io/en/v0.13.0/objects.html) +- [Operations](https://build123d.readthedocs.io/en/v0.13.0/operations.html) +- [Builder concepts](https://build123d.readthedocs.io/en/v0.13.0/key_concepts_builder.html) +- [Algebra concepts](https://build123d.readthedocs.io/en/v0.13.0/key_concepts_algebra.html) +- [Direct API reference](https://build123d.readthedocs.io/en/v0.13.0/direct_api_reference.html) +- [固定测试集上游提交](https://github.com/gumyr/build123d/tree/38effd343e1508964673faf59300a5cc68fbedc1) diff --git a/tools/research/build123dftc/BENCHMARK.md b/tools/research/build123dftc/BENCHMARK.md new file mode 100644 index 00000000..c6a2be32 --- /dev/null +++ b/tools/research/build123dftc/BENCHMARK.md @@ -0,0 +1,59 @@ +# 工作区 50 个模型实测 + +日期:2026-10-07。输入为相邻工作区 `tests/manifest.json` 中的 50 个官方 +build123d 模型,版本 0.13.0,源内核 OCP 8.0.1.1;FTC 在 +SimpleCADAPI 2.2.0b1 / OCP 7.9.3.1 中构建,参考 STEP 在独立 OCP 8 环境对账。 +运行命令见 [README.md](README.md)。 + +| 检查 | 结果 | +| --- | --- | +| 源模型执行、特征图捕获 | 50 / 50,未识别实体操作 0 | +| FTC 源码生成 | 50 / 50,410 个特征,无未翻译记录 | +| FTC 实际构建、内核有效性、实体数量 | 50 / 50 | +| 体积、表面积、世界包围盒 | 50 / 50 | +| 加入参考 STEP 双向差集的严格验收 | 49 / 50 | + +所有模型的最大相对体积误差为 `1.492e-6`,最大面积误差为 `1.092e-6`, +最大包围盒误差(相对于对应轴跨度)为 `2.778e-9`。均远低于 `1e-3` 的 +验收阈值。原始报告在本机 `out/validate.json`,逐模型源码在 `out/ftc`, +输入特征图在 `out/traces`。这些运行产物被 Git 忽略。 + +模型 `46_tea_cup` 构建成功且全部基础几何检查通过,体积相对误差约 +`1.45e-9`、世界包围盒一致。独立参考内核的 STEP 双向差集计算仍生成无效 +拓扑,因此保留 `reference_error`,严格验收判为拒绝。没有跳过该模型、 +改变差集门槛或把无效差集当作零。报告同时给出 `metrics_quality=accepted` +与 `quality=rejected`,批量 CLI 在这次严格验收中返回 1。 + +测试按先写断言再实现的方式推进,相关回归包括真实捕获、格式、几何、未知 +API、孤立进程崩溃、超时、同体积/同包围盒但孔位不同的反例、图重放以及 +既有 CQ / WHUCAD、草图、QL 测试。295 项相关用例通过,另有 35 个子测试。 +六个真实图 fixture 保存在 `test/build123d_traces`,普通 CI 不需要 build123d。 + +203 个顶层导出、540 个去重公开成员的清单与样例证据在 `api_coverage.json`。 +标为 `verified_sampled_usage` 的条目仅说明相应已验收样例使用过该 API, +不代表其全部参数组合或全部公开 API 都已通过转换。复杂曲面构造等未接入 +边界在 [API_RESEARCH.md](API_RESEARCH.md) 和转换 meta 中明确列出。 + +## 2026-10-13 审查修复与新增 41 个模型 + +修复 1 项 P1 和 9 项 P2,涵盖空输出识别、实际 build 返回值验收、多实体 +布尔及圆角、Face 截面孔、空侧分割、QL 输入录制和多输出槽重放。 +圆环的主圆周角度被保留,非完整管截面明确标为未转换。 + +| 检查 | 原有 50 个 | 新增 41 个 | +| --- | --- | --- | +| 源模型捕获 | 50 / 50 | 40 / 41 | +| 完整 FTC 生成 | 50 / 50 | 31 / 41 | +| 基础几何验收 | 50 / 50 | 21 / 41 | +| 参考 STEP 严格对账 | 49 / 50 | 21 / 41 | + +原有 50 个仅剩模型 46 的既有参考内核错误。新增模型 80 在 180 秒捕获 +上限内未完成;其他未通过项包括未录制的曲面/实体构造、草图与扫掠构建 +失败、选择失败、实际返回模型与 BODIES 不一致、几何差异及参考内核错误。 +未放宽验收阈值,未跳过失败模型。 + +最终相关回归 247 项通过,另有 137 个子测试。全仓库运行中的 3 项剩余失败 +在未修改的 HEAD 副本中复现,分别涉及圆角诊断、FreeCAD 关节计数和 Gordon +曲面体积断言。逐模型结果、生成代码和失败说明保存在相邻工作区 +`artifacts/build123dftc/REVIEW_RESULTS.md`;两批原始 JSON 分别在 +`review_fixed/validate.json` 和 `additional_review_fixed/validate.json`。 diff --git a/tools/research/build123dftc/README.md b/tools/research/build123dftc/README.md new file mode 100644 index 00000000..51ffb8c5 --- /dev/null +++ b/tools/research/build123dftc/README.md @@ -0,0 +1,164 @@ +# build123d → FTC 转换工具 + +把真实执行的 build123d 模型转换成本仓库现行 FTC Python:显式草图、实体 +特征、布尔和 QL 修改选择。组织方式沿用 `tools/research/cadqueryftc`,转换 +与验收分开。支持 Builder、代数写法及已接入的底层实体 API。 + +```text +trusted model.py → build123d_trace.py → trace.json +trace.json → build123d_to_ftc.py → ftc.py + meta.json + sketches.json +ftc.py + trace.json [+ reference STEP] → build123d_validate.py → validate.json +trace.json → build123d_profile.py → profile.json +``` + +捕获执行模型的 Python;只对可信源文件运行。模型的 `__main__` 分支不会运行, +可在顶层将最终形状绑定为 `result`。显式输出即使为空也具有优先级,不会回退 +到先前创建的实体。捕获和验收分别在隔离子进程执行,设置 +超时,一例失败仍处理后续文件。 + +## 环境 + +本次实测为 Python 3.12、build123d 0.13.0 / OCP 8.0.1.1,以及 +SimpleCADAPI 2.2.0b1 / OCP 7.9.3.1。两个内核的绑定不能混装在同一环境: + +```bash +# 在 SimpleCADAPI 仓库根目录运行。环境放在相邻工作区。 +uv venv ../.venv --python 3.12 +uv pip install --python ../.venv/bin/python 'build123d==0.13.0' +uv venv ../.venv-sca --python 3.12 +uv pip install --python ../.venv-sca/bin/python -e . 'cadquery-ocp==7.9.3.1' pytest +``` + +已有环境无需重建。翻译阶段是纯 Python,不导入 build123d 或 CAD 内核;启用 +约束恢复时才加载现有 CQ 的求解器。 + +## 单模型 + +```bash +../.venv/bin/python tools/research/build123dftc/build123d_trace.py \ + --input ../tests/models/01_rectangular_plate.py \ + --out tools/research/build123dftc/out/traces + +../.venv-sca/bin/python tools/research/build123dftc/build123d_to_ftc.py \ + --trace tools/research/build123dftc/out/traces/01_rectangular_plate.trace.json \ + --out tools/research/build123dftc/out/ftc + +../.venv-sca/bin/python tools/research/build123dftc/build123d_validate.py \ + --ftc-dir tools/research/build123dftc/out/ftc \ + --traces-dir tools/research/build123dftc/out/traces \ + --report tools/research/build123dftc/out/validate.json +``` + +`--constraints on` 从实际草图恢复推断约束,复用 `cqftc.constraints`。只有生产 +求解器确认坐标复现才插入约束;约束 ID 标记推断出处。椭圆、样条及耗时超限 +的草图在 `constraint_recovery` 中明确记录状态,其精确几何仍保留。 + +## 工作区 50 个模型 + +```bash +../.venv/bin/python tools/research/build123dftc/build123d_trace.py \ + --manifest ../tests/manifest.json \ + --out tools/research/build123dftc/out/traces --timeout 180 + +../.venv-sca/bin/python tools/research/build123dftc/build123d_to_ftc.py \ + --traces-dir tools/research/build123dftc/out/traces \ + --out tools/research/build123dftc/out/ftc + +../.venv-sca/bin/python tools/research/build123dftc/build123d_validate.py \ + --ftc-dir tools/research/build123dftc/out/ftc \ + --traces-dir tools/research/build123dftc/out/traces \ + --reference-step-dir ../tests/step --reference-python ../.venv/bin/python \ + --report tools/research/build123dftc/out/validate.json --timeout 180 + +../.venv-sca/bin/python tools/research/build123dftc/build123d_profile.py \ + --traces-dir tools/research/build123dftc/out/traces \ + --report tools/research/build123dftc/out/profile.json +``` + +也可用 `--inputs-dir` 处理目录中的 `.py` 文件,或用 `--input` 指定一个模型。 +每个模型产生 `.trace.json`、执行日志、`.ftc.py`、`.meta.json` 和 +`.sketches.json`。批量验收报告保留所有模型,缺少转换结果也计入失败。 + +## 验收依据 + +- 实际执行 FTC,以 `build()` 返回的 Solid、Part 或 Assembly 为验收对象, + 应用嵌套装配位置后检查非空实体、内核有效性、体积、表面积和实体数量。 + `BODIES` 必须非空,并与实际返回模型的实体数量和几何一致。 +- 比较世界坐标中的六个包围盒边界;同尺寸但位置错误会失败。 +- 指定参考 STEP 时,要求两个方向几何差集的总体积通过检查。 +- `--reference-python` 让参考几何对账在指定内核执行,FTC 仍在 + SimpleCADAPI 环境构建。报告记录两个内核版本,并检查 STEP 导出往返的 + 体积、包围盒、有效性和实体数。 +- 体积使用按样条节点区间计算的 Gauss–Kronrod 积分。普通固定阶积分对有理 + 样条有明显误差,还会在某些非均匀缩放后的裁剪曲面上崩溃。 + +面积只在独立副本上切开不连续的曲面参数区间,再做自适应积分;原始模型和 +拓扑保持不变。参考导出沿用 manifest 声明的参数曲线与精度选项,布尔对账 +采用输入已有的内核容差,报告保留这些设置。 + +阈值为体积、面积和包围盒相对误差 `1e-3`,另有绝对误差保护;STEP 双向 +差集体积相对误差 `1e-3`。源执行失败、缺指标、未知操作、翻译不完整和空实体 +都不会判为通过。验收通过是给定输入在这些阈值下的结果。`metrics_quality` +单独报告基础几何检查;要求 STEP 对账时,任何参考计算失败仍会使 +`quality` 为拒绝,且 CLI 返回非零。工作区实测结果见 [BENCHMARK.md](BENCHMARK.md)。 + +模型 28 的重合球面在 OCP 7.9 中出现错误的空交集;相同几何在 OCP 8 的 +严格双向差集为零。因此上述工作区命令使用独立的 OCP 8 参考环境。 + +## 转换结构和覆盖 + +`b3ftc/tracer.py` 在真实底层实体构造、布尔、修改和变换边界捕获操作图。 +所有已执行的草图对象与查询仍由 build123d 计算,在实体特征的输入处转录 +实际轮廓。JSON 保存参数、世界坐标、精确曲线控制数据和拓扑指纹,不保存 +最终实体 BRep。`profiles.py`、`emit.py`、`replay.py` 分别负责轮廓、格式和 +实体数据流;外部验收位于 `validation.py` 及独立 worker。 + +当前支持盒、圆柱、圆锥、球、完整圆环及部分原语;拉伸、带拔模角拉伸、 +限界拉伸、扭转拉伸、旋转、放样、单/多截面扫掠;布尔、圆角、倒角、拔模、 +双向实体偏移与抽壳、平面分割、刚体与一般仿射变换、多实体组合。 +草图支持直线、圆弧、圆、椭圆、精确有理 B 样条与闭合/周期样条;曲线型 +文字及三维路径使用实际曲线控制数据。多实体输出为完整 `@scad.assemble`。 + +切割与交集使用录制的 `scad.boolean_rsolidlist` 保留全部输出实体;Compound +成员属于同一交集操作数,多参数交集仍按顺序取交。每块实体的圆角/倒角使用 +各自的选边基数,未选中的实体直接保留。多输出顺序通过实体几何指纹恢复; +截面特征保存有序输入引用及输出槽,QL 得到的截面也进入录制图。 +实体排序使用世界包围盒和体积,避免不同 OCCT 版本质心计算的差异。 +圆环支持 `major_angle` 主圆周角度;非完整管截面的 `start_angle/end_angle` +目前明确记录为未转换,不会生成完整圆环代替。 +更新工具后应重新捕获旧模型;缺少逐实体选边信息的旧多实体圆角图会被标为 +未转换,历史空结果图也需要重捕获才能纠正。 + +生成代码使用现行 FTC 块头、几何档标注、keyword 实参、QL 谓词和基数。 +数字来源于实际调用,不虚构参数名。动态追踪只描述执行到的路径;输出不会 +保留未执行的分支,也不承诺恢复源草图的约束或全部表达式。 + +**API 清单不等于全部 API 转换已通过验证。** `api_coverage.json` 和 +`API_RESEARCH.md` 梳理了 203 个顶层导出、540 个去重公开成员和 138 个类。 +查询、布局和多数草图 API 通过实际求值再转录轮廓覆盖;非平面曲面构造、 +原生内核直接包装等未接入的实体构造、部分圆环截面、特殊扫掠模式、复杂孔 +拓扑变化等仍有明确边界。未知节点会阻断依赖它的后续特征,并写入 meta, +不会导入最终模型作为替代。完整清单可重生: + +```bash +../.venv/bin/python tools/research/build123dftc/research/inspect_api.py --help +``` + +## 回归测试 + +`test/build123d_traces` 保留真实捕获的轮廓、修改、镜像、文字和螺旋扫掠 +fixture,并带出处。普通 FTC 测试不依赖 build123d;执行捕获测试在其专用 +环境运行。 + +```bash +../.venv/bin/python -m pytest test/test_build123d_trace.py test/test_build123d_unknown_api.py -q +../.venv-sca/bin/python -m pytest \ + test/test_build123d_cli.py test/test_build123d_translation_output.py \ + test/test_build123d_validation.py test/test_deformation_features.py \ + test/test_section_features.py test/test_limited_extrusion.py \ + test/test_rational_volume_precision.py test/test_sketch_closed_bspline.py -q +``` + +`test/test_build123d_review_regressions.py` 额外从专用 build123d 环境捕获审查 +反例,再在 SimpleCADAPI 环境执行生成代码并验收实际返回模型;运行该文件 +需要相邻的 `../.venv`。 diff --git a/tools/research/build123dftc/api_coverage.json b/tools/research/build123dftc/api_coverage.json new file mode 100644 index 00000000..15ecbe82 --- /dev/null +++ b/tools/research/build123dftc/api_coverage.json @@ -0,0 +1,19803 @@ +{ + "schema_version": 1, + "purpose": "Version-specific API inventory and static source corpus evidence; unverified is not implemented support.", + "build123d_version": "0.13.0", + "corpus_tag": "v0.13.0", + "corpus_commit": "38effd343e1508964673faf59300a5cc68fbedc1", + "sources": [ + "https://build123d.readthedocs.io/en/v0.13.0/objects.html", + "https://build123d.readthedocs.io/en/v0.13.0/operations.html", + "https://build123d.readthedocs.io/en/v0.13.0/key_concepts_builder.html", + "https://build123d.readthedocs.io/en/v0.13.0/key_concepts_algebra.html", + "https://build123d.readthedocs.io/en/v0.13.0/direct_api_reference.html" + ], + "scope": "203 package __all__ exports plus public build123d-declared members reachable through these classes, Shape, topology mixins and Builder; Python/OCP internals are excluded.", + "counts": { + "top_level_exports": 203, + "unique_public_member_declarations": 540, + "class_interfaces": 138, + "corpus_models": 50, + "corpus_distinct_top_level_calls": 50, + "corpus_top_level_call_sites": 454, + "top_level_by_category": { + "constant_or_type_alias": 15, + "enum": 32, + "locations": 4, + "builder": 3, + "object_curve": 27, + "drawing_object": 6, + "object_sketch": 15, + "object_part": 11, + "geometry": 12, + "direct_topology": 20, + "assembly_joint": 5, + "utility": 15, + "export": 9, + "import": 6, + "operation": 23 + }, + "members_by_category": { + "algebra_operator": 41, + "property": 122, + "query_or_utility": 247, + "feature": 68, + "transform": 18, + "factory": 40, + "context_lifecycle": 4 + } + }, + "top_level_api": [ + { + "name": "MC", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "MM", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "CM", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "M", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "IN", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "FT", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "THOU", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "UNITS_PER_METER", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "G", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "KG", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "G_PER_LB", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "LB", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "UNITS_PER_KILOGRAM", + "kind": "value", + "category": "constant_or_type_alias", + "module": "builtins", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Align", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 55, + "signature": "(*values)", + "values": [ + "MIN", + "CENTER", + "MAX", + "NONE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ApproxOption", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 72, + "signature": "(*values)", + "values": [ + "ARC", + "NONE", + "SPLINE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "AngularDirection", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 83, + "signature": "(*values)", + "values": [ + "CLOCKWISE", + "COUNTER_CLOCKWISE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "CenterOf", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 93, + "signature": "(*values)", + "values": [ + "GEOMETRY", + "MASS", + "BOUNDING_BOX" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ContinuityLevel", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 104, + "signature": "(*values)", + "values": [ + "C0", + "C1", + "C2" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Convexity", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 126, + "signature": "(*values)", + "values": [ + "CONVEX", + "CONCAVE", + "SMOOTH", + "SADDLE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Extrinsic", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 150, + "signature": "(*values)", + "values": [ + "XYZ", + "XZY", + "YZX", + "YXZ", + "ZXY", + "ZYX", + "XYX", + "XZX", + "YZY", + "YXY", + "ZXZ", + "ZYZ" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "FontStyle", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 275, + "signature": "(*values)", + "values": [ + "REGULAR", + "BOLD", + "ITALIC", + "BOLDITALIC" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "FrameMethod", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 171, + "signature": "(*values)", + "values": [ + "FRENET", + "CORRECTED" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "GeomType", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 181, + "signature": "(*values)", + "values": [ + "PLANE", + "CYLINDER", + "CONE", + "SPHERE", + "TORUS", + "BEZIER", + "BSPLINE", + "REVOLUTION", + "EXTRUSION", + "OFFSET", + "LINE", + "CIRCLE", + "ELLIPSE", + "HYPERBOLA", + "PARABOLA", + "OTHER" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "HeadType", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 205, + "signature": "(*values)", + "values": [ + "STRAIGHT", + "CURVED", + "FILLETED" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Intrinsic", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 216, + "signature": "(*values)", + "values": [ + "XYZ", + "XZY", + "YZX", + "YXZ", + "ZXY", + "ZYX", + "XYX", + "XZX", + "YZY", + "YXY", + "ZXZ", + "ZYZ" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Keep", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 237, + "signature": "(*values)", + "values": [ + "ALL", + "BOTTOM", + "BOTH", + "INSIDE", + "OUTSIDE", + "TOP" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Kind", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 251, + "signature": "(*values)", + "values": [ + "ARC", + "INTERSECTION", + "TANGENT" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Sagitta", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 287, + "signature": "(*values)", + "values": [ + "SHORT", + "LONG", + "BOTH" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "LengthMode", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 298, + "signature": "(*values)", + "values": [ + "DIAGONAL", + "HORIZONTAL", + "VERTICAL" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "MeshType", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 309, + "signature": "(*values)", + "values": [ + "OTHER", + "MODEL", + "SUPPORT", + "SOLIDSUPPORT" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Mode", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 262, + "signature": "(*values)", + "values": [ + "ADD", + "SUBTRACT", + "INTERSECT", + "REPLACE", + "PRIVATE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "NumberDisplay", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 321, + "signature": "(*values)", + "values": [ + "DECIMAL", + "FRACTION" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "PageSize", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 331, + "signature": "(*values)", + "values": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "LETTER", + "LEGAL", + "LEDGER" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Tangency", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 353, + "signature": "(*values)", + "values": [ + "UNQUALIFIED", + "ENCLOSING", + "ENCLOSED", + "OUTSIDE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "PositionMode", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 365, + "signature": "(*values)", + "values": [ + "LENGTH", + "PARAMETER" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "PrecisionMode", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 375, + "signature": "(*values)", + "values": [ + "SESSION", + "GREATEST", + "AVERAGE", + "LEAST" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Select", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 394, + "signature": "(*values)", + "values": [ + "ALL", + "LAST", + "NEW" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Side", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 414, + "signature": "(*values)", + "values": [ + "LEFT", + "RIGHT", + "BOTH" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "SortBy", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 425, + "signature": "(*values)", + "values": [ + "LENGTH", + "RADIUS", + "AREA", + "VOLUME", + "DISTANCE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "TextAlign", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 438, + "signature": "(*values)", + "values": [ + "BOTTOM", + "CENTER", + "LEFT", + "RIGHT", + "TOP", + "TOPFIRSTLINE" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Transition", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 452, + "signature": "(*values)", + "values": [ + "RIGHT", + "ROUND", + "TRANSFORMED" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Unit", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 463, + "signature": "(*values)", + "values": [ + "MC", + "MM", + "CM", + "M", + "IN", + "FT", + "G", + "KG", + "LB" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Until", + "kind": "class", + "category": "enum", + "module": "build123d.build_enums", + "conversion_status": "unverified", + "source_file": "build123d/build_enums.py", + "source_line": 480, + "signature": "(*values)", + "values": [ + "NEXT", + "LAST", + "PREVIOUS", + "FIRST" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "HexLocations", + "kind": "class", + "category": "locations", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 989, + "signature": "(radius: 'float', x_count: 'int', y_count: 'int', major_radius: 'bool' = False, align: 'Align | tuple[Align, Align]' = (, ))", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "PolarLocations", + "kind": "class", + "category": "locations", + "module": "build123d.build_common", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/build_common.py", + "source_line": 1093, + "signature": "(radius: 'float', count: 'int', start_angle: 'float' = 0.0, angular_range: 'float' = 360.0, rotate: 'bool' = True, endpoint: 'bool' = False)", + "validation_evidence": { + "accepted_models": [ + "13_countersink_counterbore", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Locations", + "kind": "class", + "category": "locations", + "module": "build123d.build_common", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/build_common.py", + "source_line": 1148, + "signature": "(*pts: 'VectorLike | Vertex | Location | Face | Plane | Axis | Iterable[VectorLike | Vertex | Location | Face | Plane | Axis]')", + "validation_evidence": { + "accepted_models": [ + "05_offset_cutouts", + "06_point_list_holes", + "07_polygon_holes", + "13_countersink_counterbore", + "19_vertex_cutouts", + "23_revolved_profile", + "28_face_located_sphere_holes", + "33_function_generated_pattern", + "36_extrude_until", + "38_rotated_boxes", + "41_pillow_block", + "43_lego", + "45_key_cap", + "48_circuit_board", + "49_holes" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "GridLocations", + "kind": "class", + "category": "locations", + "module": "build123d.build_common", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/build_common.py", + "source_line": 1220, + "signature": "(x_spacing: 'float', y_spacing: 'float', x_count: 'int', y_count: 'int', align: 'Align | tuple[Align, Align]' = (, ))", + "validation_evidence": { + "accepted_models": [ + "11_grid_polygon_holes", + "16_mirrored_3d_parts", + "22_angled_drill_holes", + "31_nested_location_pattern", + "41_pillow_block", + "42_din_rail", + "43_lego", + "48_circuit_board" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BuildLine", + "kind": "class", + "category": "builder", + "module": "build123d.build_line", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/build_line.py", + "source_line": 39, + "signature": "(placement: 'Face | Plane | Location' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), mode: 'Mode' = )", + "validation_evidence": { + "accepted_models": [ + "04_lines_and_arcs_profile", + "08_i_beam", + "12_spline_profile", + "14_swept_square_tube", + "15_mirrored_profile", + "23_revolved_profile", + "29_occ_bottle", + "30_bezier_profile", + "35_curved_slots", + "39_pegboard_j_hook", + "40_handle", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BuildPart", + "kind": "class", + "category": "builder", + "module": "build123d.build_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/build_part.py", + "source_line": 42, + "signature": "(*placements: 'Face | Plane | Location', mode: 'Mode' = )", + "validation_evidence": { + "accepted_models": [ + "01_rectangular_plate", + "02_plate_with_hole", + "03_prismatic_solid", + "04_lines_and_arcs_profile", + "05_offset_cutouts", + "06_point_list_holes", + "07_polygon_holes", + "08_i_beam", + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "12_spline_profile", + "13_countersink_counterbore", + "14_swept_square_tube", + "15_mirrored_profile", + "16_mirrored_3d_parts", + "17_face_mirror", + "18_face_cutout", + "19_vertex_cutouts", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "23_revolved_profile", + "24_circle_rectangle_loft", + "25_offset_polygon_layers", + "26_thin_wall_box", + "27_split_plate", + "28_face_located_sphere_holes", + "29_occ_bottle", + "30_bezier_profile", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern", + "34_embossed_debossed_text", + "35_curved_slots", + "36_extrude_until", + "37_positioned_sketches", + "38_rotated_boxes", + "39_pegboard_j_hook", + "40_handle", + "41_pillow_block", + "42_din_rail", + "43_lego", + "44_loft", + "45_key_cap", + "47_vase", + "48_circuit_board", + "49_holes" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BuildSketch", + "kind": "class", + "category": "builder", + "module": "build123d.build_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/build_sketch.py", + "source_line": 39, + "signature": "(*placements: 'Face | Plane | Location', mode: 'Mode' = )", + "validation_evidence": { + "accepted_models": [ + "03_prismatic_solid", + "04_lines_and_arcs_profile", + "05_offset_cutouts", + "06_point_list_holes", + "07_polygon_holes", + "08_i_beam", + "11_grid_polygon_holes", + "12_spline_profile", + "14_swept_square_tube", + "15_mirrored_profile", + "16_mirrored_3d_parts", + "17_face_mirror", + "18_face_cutout", + "19_vertex_cutouts", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "23_revolved_profile", + "24_circle_rectangle_loft", + "25_offset_polygon_layers", + "27_split_plate", + "28_face_located_sphere_holes", + "29_occ_bottle", + "30_bezier_profile", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern", + "34_embossed_debossed_text", + "35_curved_slots", + "36_extrude_until", + "37_positioned_sketches", + "39_pegboard_j_hook", + "40_handle", + "41_pillow_block", + "42_din_rail", + "43_lego", + "44_loft", + "45_key_cap", + "47_vase", + "48_circuit_board" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BaseCurveObject", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 75, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BaseEdgeObject", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 105, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BaseLineObject", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 90, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Airfoil", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 119, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Bezier", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 240, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "30_bezier_profile" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BlendCurve", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 269, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BSpline", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 432, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "CenterArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 491, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ConstrainedArcs", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 563, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ConstrainedLines", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 819, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "DoubleTangentArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 971, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "EllipticalCenterArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1070, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "EllipticalStartArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1149, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ParabolicCenterArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1257, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "HyperbolicCenterArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1335, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "FilletPolyline", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1458, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Helix", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 1416, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "50_spring" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "IntersectingLine", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1760, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Line", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 1731, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "04_lines_and_arcs_profile", + "12_spline_profile", + "14_swept_square_tube", + "15_mirrored_profile", + "23_revolved_profile", + "29_occ_bottle", + "39_pegboard_j_hook", + "47_vase" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "PolarLine", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 1799, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "39_pegboard_j_hook" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Polyline", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 1894, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "08_i_beam", + "23_revolved_profile", + "30_bezier_profile", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "RadiusArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 1933, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "35_curved_slots", + "47_vase" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "SagittaArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 1985, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Spline", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 2023, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "12_spline_profile", + "40_handle", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "TangentArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 2080, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "JernArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 1631, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "14_swept_square_tube", + "39_pegboard_j_hook" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ThreePointArc", + "kind": "class", + "category": "object_curve", + "module": "build123d.objects_curve", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_curve.py", + "source_line": 2120, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "04_lines_and_arcs_profile", + "29_occ_bottle" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ArrowHead", + "kind": "class", + "category": "drawing_object", + "module": "build123d.drafting", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 66, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Arrow", + "kind": "class", + "category": "drawing_object", + "module": "build123d.drafting", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 108, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BaseSketchObject", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 64, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Circle", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 101, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "03_prismatic_solid", + "05_offset_cutouts", + "06_point_list_holes", + "16_mirrored_3d_parts", + "19_vertex_cutouts", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "23_revolved_profile", + "24_circle_rectangle_loft", + "27_split_plate", + "29_occ_bottle", + "36_extrude_until", + "37_positioned_sketches", + "39_pegboard_j_hook", + "40_handle", + "43_lego", + "44_loft", + "45_key_cap", + "48_circuit_board", + "50_spring" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Draft", + "kind": "class", + "category": "drawing_object", + "module": "build123d.drafting", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 163, + "signature": "(font_size: float = 5.0, font: str = 'Arial', font_style: build123d.build_enums.FontStyle = , head_type: build123d.build_enums.HeadType = , arrow_length: float = 3.0, line_width: float = 0.5, pad_around_text: float = 2.0, unit: build123d.build_enums.Unit = , number_display: build123d.build_enums.NumberDisplay = , display_units: bool = True, decimal_precision: int = 2, fractional_precision: int = 64, extension_gap: float = 2.0) -> None", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "DimensionLine", + "kind": "class", + "category": "drawing_object", + "module": "build123d.drafting", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 330, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Ellipse", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 136, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "37_positioned_sketches" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ExtensionLine", + "kind": "class", + "category": "drawing_object", + "module": "build123d.drafting", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 498, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Polygon", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 169, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Rectangle", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 206, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "03_prismatic_solid", + "05_offset_cutouts", + "07_polygon_holes", + "14_swept_square_tube", + "16_mirrored_3d_parts", + "18_face_cutout", + "24_circle_rectangle_loft", + "36_extrude_until", + "37_positioned_sketches", + "40_handle", + "41_pillow_block", + "42_din_rail", + "43_lego", + "45_key_cap", + "48_circuit_board" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "RectangleRounded", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 239, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "RegularPolygon", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 278, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "07_polygon_holes", + "11_grid_polygon_holes", + "17_face_mirror", + "19_vertex_cutouts", + "25_offset_polygon_layers", + "28_face_located_sphere_holes", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "SlotArc", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 347, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "35_curved_slots" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "SlotCenterPoint", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 377, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "SlotCenterToCenter", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 426, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "35_curved_slots" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "SlotOverall", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 470, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "42_din_rail" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Superellipse", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 517, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Text", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_sketch.py", + "source_line": 581, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "34_embossed_debossed_text" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "TechnicalDrawing", + "kind": "class", + "category": "drawing_object", + "module": "build123d.drafting", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 699, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Trapezoid", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 688, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Triangle", + "kind": "class", + "category": "object_sketch", + "module": "build123d.objects_sketch", + "conversion_status": "unverified", + "source_file": "build123d/objects_sketch.py", + "source_line": 767, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BasePartObject", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "unverified", + "source_file": "build123d/objects_part.py", + "source_line": 58, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Box", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_part.py", + "source_line": 112, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "01_rectangular_plate", + "02_plate_with_hole", + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "18_face_cutout", + "20_offset_plane_cylinder", + "22_angled_drill_holes", + "24_circle_rectangle_loft", + "26_thin_wall_box", + "27_split_plate", + "34_embossed_debossed_text", + "35_curved_slots", + "38_rotated_boxes", + "43_lego" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Cone", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "unverified", + "source_file": "build123d/objects_part.py", + "source_line": 154, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ConvexPolyhedron", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "unverified", + "source_file": "build123d/objects_part.py", + "source_line": 205, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "CounterBoreHole", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_part.py", + "source_line": 264, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "13_countersink_counterbore", + "41_pillow_block", + "49_holes" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "CounterSinkHole", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_part.py", + "source_line": 312, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "13_countersink_counterbore", + "49_holes" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Cylinder", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_part.py", + "source_line": 365, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "02_plate_with_hole", + "13_countersink_counterbore", + "43_lego", + "49_holes" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Hole", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_part.py", + "source_line": 411, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "10_filleted_hole", + "11_grid_polygon_holes", + "28_face_located_sphere_holes", + "49_holes" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Sphere", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/objects_part.py", + "source_line": 453, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "28_face_located_sphere_holes", + "45_key_cap" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Torus", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "unverified", + "source_file": "build123d/objects_part.py", + "source_line": 503, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Wedge", + "kind": "class", + "category": "object_part", + "module": "build123d.objects_part", + "conversion_status": "unverified", + "source_file": "build123d/objects_part.py", + "source_line": 557, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BoundBox", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1034, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "OrientedBoundBox", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2214, + "signature": "(shape: 'Bnd_OBB | Shape')", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Rotation", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2425, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Rot", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/geometry.py", + "source_line": 2425, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "38_rotated_boxes", + "47_vase" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Pos", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2575, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "RotationLike", + "kind": "value", + "category": "constant_or_type_alias", + "module": "types", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ShapeList", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.shape_core", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3301, + "signature": "(iterable=(), /)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "topo_distance_to", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.shape_core", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3115, + "signature": "(other: 'Shape | Iterable[Shape]') -> 'Callable[[Shape], int | float]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Axis", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/geometry.py", + "source_line": 631, + "signature": "(*args: 'Any', **kwargs: 'Any') -> 'None'", + "validation_evidence": { + "accepted_models": [ + "19_vertex_cutouts" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Color", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1380, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Curve", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.composite", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 954, + "signature": "(obj: 'TopoDS_Compound | Iterable[Shape] | None' = None, label: 'str' = '', color: 'Color | None' = None, material: 'FinishedMaterial | None' = None, joints: 'dict[str, Joint] | None' = None, parent: 'Compound | None' = None, children: 'Sequence[Shape] | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Vector", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/geometry.py", + "source_line": 163, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [ + "47_vase" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "VectorLike", + "kind": "value", + "category": "constant_or_type_alias", + "module": "types", + "conversion_status": "unverified", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Vertex", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.zero_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 89, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Edge", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.one_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1638, + "signature": "(obj: 'TopoDS_Edge | Axis | None | None' = None, label: 'str' = '', color: 'Color | None' = None, parent: 'Compound | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Wire", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.one_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3528, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Face", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.two_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 804, + "signature": "(*args: 'Any', **kwargs: 'Any')", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Matrix", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2617, + "signature": "(*args, **kwargs)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Solid", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.three_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 720, + "signature": "(obj: 'TopoDS_Solid | Shell | None' = None, label: 'str' = '', color: 'Color | None' = None, material: 'FinishedMaterial | None' = None, joints: 'dict[str, Joint] | None' = None, parent: 'Compound | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Shell", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.two_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2843, + "signature": "(obj: 'TopoDS_Shell | Face | Iterable[Face] | None' = None, label: 'str' = '', color: 'Color | None' = None, parent: 'Compound | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Part", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.composite", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 1000, + "signature": "(obj: 'TopoDS_Compound | Iterable[Shape] | None' = None, label: 'str' = '', color: 'Color | None' = None, material: 'FinishedMaterial | None' = None, joints: 'dict[str, Joint] | None' = None, parent: 'Compound | None' = None, children: 'Sequence[Shape] | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Plane", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/geometry.py", + "source_line": 2840, + "signature": "(*args: 'Any', **kwargs: 'Any') -> 'None'", + "validation_evidence": { + "accepted_models": [ + "17_face_mirror", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "27_split_plate", + "37_positioned_sketches", + "39_pegboard_j_hook", + "44_loft", + "45_key_cap" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Compound", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.composite", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/composite.py", + "source_line": 123, + "signature": "(obj: 'TopoDS_Compound | Iterable[Shape] | None' = None, label: 'str' = '', color: 'Color | None' = None, material: 'FinishedMaterial | None' = None, joints: 'dict[str, Joint] | None' = None, parent: 'Compound | None' = None, children: 'Sequence[Shape] | None' = None)", + "validation_evidence": { + "accepted_models": [ + "49_holes" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Location", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/geometry.py", + "source_line": 1739, + "signature": "(*args: 'Any', **kwargs: 'Any') -> 'None'", + "validation_evidence": { + "accepted_models": [ + "38_rotated_boxes" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "GeomEncoder", + "kind": "class", + "category": "geometry", + "module": "build123d.geometry", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1677, + "signature": "(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Joint", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.shape_core", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3936, + "signature": "(label: 'str', parent: 'BuildPart | Solid | Compound')", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "RigidJoint", + "kind": "class", + "category": "assembly_joint", + "module": "build123d.joints", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 53, + "signature": "(label: 'str', to_part: 'Solid | Compound | None' = None, joint_location: 'Location | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "RevoluteJoint", + "kind": "class", + "category": "assembly_joint", + "module": "build123d.joints", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 219, + "signature": "(label: 'str', to_part: 'Solid | Compound | None' = None, axis: 'Axis' = Axis((0, 0, 0), (0, 0, 1)), angle_reference: 'VectorLike | None' = None, angular_range: 'tuple[float, float]' = (0, 360))", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Sketch", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.composite", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 985, + "signature": "(obj: 'TopoDS_Compound | Iterable[Shape] | None' = None, label: 'str' = '', color: 'Color | None' = None, material: 'FinishedMaterial | None' = None, joints: 'dict[str, Joint] | None' = None, parent: 'Compound | None' = None, children: 'Sequence[Shape] | None' = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "LinearJoint", + "kind": "class", + "category": "assembly_joint", + "module": "build123d.joints", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 346, + "signature": "(label: 'str', to_part: 'Solid | Compound | None' = None, axis: 'Axis' = Axis((0, 0, 0), (0, 0, 1)), linear_range: 'tuple[float, float]' = (0, inf))", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "CylindricalJoint", + "kind": "class", + "category": "assembly_joint", + "module": "build123d.joints", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 516, + "signature": "(label: 'str', to_part: 'Solid | Compound | None' = None, axis: 'Axis' = Axis((0, 0, 0), (0, 0, 1)), angle_reference: 'VectorLike | None' = None, linear_range: 'tuple[float, float]' = (0, inf), angular_range: 'tuple[float, float]' = (0, 360))", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "BallJoint", + "kind": "class", + "category": "assembly_joint", + "module": "build123d.joints", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 671, + "signature": "(label: 'str', to_part: 'Solid | Compound | None' = None, joint_location: 'Location | None' = None, angular_range: 'tuple[tuple[float, float], tuple[float, float], tuple[float, float]]' = ((0, 360), (0, 360), (0, 360)), angle_reference: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)))", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "DraftAngleError", + "kind": "class", + "category": "direct_topology", + "module": "build123d.topology.three_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1849, + "signature": "(message, face=None, problematic_shape=None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "FontManager", + "kind": "class", + "category": "utility", + "module": "build123d.text", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 65, + "signature": "()", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Export2D", + "kind": "class", + "category": "export", + "module": "build123d.exporters", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 271, + "signature": "()", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ExportDXF", + "kind": "class", + "category": "export", + "module": "build123d.exporters", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 455, + "signature": "(version: str = 'AC1027', unit: build123d.build_enums.Unit = , color: build123d.exporters.ColorIndex | None = None, line_weight: float | None = None, line_type: build123d.exporters.LineType | None = None)", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "ExportSVG", + "kind": "class", + "category": "export", + "module": "build123d.exporters", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 840, + "signature": "(unit: build123d.build_enums.Unit = , scale: float = 1, margin: float = 0, fit_to_stroke: bool = True, precision: int = 6, fill_color: str | tuple[str, float | int] | tuple[float | int, float | int, float | int] | tuple[float | int, float | int, float | int, float | int] | int | tuple[int, int] | build123d.geometry.Color | OCP.OCP.Quantity.Quantity_ColorRGBA | build123d.exporters.ColorIndex | ezdxf.colors.RGB | None | typing_extensions.sentinel = INHERIT_COLOR, line_color: str | tuple[str, float | int] | tuple[float | int, float | int, float | int] | tuple[float | int, float | int, float | int, float | int] | int | tuple[int, int] | build123d.geometry.Color | OCP.OCP.Quantity.Quantity_ColorRGBA | build123d.exporters.ColorIndex | ezdxf.colors.RGB | None | typing_extensions.sentinel = INHERIT_COLOR, line_weight: float = 0.09, line_type: build123d.exporters.LineType = , dot_length: build123d.exporters.DotLength | float = )", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "LineType", + "kind": "class", + "category": "enum", + "module": "build123d.exporters", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 165, + "signature": "(*values)", + "values": [ + "CONTINUOUS", + "BORDER", + "BORDER2", + "BORDERX2", + "CENTER", + "CENTER2", + "CENTERX2", + "DASHDOT", + "DASHDOT2", + "DASHDOTX2", + "DASHED", + "DASHED2", + "DASHEDX2", + "DIVIDE", + "DIVIDE2", + "DIVIDEX2", + "DOT", + "DOT2", + "DOTX2", + "HIDDEN", + "HIDDEN2", + "HIDDENX2", + "PHANTOM", + "PHANTOM2", + "PHANTOMX2", + "ISO_DASH", + "ISO_DASH_SPACE", + "ISO_LONG_DASH_DOT", + "ISO_LONG_DASH_DOUBLE_DOT", + "ISO_LONG_DASH_TRIPLE_DOT", + "ISO_DOT", + "ISO_LONG_DASH_SHORT_DASH", + "ISO_LONG_DASH_DOUBLE_SHORT_DASH", + "ISO_DASH_DOT", + "ISO_DOUBLE_DASH_DOT", + "ISO_DASH_DOUBLE_DOT", + "ISO_DOUBLE_DASH_DOUBLE_DOT", + "ISO_DASH_TRIPLE_DOT", + "ISO_DOUBLE_DASH_TRIPLE_DOT" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "DotLength", + "kind": "class", + "category": "enum", + "module": "build123d.exporters", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 225, + "signature": "(*values)", + "values": [ + "TRUE_DOT", + "INKSCAPE_COMPAT", + "QCAD_IMPERIAL" + ], + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "Mesher", + "kind": "class", + "category": "utility", + "module": "build123d.mesher", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 122, + "signature": "(unit: build123d.build_enums.Unit = )", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "detect_primitives", + "kind": "function", + "category": "utility", + "module": "build123d.brep_from_stl", + "conversion_status": "unverified", + "source_file": "build123d/brep_from_stl.py", + "source_line": 1850, + "signature": "(mesh: 'Shape') -> 'tuple[ShapeList[Face], ShapeList[Face], list[str]]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "import_brep", + "kind": "function", + "category": "import", + "module": "build123d.importers", + "conversion_status": "unverified", + "source_file": "build123d/importers.py", + "source_line": 108, + "signature": "(file_name: os.PathLike | str | bytes) -> build123d.topology.shape_core.Shape", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "import_dxf", + "kind": "function", + "category": "import", + "module": "build123d.import_dxf", + "conversion_status": "unverified", + "source_file": "build123d/import_dxf.py", + "source_line": 461, + "signature": "(dxf_file: str | os.PathLike | typing.TextIO | typing.BinaryIO) -> build123d.topology.shape_core.ShapeList", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "import_step", + "kind": "function", + "category": "import", + "module": "build123d.importers", + "conversion_status": "unverified", + "source_file": "build123d/importers.py", + "source_line": 132, + "signature": "(filename: os.PathLike | str | bytes) -> build123d.topology.composite.Compound", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "import_stl", + "kind": "function", + "category": "import", + "module": "build123d.importers", + "conversion_status": "unverified", + "source_file": "build123d/importers.py", + "source_line": 272, + "signature": "(file_name: os.PathLike | str | bytes, model_unit: build123d.build_enums.Unit = ) -> build123d.topology.two_d.Face", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "import_svg", + "kind": "function", + "category": "import", + "module": "build123d.importers", + "conversion_status": "unverified", + "source_file": "build123d/importers.py", + "source_line": 403, + "signature": "(svg_file: str | pathlib.Path | typing.TextIO, *, flip_y: bool = True, align: build123d.build_enums.Align | tuple[build123d.build_enums.Align, build123d.build_enums.Align] | None = , ignore_visibility: bool = False, label_by: Union[Literal['id', 'class', 'inkscape:label'], str] = 'id') -> build123d.topology.shape_core.ShapeList[build123d.topology.one_d.Wire | build123d.topology.two_d.Face]", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "import_svg_as_buildline_code", + "kind": "function", + "category": "import", + "module": "build123d.importers", + "conversion_status": "unverified", + "source_file": "build123d/importers.py", + "source_line": 327, + "signature": "(file_name: os.PathLike | str | bytes, precision: int = 6) -> tuple[str, str]", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "delta", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.utils", + "conversion_status": "unverified", + "source_file": "build123d/topology/utils.py", + "source_line": 260, + "signature": "(shapes_one: 'Iterable[Shape]', shapes_two: 'Iterable[Shape]') -> 'list[Shape]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "edges_to_wires", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.one_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4731, + "signature": "(edges: 'Iterable[Edge]', tol: 'float' = 1e-06) -> 'ShapeList[Wire]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "new_edges", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.utils", + "conversion_status": "unverified", + "source_file": "build123d/topology/utils.py", + "source_line": 301, + "signature": "(*objects: 'Shape', combined: 'Shape') -> 'ShapeList[Edge]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "pack", + "kind": "function", + "category": "utility", + "module": "build123d.pack", + "conversion_status": "unverified", + "source_file": "build123d/pack.py", + "source_line": 40, + "signature": "(objects: 'Collection[Shape]', padding: 'float', align_z: 'bool' = False) -> 'Collection[Shape]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "polar", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.utils", + "conversion_status": "unverified", + "source_file": "build123d/topology/utils.py", + "source_line": 343, + "signature": "(length: 'float', angle: 'float') -> 'tuple[float, float]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "available_fonts", + "kind": "function", + "category": "utility", + "module": "build123d.text", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 307, + "signature": "() -> list[build123d.text.FontInfo]", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "solids", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 754, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Solid]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "faces", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 720, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Face]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "wires", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 686, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Wire]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "edges", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 652, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Edge]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "vertices", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 615, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Vertex]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "solid", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 771, + "signature": "(self, select: 'Select' = ) -> 'Solid'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "face", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 737, + "signature": "(self, select: 'Select' = ) -> 'Face'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "wire", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 703, + "signature": "(self, select: 'Select' = ) -> 'Wire'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "edge", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 669, + "signature": "(self, select: 'Select' = ) -> 'Edge'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "vertex", + "kind": "function", + "category": "utility", + "module": "build123d.build_common", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 635, + "signature": "(self, select: 'Select' = ) -> 'Vertex'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "add", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "unverified", + "source_file": "typing_extensions.py", + "source_line": 204, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | build123d.topology.composite.Compound | build123d.build_common.Builder | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | build123d.topology.composite.Compound | build123d.build_common.Builder], rotation: float | build123d.geometry.Rotation | tuple[float, float, float] | None = None, clean: bool = True, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Compound", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "insert", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 79, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | build123d.topology.composite.Compound | build123d.build_common.Builder | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | build123d.topology.composite.Compound | build123d.build_common.Builder], rotation: float | build123d.geometry.Rotation | tuple[float, float, float] | None = None, clean: bool = True, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Compound", + "validation_evidence": { + "accepted_models": [ + "16_mirrored_3d_parts", + "32_variable_height_pattern", + "33_function_generated_pattern", + "38_rotated_boxes" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "bounding_box", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "unverified", + "source_file": "build123d/operations_generic.py", + "source_line": 219, + "signature": "(objects: build123d.topology.shape_core.Shape | collections.abc.Iterable[build123d.topology.shape_core.Shape] | None = None, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Sketch | build123d.topology.composite.Part", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "chamfer", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 282, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.zero_d.Vertex | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.zero_d.Vertex], length: float, length2: float | None = None, angle: float | None = None, reference: build123d.topology.one_d.Edge | build123d.topology.two_d.Face | None = None) -> build123d.topology.composite.Sketch | build123d.topology.composite.Part", + "validation_evidence": { + "accepted_models": [ + "09_fillets_and_chamfers", + "11_grid_polygon_holes", + "18_face_cutout" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "draft", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "unverified", + "source_file": "build123d/operations_part.py", + "source_line": 60, + "signature": "(faces: 'Face | Iterable[Face]', neutral_plane: 'Plane', angle: 'float') -> 'Part'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "extrude", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_part.py", + "source_line": 113, + "signature": "(to_extrude: 'Face | Sketch | None' = None, amount: 'float | None' = None, dir: 'VectorLike | None' = None, until: 'Until | None' = None, target: 'Compound | Solid | None' = None, both: 'bool' = False, taper: 'float' = 0.0, clean: 'bool' = True, mode: 'Mode' = ) -> 'Part'", + "validation_evidence": { + "accepted_models": [ + "03_prismatic_solid", + "04_lines_and_arcs_profile", + "05_offset_cutouts", + "06_point_list_holes", + "07_polygon_holes", + "08_i_beam", + "11_grid_polygon_holes", + "12_spline_profile", + "15_mirrored_profile", + "16_mirrored_3d_parts", + "17_face_mirror", + "18_face_cutout", + "19_vertex_cutouts", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "25_offset_polygon_layers", + "27_split_plate", + "28_face_located_sphere_holes", + "29_occ_bottle", + "30_bezier_profile", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern", + "34_embossed_debossed_text", + "35_curved_slots", + "36_extrude_until", + "37_positioned_sketches", + "41_pillow_block", + "42_din_rail", + "43_lego", + "45_key_cap", + "48_circuit_board" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "fillet", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 406, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.zero_d.Vertex | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.zero_d.Vertex], radius: float) -> build123d.topology.composite.Sketch | build123d.topology.composite.Part | build123d.topology.composite.Curve", + "validation_evidence": { + "accepted_models": [ + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "16_mirrored_3d_parts", + "18_face_cutout", + "29_occ_bottle", + "40_handle", + "41_pillow_block", + "42_din_rail", + "45_key_cap", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "full_round", + "kind": "function", + "category": "operation", + "module": "build123d.operations_sketch", + "conversion_status": "unverified", + "source_file": "build123d/operations_sketch.py", + "source_line": 53, + "signature": "(edge: 'Edge', invert: 'bool' = False, voronoi_point_count: 'int' = 100, mode: 'Mode' = ) -> 'Sketch'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "loft", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_part.py", + "source_line": 245, + "signature": "(sections: 'Face | Sketch | Iterable[Vertex | Face | Sketch] | None' = None, ruled: 'bool' = False, clean: 'bool' = True, mode: 'Mode' = ) -> 'Part'", + "validation_evidence": { + "accepted_models": [ + "24_circle_rectangle_loft", + "44_loft" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "make_brake_formed", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "unverified", + "source_file": "build123d/operations_part.py", + "source_line": 371, + "signature": "(thickness: 'float', station_widths: 'float | Iterable[float]', line: 'Edge | Wire | Curve | None' = None, side: 'Side' = , kind: 'Kind' = , clean: 'bool' = True, mode: 'Mode' = ) -> 'Part'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "make_face", + "kind": "function", + "category": "operation", + "module": "build123d.operations_sketch", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_sketch.py", + "source_line": 198, + "signature": "(edges: 'Edge | Wire | Curve | Iterable[Edge | Wire | Curve] | None' = None, mode: 'Mode' = ) -> 'Sketch'", + "validation_evidence": { + "accepted_models": [ + "04_lines_and_arcs_profile", + "08_i_beam", + "12_spline_profile", + "15_mirrored_profile", + "23_revolved_profile", + "29_occ_bottle", + "30_bezier_profile", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "make_hull", + "kind": "function", + "category": "operation", + "module": "build123d.operations_sketch", + "conversion_status": "unverified", + "source_file": "build123d/operations_sketch.py", + "source_line": 238, + "signature": "(edges: 'Edge | Iterable[Edge] | None' = None, mode: 'Mode' = ) -> 'Sketch'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "mirror", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 512, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.composite.Compound | build123d.topology.composite.Curve | build123d.topology.composite.Sketch | build123d.topology.composite.Part | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.composite.Compound | build123d.topology.composite.Curve | build123d.topology.composite.Sketch | build123d.topology.composite.Part] | None = None, about: build123d.geometry.Plane = Plane((0, 0, 0), (1, 0, 0), (0, -1, 0)), mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Curve | build123d.topology.composite.Sketch | build123d.topology.composite.Part | build123d.topology.composite.Compound", + "validation_evidence": { + "accepted_models": [ + "08_i_beam", + "15_mirrored_profile", + "16_mirrored_3d_parts", + "17_face_mirror", + "29_occ_bottle" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "offset", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 565, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | build123d.topology.composite.Compound | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | build123d.topology.composite.Compound] | None = None, amount: float = 0, openings: build123d.topology.two_d.Face | list[build123d.topology.two_d.Face] | None = None, kind: build123d.build_enums.Kind = , side: build123d.build_enums.Side = , closed: bool = True, min_edge_length: float | None = None, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Curve | build123d.topology.composite.Sketch | build123d.topology.composite.Part | build123d.topology.composite.Compound", + "validation_evidence": { + "accepted_models": [ + "25_offset_polygon_layers", + "26_thin_wall_box", + "29_occ_bottle", + "43_lego", + "44_loft", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "project", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "unverified", + "source_file": "build123d/operations_generic.py", + "source_line": 720, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.two_d.Face | build123d.topology.one_d.Wire | build123d.geometry.Vector | build123d.topology.zero_d.Vertex | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.two_d.Face | build123d.topology.one_d.Wire | build123d.geometry.Vector | build123d.topology.zero_d.Vertex] | None = None, workplane: build123d.geometry.Plane | None = None, target: build123d.topology.three_d.Solid | build123d.topology.composite.Compound | build123d.topology.composite.Part | None = None, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Curve | build123d.topology.composite.Sketch | build123d.topology.composite.Compound | build123d.topology.shape_core.ShapeList[build123d.geometry.Vector]", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "project_workplane", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "unverified", + "source_file": "build123d/operations_part.py", + "source_line": 481, + "signature": "(origin: 'VectorLike | Vertex', x_dir: 'VectorLike | Vertex', projection_dir: 'VectorLike', distance: 'float') -> 'Plane'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "revolve", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_part.py", + "source_line": 536, + "signature": "(profiles: 'Face | Iterable[Face] | None' = None, axis: 'Axis' = Axis((0, 0, 0), (0, 0, 1)), revolution_arc: 'float' = 360.0, clean: 'bool' = True, mode: 'Mode' = ) -> 'Part'", + "validation_evidence": { + "accepted_models": [ + "23_revolved_profile", + "36_extrude_until", + "47_vase" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "scale", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 863, + "signature": "(objects: build123d.topology.shape_core.Shape | collections.abc.Iterable[build123d.topology.shape_core.Shape] | None = None, by: float | tuple[float, float, float] = 1, about: build123d.geometry.Vector | tuple[float, float] | tuple[float, float, float] | collections.abc.Sequence[float] | None = None, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Curve | build123d.topology.composite.Sketch | build123d.topology.composite.Part | build123d.topology.composite.Compound", + "validation_evidence": { + "accepted_models": [ + "45_key_cap" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "section", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_part.py", + "source_line": 591, + "signature": "(obj: 'Part | None' = None, section_by: 'Plane | Iterable[Plane]' = Plane((0, 0, 0), (1, 0, 0), (0, -1, 0)), height: 'float' = 0.0, clean: 'bool' = True, mode: 'Mode' = ) -> 'Sketch'", + "validation_evidence": { + "accepted_models": [ + "45_key_cap" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "split", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 921, + "signature": "(objects: build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | collections.abc.Iterable[build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid] | None = None, bisect_by: build123d.geometry.Plane | build123d.topology.two_d.Face | build123d.topology.two_d.Shell = Plane((0, 0, 0), (1, 0, 0), (0, -1, 0)), keep: build123d.build_enums.Keep = , mode: build123d.build_enums.Mode = )", + "validation_evidence": { + "accepted_models": [ + "23_revolved_profile", + "27_split_plate", + "39_pegboard_j_hook" + ], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "sweep", + "kind": "function", + "category": "operation", + "module": "build123d.operations_generic", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/operations_generic.py", + "source_line": 984, + "signature": "(sections: build123d.topology.composite.Compound | build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid | collections.abc.Iterable[build123d.topology.composite.Compound | build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | build123d.topology.two_d.Face | build123d.topology.three_d.Solid] | None = None, path: build123d.topology.composite.Curve | build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | collections.abc.Iterable[build123d.topology.one_d.Edge] | None = None, multisection: bool = False, is_frenet: bool = False, transition: build123d.build_enums.Transition = , normal: build123d.geometry.Vector | tuple[float, float] | tuple[float, float, float] | collections.abc.Sequence[float] | None = None, binormal: build123d.topology.one_d.Edge | build123d.topology.one_d.Wire | None = None, clean: bool = True, mode: build123d.build_enums.Mode = ) -> build123d.topology.composite.Part | build123d.topology.composite.Sketch", + "validation_evidence": { + "accepted_models": [ + "14_swept_square_tube", + "39_pegboard_j_hook", + "40_handle", + "50_spring" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "thicken", + "kind": "function", + "category": "operation", + "module": "build123d.operations_part", + "conversion_status": "unverified", + "source_file": "build123d/operations_part.py", + "source_line": 658, + "signature": "(to_thicken: 'Face | Sketch | None' = None, amount: 'float | None' = None, normal_override: 'VectorLike | None' = None, both: 'bool' = False, clean: 'bool' = True, mode: 'Mode' = ) -> 'Part'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "trace", + "kind": "function", + "category": "operation", + "module": "build123d.operations_sketch", + "conversion_status": "unverified", + "source_file": "build123d/operations_sketch.py", + "source_line": 276, + "signature": "(lines: 'Curve | Edge | Wire | Iterable[Curve | Edge | Wire] | None' = None, line_width: 'float' = 1, mode: 'Mode' = ) -> 'Sketch'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "topo_explore_connected_edges", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.one_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4767, + "signature": "(edge: 'Edge', parent: 'Shape | None' = None, continuity: 'ContinuityLevel' = ) -> 'ShapeList[Edge]'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "topo_explore_common_vertex", + "kind": "function", + "category": "direct_topology", + "module": "build123d.topology.zero_d", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 465, + "signature": "(edge1: 'Edge | TopoDS_Edge', edge2: 'Edge | TopoDS_Edge') -> 'Vertex | None'", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "export_step", + "kind": "function", + "category": "export", + "module": "build123d.exporters3d", + "conversion_status": "unverified", + "source_file": "build123d/exporters3d.py", + "source_line": 364, + "signature": "(to_export: build123d.topology.shape_core.Shape, file_path: os.PathLike | str | bytes | _io.BytesIO | typing.BinaryIO, unit: build123d.build_enums.Unit = , write_pcurves: bool = True, precision_mode: build123d.build_enums.PrecisionMode = , *, timestamp: str | datetime.datetime | None = None) -> bool", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "export_gltf", + "kind": "function", + "category": "export", + "module": "build123d.exporters3d", + "conversion_status": "unverified", + "source_file": "build123d/exporters3d.py", + "source_line": 260, + "signature": "(to_export: build123d.topology.shape_core.Shape, file_path: os.PathLike | str | bytes, unit: build123d.build_enums.Unit = , binary: bool = False, linear_deflection: float = 0.001, angular_deflection: float = 0.1) -> bool", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "export_stl", + "kind": "function", + "category": "export", + "module": "build123d.exporters3d", + "conversion_status": "unverified", + "source_file": "build123d/exporters3d.py", + "source_line": 449, + "signature": "(to_export: build123d.topology.shape_core.Shape, file_path: os.PathLike | str | bytes, tolerance: float = 0.001, angular_tolerance: float = 0.1, ascii_format: bool = False) -> bool", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "export_brep", + "kind": "function", + "category": "export", + "module": "build123d.exporters3d", + "conversion_status": "unverified", + "source_file": "build123d/exporters3d.py", + "source_line": 238, + "signature": "(to_export: build123d.topology.shape_core.Shape, file_path: os.PathLike | str | bytes | _io.BytesIO | typing.BinaryIO) -> bool", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "export_obj", + "kind": "function", + "category": "export", + "module": "build123d.exporters3d", + "conversion_status": "unverified", + "source_file": "build123d/exporters3d.py", + "source_line": 494, + "signature": "(to_export: build123d.topology.shape_core.Shape, file_path: os.PathLike | str | bytes | _io.BytesIO | typing.BinaryIO, linear_deflection: float = 0.001, angular_deflection: float = 0.1, include_uvs: bool = True, atlas_packing: bool = True, atlas_gutter: float = 0.0) -> bool", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + }, + { + "name": "export_to_pcbway", + "kind": "function", + "category": "export", + "module": "build123d.exporters3d", + "conversion_status": "unverified", + "source_file": "build123d/exporters3d.py", + "source_line": 580, + "signature": "(to_export: build123d.topology.shape_core.Shape, unit: build123d.build_enums.Unit = , write_pcurves: bool = True, precision_mode: build123d.build_enums.PrecisionMode = ) -> str", + "validation_evidence": { + "accepted_models": [], + "rejected_or_unverified_models": [], + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified." + } + } + ], + "members": { + "build123d.topology.shape_core.Shape.__add__": { + "name": "__add__", + "owner": "build123d.topology.shape_core.Shape", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1100, + "signature": "(self, other)" + }, + "build123d.topology.shape_core.Shape.__and__": { + "name": "__and__", + "owner": "build123d.topology.shape_core.Shape", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1135, + "signature": "(self, other: 'Shape | Iterable[Shape]') -> 'None | Self | Compound'" + }, + "build123d.topology.shape_core.Shape.__rmul__": { + "name": "__rmul__", + "owner": "build123d.topology.shape_core.Shape", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1221, + "signature": "(self, other: 'Plane | Location | Iterable[Plane | Location]')" + }, + "build123d.topology.shape_core.Shape.__sub__": { + "name": "__sub__", + "owner": "build123d.topology.shape_core.Shape", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1229, + "signature": "(self, other)" + }, + "build123d.topology.shape_core.Shape.area": { + "name": "area", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 391, + "signature": "(self) -> 'float'" + }, + "build123d.topology.shape_core.Shape.as_shape": { + "name": "as_shape", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 783, + "signature": "(cls, obj: 'Shape | Vector | Location | Axis | Plane') -> 'Shape'" + }, + "build123d.topology.shape_core.Shape.bounding_box": { + "name": "bounding_box", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1327, + "signature": "(self, tolerance: 'float | None' = None, optimal: 'bool' = True) -> 'BoundBox'" + }, + "build123d.topology.shape_core.Shape.cast": { + "name": "cast", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 801, + "signature": "(cls, obj: 'TopoDS_Shape') -> 'Shape'" + }, + "build123d.topology.shape_core.Shape.clean": { + "name": "clean", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1346, + "signature": "(self) -> 'Self'" + }, + "build123d.topology.shape_core.Shape.closest_points": { + "name": "closest_points", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1374, + "signature": "(self, other: 'Shape | VectorLike') -> 'tuple[Vector, Vector]'" + }, + "build123d.topology.shape_core.Shape.color": { + "name": "color", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 401, + "signature": "(self) -> 'None | Color'" + }, + "build123d.topology.shape_core.Shape.combined_center": { + "name": "combined_center", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 909, + "signature": "(objects: 'Iterable[Shape]', center_of: 'CenterOf' = ) -> 'Vector'" + }, + "build123d.topology.shape_core.Shape.compound": { + "name": "compound", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1378, + "signature": "(self) -> 'Compound'" + }, + "build123d.topology.shape_core.Shape.compounds": { + "name": "compounds", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1382, + "signature": "(self) -> 'ShapeList[Compound]'" + }, + "build123d.topology.shape_core.Shape.compute_mass": { + "name": "compute_mass", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1288, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.shape_core.Shape.compute_volume": { + "name": "compute_volume", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1269, + "signature": "(self) -> 'float'" + }, + "build123d.topology.shape_core.Shape.convexity": { + "name": "convexity", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 469, + "signature": "(self) -> 'Convexity'" + }, + "build123d.topology.shape_core.Shape.copy_attributes_to": { + "name": "copy_attributes_to", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1386, + "signature": "(self, target: 'Shape', exceptions: 'Iterable[str] | None' = None)" + }, + "build123d.topology.shape_core.Shape.cut": { + "name": "cut", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1418, + "signature": "(self, *to_cut: 'Shape') -> 'Self | Compound'", + "validation_evidence": { + "accepted_models": [ + "02_plate_with_hole", + "10_filleted_hole", + "11_grid_polygon_holes", + "13_countersink_counterbore", + "18_face_cutout", + "19_vertex_cutouts", + "22_angled_drill_holes", + "27_split_plate", + "28_face_located_sphere_holes", + "34_embossed_debossed_text", + "35_curved_slots", + "41_pillow_block", + "42_din_rail", + "45_key_cap", + "49_holes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.distance": { + "name": "distance", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1432, + "signature": "(self, other: 'Shape') -> 'float'" + }, + "build123d.topology.shape_core.Shape.distance_to": { + "name": "distance_to", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1446, + "signature": "(self, other: 'Shape | VectorLike') -> 'float'" + }, + "build123d.topology.shape_core.Shape.distance_to_with_closest_points": { + "name": "distance_to_with_closest_points", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1450, + "signature": "(self, other: 'Shape | VectorLike') -> 'tuple[float, Vector, Vector]'" + }, + "build123d.topology.shape_core.Shape.distances": { + "name": "distances", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1475, + "signature": "(self, *others: 'Shape') -> 'Iterator[float]'" + }, + "build123d.topology.shape_core.Shape.edge": { + "name": "edge", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1498, + "signature": "(self) -> 'Edge'" + }, + "build123d.topology.shape_core.Shape.edges": { + "name": "edges", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1502, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Edge]'" + }, + "build123d.topology.shape_core.Shape.entities": { + "name": "entities", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1538, + "signature": "(self, topo_type: 'Shapes') -> 'list[TopoDS_Shape]'" + }, + "build123d.topology.shape_core.Shape.extrude": { + "name": "extrude", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 812, + "signature": "(cls, obj: 'Shape', direction: 'VectorLike') -> 'Edge | Face | Shell | Solid | Compound'" + }, + "build123d.topology.shape_core.Shape.face": { + "name": "face", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1544, + "signature": "(self) -> 'Face'" + }, + "build123d.topology.shape_core.Shape.faces": { + "name": "faces", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1548, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Face]'" + }, + "build123d.topology.shape_core.Shape.faces_intersected_by_axis": { + "name": "faces_intersected_by_axis", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1552, + "signature": "(self, axis: 'Axis', tol: 'float' = 0.0001) -> 'ShapeList[Face]'" + }, + "build123d.topology.shape_core.Shape.fix": { + "name": "fix", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1596, + "signature": "(self) -> 'Self'" + }, + "build123d.topology.shape_core.Shape.fuse": { + "name": "fuse", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1608, + "signature": "(self, *to_fuse: 'Shape', glue: 'bool' = False, tol: 'float | None' = None) -> 'Self | Compound'", + "validation_evidence": { + "accepted_models": [ + "13_countersink_counterbore", + "16_mirrored_3d_parts", + "17_face_mirror", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "24_circle_rectangle_loft", + "25_offset_polygon_layers", + "29_occ_bottle", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern", + "34_embossed_debossed_text", + "36_extrude_until", + "37_positioned_sketches", + "38_rotated_boxes", + "39_pegboard_j_hook", + "41_pillow_block", + "42_din_rail", + "43_lego", + "45_key_cap", + "49_holes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.geom_type": { + "name": "geom_type", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 486, + "signature": "(self) -> 'GeomType'" + }, + "build123d.topology.shape_core.Shape.get_shape_list": { + "name": "get_shape_list", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 997, + "signature": "(shape: 'Shape', entity_type: \"Literal['Vertex', 'Edge', 'Wire', 'Face', 'Shell', 'Solid', 'Compound']\") -> 'ShapeList'" + }, + "build123d.topology.shape_core.Shape.get_single_shape": { + "name": "get_single_shape", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1044, + "signature": "(shape: 'Shape', entity_type: \"Literal['Vertex', 'Edge', 'Wire', 'Face', 'Shell', 'Solid', 'Compound']\") -> 'Shape'" + }, + "build123d.topology.shape_core.Shape.get_top_level_shapes": { + "name": "get_top_level_shapes", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1659, + "signature": "(self) -> 'ShapeList[Shape]'" + }, + "build123d.topology.shape_core.Shape.global_location": { + "name": "global_location", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 590, + "signature": "(self) -> 'Location'" + }, + "build123d.topology.shape_core.Shape.intersect": { + "name": "intersect", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1683, + "signature": "(self, *to_intersect: 'Shape | Vector | Location | Axis | Plane', tolerance: 'float' = 1e-06, include_touched: 'bool' = False) -> 'ShapeList | None'" + }, + "build123d.topology.shape_core.Shape.is_equal": { + "name": "is_equal", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1770, + "signature": "(self, other: 'Shape') -> 'bool'" + }, + "build123d.topology.shape_core.Shape.is_manifold": { + "name": "is_manifold", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 512, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.shape_core.Shape.is_null": { + "name": "is_null", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 570, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.shape_core.Shape.is_same": { + "name": "is_same", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1785, + "signature": "(self, other: 'Shape') -> 'bool'" + }, + "build123d.topology.shape_core.Shape.is_valid": { + "name": "is_valid", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 578, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.shape_core.Shape.locate": { + "name": "locate", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1800, + "signature": "(self, loc: 'Location') -> 'Self'" + }, + "build123d.topology.shape_core.Shape.located": { + "name": "located", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1815, + "signature": "(self, loc: 'Location') -> 'Self'" + }, + "build123d.topology.shape_core.Shape.location": { + "name": "location", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 607, + "signature": "(self) -> 'Location'" + }, + "build123d.topology.shape_core.Shape.make_composite": { + "name": "make_composite", + "owner": "build123d.topology.shape_core.Shape", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1074, + "signature": "(cls, shapes: 'Iterable[Shape]', dimension: 'int | None' = None) -> 'Shape'" + }, + "build123d.topology.shape_core.Shape.material": { + "name": "material", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 425, + "signature": "(self) -> 'None | FinishedMaterial'" + }, + "build123d.topology.shape_core.Shape.matrix_of_inertia": { + "name": "matrix_of_inertia", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 620, + "signature": "(self) -> 'list[list[float]]'" + }, + "build123d.topology.shape_core.Shape.mesh": { + "name": "mesh", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1832, + "signature": "(self, tolerance: 'float', angular_tolerance: 'float' = 0.1)" + }, + "build123d.topology.shape_core.Shape.mirror": { + "name": "mirror", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1850, + "signature": "(self, mirror_plane: 'Plane | None' = None) -> 'Self'", + "validation_evidence": { + "accepted_models": [ + "16_mirrored_3d_parts", + "17_face_mirror" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.move": { + "name": "move", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1872, + "signature": "(self, loc: 'Location') -> 'Self'", + "validation_evidence": { + "accepted_models": [ + "01_rectangular_plate", + "02_plate_with_hole", + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "13_countersink_counterbore", + "18_face_cutout", + "20_offset_plane_cylinder", + "22_angled_drill_holes", + "24_circle_rectangle_loft", + "26_thin_wall_box", + "27_split_plate", + "28_face_located_sphere_holes", + "34_embossed_debossed_text", + "35_curved_slots", + "38_rotated_boxes", + "43_lego", + "45_key_cap", + "49_holes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.moved": { + "name": "moved", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1888, + "signature": "(self, loc: 'Location | Plane') -> 'Self'", + "validation_evidence": { + "accepted_models": [ + "13_countersink_counterbore", + "16_mirrored_3d_parts", + "28_face_located_sphere_holes", + "38_rotated_boxes", + "41_pillow_block", + "43_lego", + "45_key_cap", + "47_vase", + "49_holes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.orientation": { + "name": "orientation", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 664, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.shape_core.Shape.oriented_bounding_box": { + "name": "oriented_bounding_box", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1907, + "signature": "(self) -> 'OrientedBoundBox'" + }, + "build123d.topology.shape_core.Shape.position": { + "name": "position", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 679, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.shape_core.Shape.principal_properties": { + "name": "principal_properties", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 694, + "signature": "(self) -> 'list[tuple[Vector, float]]'" + }, + "build123d.topology.shape_core.Shape.project_faces": { + "name": "project_faces", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1917, + "signature": "(self, faces: 'list[Face] | Compound', path: 'Wire | Edge', start: 'float' = 0) -> 'ShapeList[Face]'" + }, + "build123d.topology.shape_core.Shape.radius_of_gyration": { + "name": "radius_of_gyration", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1987, + "signature": "(self, axis: 'Axis') -> 'float'" + }, + "build123d.topology.shape_core.Shape.register_composite_factory": { + "name": "register_composite_factory", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1066, + "signature": "(cls, dimension: 'int | None', factory: 'CompositeFactory') -> 'None'" + }, + "build123d.topology.shape_core.Shape.register_geometry_constructor": { + "name": "register_geometry_constructor", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 772, + "signature": "(cls, geometry_type: 'type', constructor: 'GeometryConstructor') -> 'None'" + }, + "build123d.topology.shape_core.Shape.register_shape_constructor": { + "name": "register_shape_constructor", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 760, + "signature": "(cls, shape_type: 'TopAbs_ShapeEnum', constructor: 'ShapeConstructor') -> 'None'" + }, + "build123d.topology.shape_core.Shape.rotate": { + "name": "rotate", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2021, + "signature": "(self, axis: 'Axis', angle: 'float', transform: 'bool' = False) -> 'Self'", + "validation_evidence": { + "accepted_models": [ + "38_rotated_boxes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.scale": { + "name": "scale", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2047, + "signature": "(self, factor: 'float | tuple[float, float, float]', about: 'VectorLike | None' = None) -> 'Self'", + "validation_evidence": { + "accepted_models": [ + "45_key_cap" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.shape_type": { + "name": "shape_type", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 724, + "signature": "(self) -> 'Shapes'" + }, + "build123d.topology.shape_core.Shape.shell": { + "name": "shell", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2107, + "signature": "(self) -> 'Shell'" + }, + "build123d.topology.shape_core.Shape.shells": { + "name": "shells", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2111, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Shell]'" + }, + "build123d.topology.shape_core.Shape.show_topology": { + "name": "show_topology", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2115, + "signature": "(self, limit_class: \"Literal['Compound', 'Edge', 'Face', 'Shell', 'Solid', 'Vertex', 'Wire']\" = 'Vertex', show_center: 'bool | None' = None) -> 'str'" + }, + "build123d.topology.shape_core.Shape.solid": { + "name": "solid", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2165, + "signature": "(self) -> 'Solid'" + }, + "build123d.topology.shape_core.Shape.solids": { + "name": "solids", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2169, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Solid]'", + "validation_evidence": { + "accepted_models": [ + "22_angled_drill_holes", + "43_lego" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.split": { + "name": "split", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2200, + "signature": "(self, tool: 'TrimmingTool', keep: 'Keep' = )", + "validation_evidence": { + "accepted_models": [ + "27_split_plate", + "39_pegboard_j_hook" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.shape_core.Shape.static_moments": { + "name": "static_moments", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 729, + "signature": "(self) -> 'tuple[float, float, float]'" + }, + "build123d.topology.shape_core.Shape.tessellate": { + "name": "tessellate", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2322, + "signature": "(self, tolerance: 'float', angular_tolerance: 'float' = 0.1) -> 'tuple[list[Vector], list[tuple[int, int, int]]]'" + }, + "build123d.topology.shape_core.Shape.tessellate_with_uvs": { + "name": "tessellate_with_uvs", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2371, + "signature": "(self, tolerance: 'float', angular_tolerance: 'float' = 0.1, atlas_packing: 'bool' = True, atlas_gutter: 'float' = 0.0) -> 'tuple[list[Vector], list[tuple[int, int, int]], list[Vector], list[tuple[float, float]]]'" + }, + "build123d.topology.shape_core.Shape.to_splines": { + "name": "to_splines", + "owner": "build123d.topology.shape_core.Shape", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2590, + "signature": "(self, degree: 'int' = 3, tolerance: 'float' = 0.001, nurbs: 'bool' = False) -> 'Self'" + }, + "build123d.topology.shape_core.Shape.topo_owner": { + "name": "topo_owner", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 359, + "signature": "(self) -> 'Shape | None'" + }, + "build123d.topology.shape_core.Shape.topo_parent": { + "name": "topo_parent", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 345, + "signature": "(self) -> 'Shape | None'" + }, + "build123d.topology.shape_core.Shape.touch": { + "name": "touch", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 1753, + "signature": "(self, other: 'Shape', tolerance: 'float' = 1e-06) -> 'ShapeList'" + }, + "build123d.topology.shape_core.Shape.transform_geometry": { + "name": "transform_geometry", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2627, + "signature": "(self, t_matrix: 'Matrix') -> 'Self'" + }, + "build123d.topology.shape_core.Shape.transform_shape": { + "name": "transform_shape", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2654, + "signature": "(self, t_matrix: 'Matrix') -> 'Self'" + }, + "build123d.topology.shape_core.Shape.transformed": { + "name": "transformed", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2677, + "signature": "(self, rotate: 'VectorLike' = (0, 0, 0), offset: 'VectorLike' = (0, 0, 0)) -> 'Self'" + }, + "build123d.topology.shape_core.Shape.translate": { + "name": "translate", + "owner": "build123d.topology.shape_core.Shape", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2706, + "signature": "(self, vector: 'VectorLike', transform: 'bool' = False) -> 'Self'" + }, + "build123d.topology.shape_core.Shape.vertex": { + "name": "vertex", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2962, + "signature": "(self) -> 'Vertex'" + }, + "build123d.topology.shape_core.Shape.vertices": { + "name": "vertices", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2966, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Vertex]'" + }, + "build123d.topology.shape_core.Shape.wire": { + "name": "wire", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2729, + "signature": "(self) -> 'Wire'" + }, + "build123d.topology.shape_core.Shape.wires": { + "name": "wires", + "owner": "build123d.topology.shape_core.Shape", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 2733, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Wire]'" + }, + "build123d.topology.shape_core.Shape.wrapped": { + "name": "wrapped", + "owner": "build123d.topology.shape_core.Shape", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 373, + "signature": "(self)" + }, + "build123d.topology.one_d.Mixin1D.__add__": { + "name": "__add__", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 682, + "signature": "(self, other: 'None | Shape | Iterable[Shape]') -> 'Edge | Wire | Curve | Self'" + }, + "build123d.topology.one_d.Mixin1D.__matmul__": { + "name": "__matmul__", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 739, + "signature": "(self, position: 'float') -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.__mod__": { + "name": "__mod__", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 743, + "signature": "(self, position: 'float') -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.__xor__": { + "name": "__xor__", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 747, + "signature": "(self, position: 'float') -> 'Location'" + }, + "build123d.topology.one_d.Mixin1D.center": { + "name": "center", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 751, + "signature": "(self, center_of: 'CenterOf' = ) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.common_plane": { + "name": "common_plane", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 775, + "signature": "(self, *lines: 'Edge | Wire | None', tolerance: 'float' = 1e-06) -> 'None | Plane'" + }, + "build123d.topology.one_d.Mixin1D.convexity": { + "name": "convexity", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 564, + "signature": "(self) -> 'Convexity'" + }, + "build123d.topology.one_d.Mixin1D.curvature_comb": { + "name": "curvature_comb", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 862, + "signature": "(self, count: 'int' = 100, max_tooth_size: 'float | None' = None) -> 'ShapeList[Edge]'" + }, + "build123d.topology.one_d.Mixin1D.derivative_at": { + "name": "derivative_at", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 945, + "signature": "(self, position: 'float | VectorLike', order: 'int' = 2, position_mode: 'PositionMode' = ) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.end_point": { + "name": "end_point", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 998, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.extrude": { + "name": "extrude", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 654, + "signature": "(cls, obj: 'Shape', direction: 'VectorLike') -> 'Edge | Face | Shell | Solid | Compound'" + }, + "build123d.topology.one_d.Mixin1D.is_closed": { + "name": "is_closed", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 550, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.one_d.Mixin1D.is_forward": { + "name": "is_forward", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 557, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.one_d.Mixin1D.is_interior": { + "name": "is_interior", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 599, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.one_d.Mixin1D.length": { + "name": "length", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 612, + "signature": "(self) -> 'float'" + }, + "build123d.topology.one_d.Mixin1D.location_at": { + "name": "location_at", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1072, + "signature": "(self, distance: 'float', position_mode: 'PositionMode' = , frame_method: 'FrameMethod' = , x_dir: 'VectorLike | None' = None) -> 'Location'" + }, + "build123d.topology.one_d.Mixin1D.locations": { + "name": "locations", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1171, + "signature": "(self, distances: 'Iterable[float]', position_mode: 'PositionMode' = , frame_method: 'FrameMethod' = , x_dir: 'VectorLike | None' = None) -> 'list[Location]'" + }, + "build123d.topology.one_d.Mixin1D.mass": { + "name": "mass", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 647, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.one_d.Mixin1D.normal": { + "name": "normal", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1200, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.offset_2d": { + "name": "offset_2d", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1234, + "signature": "(self, distance: 'float', kind: 'Kind' = , side: 'Side' = , closed: 'bool' = True) -> 'Edge | Wire'" + }, + "build123d.topology.one_d.Mixin1D.perpendicular_line": { + "name": "perpendicular_line", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1331, + "signature": "(self, length: 'float', u_value: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'Edge'" + }, + "build123d.topology.one_d.Mixin1D.position_at": { + "name": "position_at", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1356, + "signature": "(self, position: 'float', position_mode: 'PositionMode' = ) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.positions": { + "name": "positions", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1384, + "signature": "(self, distances: 'Iterable[float] | None' = None, position_mode: 'PositionMode' = , deflection: 'float | None' = None) -> 'list[Vector]'" + }, + "build123d.topology.one_d.Mixin1D.project": { + "name": "project", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1432, + "signature": "(self, face: 'Face', direction: 'VectorLike', closest: 'bool' = True) -> 'Edge | Wire | ShapeList[Edge | Wire]'" + }, + "build123d.topology.one_d.Mixin1D.project_to_viewport": { + "name": "project_to_viewport", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1479, + "signature": "(self, viewport_origin: 'VectorLike', viewport_up: 'VectorLike' = (0, 0, 1), look_at: 'VectorLike | None' = None, focus: 'float | None' = None) -> 'tuple[ShapeList[Edge], ShapeList[Edge]]'" + }, + "build123d.topology.one_d.Mixin1D.radius": { + "name": "radius", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 619, + "signature": "(self) -> 'float'" + }, + "build123d.topology.one_d.Mixin1D.start_point": { + "name": "start_point", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1577, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.tangent_angle_at": { + "name": "tangent_angle_at", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1587, + "signature": "(self, location_param: 'float' = 0.5, position_mode: 'PositionMode' = , plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'float'" + }, + "build123d.topology.one_d.Mixin1D.tangent_at": { + "name": "tangent_at", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1610, + "signature": "(self, position: 'float | VectorLike' = 0.5, position_mode: 'PositionMode' = ) -> 'Vector'" + }, + "build123d.topology.one_d.Mixin1D.volume": { + "name": "volume", + "owner": "build123d.topology.one_d.Mixin1D", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 642, + "signature": "(self) -> 'float'" + }, + "build123d.topology.two_d.Mixin2D.__neg__": { + "name": "__neg__", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 207, + "signature": "(self) -> 'Self'" + }, + "build123d.topology.two_d.Mixin2D.extrude": { + "name": "extrude", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 198, + "signature": "(cls, obj: 'Shape', direction: 'VectorLike') -> 'Edge | Face | Shell | Solid | Compound'" + }, + "build123d.topology.two_d.Mixin2D.find_intersection_points": { + "name": "find_intersection_points", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 373, + "signature": "(self, other: 'Axis', tolerance: 'float' = 1e-06) -> 'list[tuple[Vector, Vector]]'" + }, + "build123d.topology.two_d.Mixin2D.location_at": { + "name": "location_at", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 625, + "signature": "(self, *args: 'Any', **kwargs: 'Any') -> 'Location'" + }, + "build123d.topology.two_d.Mixin2D.offset": { + "name": "offset", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 629, + "signature": "(self, amount: 'float') -> 'Self'" + }, + "build123d.topology.two_d.Mixin2D.project_to_viewport": { + "name": "project_to_viewport", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 633, + "signature": "(self, viewport_origin: 'VectorLike', viewport_up: 'VectorLike' = (0, 0, 1), look_at: 'VectorLike | None' = None, focus: 'float | None' = None) -> 'tuple[ShapeList[Edge], ShapeList[Edge]]'" + }, + "build123d.topology.two_d.Mixin2D.split_by_perimeter": { + "name": "split_by_perimeter", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 240, + "signature": "(self, perimeter: 'Edge | Wire', keep: 'Keep' = )" + }, + "build123d.topology.two_d.Mixin2D.touch": { + "name": "touch", + "owner": "build123d.topology.two_d.Mixin2D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 522, + "signature": "(self, other: 'Shape', tolerance: 'float' = 1e-06, found_faces: 'ShapeList | None' = None, found_edges: 'ShapeList | None' = None) -> 'ShapeList'" + }, + "build123d.topology.three_d.Mixin3D.center": { + "name": "center", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 195, + "signature": "(self, center_of: 'CenterOf' = ) -> 'Vector'" + }, + "build123d.topology.three_d.Mixin3D.chamfer": { + "name": "chamfer", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "feature", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 222, + "signature": "(self, length: 'float', length2: 'float | None', edge_list: 'Iterable[Edge]', face: 'Face | None' = None) -> 'Solid | Part'", + "validation_evidence": { + "accepted_models": [ + "09_fillets_and_chamfers", + "11_grid_polygon_holes", + "18_face_cutout" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Mixin3D.dprism": { + "name": "dprism", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 293, + "signature": "(self, basis: 'Face | None', bounds: 'list[Face | Wire]', depth: 'float | None' = None, taper: 'float' = 0, up_to_face: 'Face | None' = None, thru_all: 'bool' = True, additive: 'bool' = True) -> 'Solid'" + }, + "build123d.topology.three_d.Mixin3D.extrude": { + "name": "extrude", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 170, + "signature": "(cls, obj: 'Shape', direction: 'VectorLike') -> 'Edge | Face | Shell | Solid | Compound'" + }, + "build123d.topology.three_d.Mixin3D.fillet": { + "name": "fillet", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "feature", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 347, + "signature": "(self, radius: 'float', edge_list: 'Iterable[Edge]') -> 'Solid | Part'", + "validation_evidence": { + "accepted_models": [ + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "18_face_cutout", + "29_occ_bottle", + "45_key_cap", + "47_vase" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Mixin3D.find_intersection_points": { + "name": "find_intersection_points", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 373, + "signature": "(self, other: 'Axis', tolerance: 'float' = 1e-06) -> 'list[tuple[Vector, Vector]]'" + }, + "build123d.topology.three_d.Mixin3D.hollow": { + "name": "hollow", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 382, + "signature": "(self, faces: 'Iterable[Face] | None', thickness: 'float', tolerance: 'float' = 0.0001, kind: 'Kind' = ) -> 'Solid'" + }, + "build123d.topology.three_d.Mixin3D.is_inside": { + "name": "is_inside", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 529, + "signature": "(self, point: 'VectorLike', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.topology.three_d.Mixin3D.max_fillet": { + "name": "max_fillet", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 548, + "signature": "(self, edge_list: 'Iterable[Edge]', tolerance=0.1, max_iterations: 'int' = 10) -> 'float'" + }, + "build123d.topology.three_d.Mixin3D.offset_3d": { + "name": "offset_3d", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "feature", + "descriptor": "method", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 626, + "signature": "(self, openings: 'Iterable[Face] | None', thickness: 'float', tolerance: 'float' = 0.0001, kind: 'Kind' = ) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "26_thin_wall_box", + "29_occ_bottle", + "44_loft", + "47_vase" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Mixin3D.project_to_viewport": { + "name": "project_to_viewport", + "owner": "build123d.topology.three_d.Mixin3D", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 692, + "signature": "(self, viewport_origin: 'VectorLike', viewport_up: 'VectorLike' = (0, 0, 1), look_at: 'VectorLike | None' = None, focus: 'float | None' = None) -> 'tuple[ShapeList[Edge], ShapeList[Edge]]'" + }, + "build123d.build_common.Builder.__add__": { + "name": "__add__", + "owner": "build123d.build_common.Builder", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 886, + "signature": "(self, _other) -> 'Self'" + }, + "build123d.build_common.Builder.__and__": { + "name": "__and__", + "owner": "build123d.build_common.Builder", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 894, + "signature": "(self, _other) -> 'Self'" + }, + "build123d.build_common.Builder.__enter__": { + "name": "__enter__", + "owner": "build123d.build_common.Builder", + "category": "context_lifecycle", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 268, + "signature": "(self) -> 'Self'" + }, + "build123d.build_common.Builder.__exit__": { + "name": "__exit__", + "owner": "build123d.build_common.Builder", + "category": "context_lifecycle", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 322, + "signature": "(self, exception_type, exception_value, traceback)" + }, + "build123d.build_common.Builder.__sub__": { + "name": "__sub__", + "owner": "build123d.build_common.Builder", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 890, + "signature": "(self, _other) -> 'Self'" + }, + "build123d.build_common.Builder.edge": { + "name": "edge", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 669, + "signature": "(self, select: 'Select' = ) -> 'Edge'" + }, + "build123d.build_common.Builder.edges": { + "name": "edges", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 652, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Edge]'" + }, + "build123d.build_common.Builder.face": { + "name": "face", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 737, + "signature": "(self, select: 'Select' = ) -> 'Face'" + }, + "build123d.build_common.Builder.faces": { + "name": "faces", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 720, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Face]'" + }, + "build123d.build_common.Builder.label": { + "name": "label", + "owner": "build123d.build_common.Builder", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 252, + "signature": "(self) -> 'str'" + }, + "build123d.build_common.Builder.lasts": { + "name": "lasts", + "owner": "build123d.build_common.Builder", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 804, + "signature": "(self) -> 'dict'" + }, + "build123d.build_common.Builder.max_dimension": { + "name": "max_dimension", + "owner": "build123d.build_common.Builder", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 247, + "signature": "(self) -> 'float'" + }, + "build123d.build_common.Builder.new_edges": { + "name": "new_edges", + "owner": "build123d.build_common.Builder", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "typing_extensions.py", + "source_line": 262, + "signature": "(self) -> 'ShapeList[Edge]'" + }, + "build123d.build_common.Builder.news": { + "name": "news", + "owner": "build123d.build_common.Builder", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 811, + "signature": "(self) -> 'dict'" + }, + "build123d.build_common.Builder.solid": { + "name": "solid", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 771, + "signature": "(self, select: 'Select' = ) -> 'Solid'" + }, + "build123d.build_common.Builder.solids": { + "name": "solids", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 754, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Solid]'" + }, + "build123d.build_common.Builder.validate_inputs": { + "name": "validate_inputs", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 837, + "signature": "(self, validating_class, objects: 'Shape | Iterable[Shape] | None' = None)" + }, + "build123d.build_common.Builder.vertex": { + "name": "vertex", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 635, + "signature": "(self, select: 'Select' = ) -> 'Vertex'" + }, + "build123d.build_common.Builder.vertices": { + "name": "vertices", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 615, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Vertex]'" + }, + "build123d.build_common.Builder.wire": { + "name": "wire", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 703, + "signature": "(self, select: 'Select' = ) -> 'Wire'" + }, + "build123d.build_common.Builder.wires": { + "name": "wires", + "owner": "build123d.build_common.Builder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 686, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Wire]'" + }, + "build123d.build_common.BaseObjectMeta.__call__": { + "name": "__call__", + "owner": "build123d.build_common.BaseObjectMeta", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 1527, + "signature": "(cls, *args, **kwargs)" + }, + "build123d.build_common.LocationList.__enter__": { + "name": "__enter__", + "owner": "build123d.build_common.LocationList", + "category": "context_lifecycle", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 939, + "signature": "(self)" + }, + "build123d.build_common.LocationList.__exit__": { + "name": "__exit__", + "owner": "build123d.build_common.LocationList", + "category": "context_lifecycle", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 966, + "signature": "(self, exception_type, exception_value, traceback)" + }, + "build123d.build_common.LocationList.locations": { + "name": "locations", + "owner": "build123d.build_common.LocationList", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_common.py", + "source_line": 930, + "signature": "(self) -> 'list[Location]'" + }, + "build123d.build_line.BuildLine.face": { + "name": "face", + "owner": "build123d.build_line.BuildLine", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_line.py", + "source_line": 109, + "signature": "(self, *args)" + }, + "build123d.build_line.BuildLine.faces": { + "name": "faces", + "owner": "build123d.build_line.BuildLine", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_line.py", + "source_line": 105, + "signature": "(self, *args)" + }, + "build123d.build_line.BuildLine.line": { + "name": "line", + "owner": "build123d.build_line.BuildLine", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_line.py", + "source_line": 80, + "signature": "(self) -> 'Curve | None'" + }, + "build123d.build_line.BuildLine.line_local": { + "name": "line_local", + "owner": "build123d.build_line.BuildLine", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_line.py", + "source_line": 90, + "signature": "(self) -> 'Curve | None'" + }, + "build123d.build_line.BuildLine.solid": { + "name": "solid", + "owner": "build123d.build_line.BuildLine", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_line.py", + "source_line": 117, + "signature": "(self, *args)" + }, + "build123d.build_line.BuildLine.solids": { + "name": "solids", + "owner": "build123d.build_line.BuildLine", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_line.py", + "source_line": 113, + "signature": "(self, *args)" + }, + "build123d.build_part.BuildPart.location": { + "name": "location", + "owner": "build123d.build_part.BuildPart", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_part.py", + "source_line": 106, + "signature": "(self) -> 'Location'" + }, + "build123d.build_part.BuildPart.part": { + "name": "part", + "owner": "build123d.build_part.BuildPart", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_part.py", + "source_line": 76, + "signature": "(self) -> 'Part | None'" + }, + "build123d.build_part.BuildPart.part_local": { + "name": "part_local", + "owner": "build123d.build_part.BuildPart", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_part.py", + "source_line": 86, + "signature": "(self) -> 'Part | None'" + }, + "build123d.build_part.BuildPart.pending_edges_as_wire": { + "name": "pending_edges_as_wire", + "owner": "build123d.build_part.BuildPart", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_part.py", + "source_line": 101, + "signature": "(self) -> 'Wire'" + }, + "build123d.build_sketch.BuildSketch.consolidate_edges": { + "name": "consolidate_edges", + "owner": "build123d.build_sketch.BuildSketch", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_sketch.py", + "source_line": 110, + "signature": "(self) -> 'Wire | list[Wire]'" + }, + "build123d.build_sketch.BuildSketch.sketch": { + "name": "sketch", + "owner": "build123d.build_sketch.BuildSketch", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_sketch.py", + "source_line": 97, + "signature": "(self)" + }, + "build123d.build_sketch.BuildSketch.sketch_local": { + "name": "sketch_local", + "owner": "build123d.build_sketch.BuildSketch", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/build_sketch.py", + "source_line": 77, + "signature": "(self) -> 'Sketch | None'" + }, + "build123d.build_sketch.BuildSketch.solid": { + "name": "solid", + "owner": "build123d.build_sketch.BuildSketch", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_sketch.py", + "source_line": 106, + "signature": "(self, *args)" + }, + "build123d.build_sketch.BuildSketch.solids": { + "name": "solids", + "owner": "build123d.build_sketch.BuildSketch", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/build_sketch.py", + "source_line": 102, + "signature": "(self, *args)" + }, + "build123d.topology.composite.Curve.__add__": { + "name": "__add__", + "owner": "build123d.topology.composite.Curve", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 682, + "signature": "(self, other: 'None | Shape | Iterable[Shape]') -> 'Edge | Wire | Curve | Self'" + }, + "build123d.topology.composite.Compound.__and__": { + "name": "__and__", + "owner": "build123d.topology.composite.Compound", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 544, + "signature": "(self, other: 'Shape | Iterable[Shape]') -> 'Compound'" + }, + "build123d.topology.composite.Curve.__matmul__": { + "name": "__matmul__", + "owner": "build123d.topology.composite.Curve", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 968, + "signature": "(self, position: 'float') -> 'Vector'" + }, + "build123d.topology.composite.Curve.__mod__": { + "name": "__mod__", + "owner": "build123d.topology.composite.Curve", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 972, + "signature": "(self, position: 'float') -> 'Vector'" + }, + "build123d.topology.composite.Compound.__sub__": { + "name": "__sub__", + "owner": "build123d.topology.composite.Compound", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 594, + "signature": "(self, other: 'None | Shape | Iterable[Shape]') -> 'Compound'" + }, + "build123d.topology.composite.Curve.__xor__": { + "name": "__xor__", + "owner": "build123d.topology.composite.Curve", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 976, + "signature": "(self, position: 'float') -> 'Location'" + }, + "build123d.topology.composite.Compound.center": { + "name": "center", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 603, + "signature": "(self, center_of: 'CenterOf' = ) -> 'Vector'" + }, + "build123d.topology.composite.Compound.compound": { + "name": "compound", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 632, + "signature": "(self) -> 'Compound'" + }, + "build123d.topology.composite.Compound.compounds": { + "name": "compounds", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 640, + "signature": "(self) -> 'ShapeList[Compound]'" + }, + "build123d.topology.composite.Compound.do_children_intersect": { + "name": "do_children_intersect", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 652, + "signature": "(self, include_parent: 'bool' = False, tolerance: 'float' = 1e-05) -> 'tuple[bool, tuple[Shape | None, Shape | None], float]'" + }, + "build123d.topology.composite.Compound.extrude": { + "name": "extrude", + "owner": "build123d.topology.composite.Compound", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 212, + "signature": "(cls, obj: 'Shell', direction: 'VectorLike') -> 'Compound'" + }, + "build123d.topology.composite.Compound.get_type": { + "name": "get_type", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 703, + "signature": "(self, obj_type: 'type[Vertex] | type[Edge] | type[Face] | type[Shell] | type[Solid] | type[Wire]') -> 'list[Vertex | Edge | Face | Shell | Solid | Wire]'" + }, + "build123d.topology.composite.Compound.make_text": { + "name": "make_text", + "owner": "build123d.topology.composite.Compound", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 266, + "signature": "(cls, txt: 'str', font_size: 'float', font: 'str' = 'Arial', font_path: 'PathLike[str] | str | None' = None, font_style: 'FontStyle' = , text_align: 'tuple[TextAlign, TextAlign]' = (, ), align: 'Align | tuple[Align, Align] | None' = None, position_on_path: 'float' = 0.0, text_path: 'Edge | Wire | None' = None, single_line_width: 'float' = 0.0) -> 'Compound'" + }, + "build123d.topology.composite.Compound.make_triad": { + "name": "make_triad", + "owner": "build123d.topology.composite.Compound", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 437, + "signature": "(cls, axes_scale: 'float') -> 'Compound'" + }, + "build123d.topology.composite.Compound.mass": { + "name": "mass", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 190, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.composite.Compound.project_to_viewport": { + "name": "project_to_viewport", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 833, + "signature": "(self, viewport_origin: 'VectorLike', viewport_up: 'VectorLike' = (0, 0, 1), look_at: 'VectorLike | None' = None, focus: 'float | None' = None) -> 'tuple[ShapeList[Edge], ShapeList[Edge]]'" + }, + "build123d.topology.composite.Compound.resolve_font": { + "name": "resolve_font", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 230, + "signature": "(font: 'str' = 'Arial', font_path: 'PathLike[str] | str | None' = None, font_style: 'FontStyle' = ) -> 'tuple[str, str, Font_SystemFont]'" + }, + "build123d.topology.composite.Compound.touch": { + "name": "touch", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 808, + "signature": "(self, other: 'Shape', tolerance: 'float' = 1e-06) -> 'ShapeList[Vertex | Edge | Face]'" + }, + "build123d.topology.composite.Compound.unwrap": { + "name": "unwrap", + "owner": "build123d.topology.composite.Compound", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 860, + "signature": "(self, fully: 'bool' = True) -> 'Self | Shape'" + }, + "build123d.topology.composite.Compound.volume": { + "name": "volume", + "owner": "build123d.topology.composite.Compound", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 184, + "signature": "(self) -> 'float'" + }, + "build123d.topology.composite.Curve.wires": { + "name": "wires", + "owner": "build123d.topology.composite.Curve", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 980, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Wire]'" + }, + "build123d.topology.one_d.Edge.arc_center": { + "name": "arc_center", + "owner": "build123d.topology.one_d.Edge", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1684, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.one_d.Edge.close": { + "name": "close", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2744, + "signature": "(self) -> 'Edge | Wire'" + }, + "build123d.topology.one_d.Edge.distribute_locations": { + "name": "distribute_locations", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2753, + "signature": "(self: 'Wire | Edge', count: 'int', start: 'float' = 0.0, stop: 'float' = 1.0, positions_only: 'bool' = False) -> 'list[Location]'" + }, + "build123d.topology.one_d.Edge.extrude": { + "name": "extrude", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1702, + "signature": "(cls, obj: 'Vertex', direction: 'VectorLike') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.find_intersection_points": { + "name": "find_intersection_points", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2836, + "signature": "(self, other: 'Axis | Edge | None' = None, tolerance: 'float' = 1e-06) -> 'ShapeList[Vector]'" + }, + "build123d.topology.one_d.Edge.find_tangent": { + "name": "find_tangent", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2920, + "signature": "(self, angle: 'float') -> 'list[float]'" + }, + "build123d.topology.one_d.Edge.geom_adaptor": { + "name": "geom_adaptor", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2980, + "signature": "(self) -> 'BRepAdaptor_Curve'" + }, + "build123d.topology.one_d.Edge.geom_equal": { + "name": "geom_equal", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2986, + "signature": "(self, other: 'Edge', tol: 'float' = 1e-06, num_interpolation_points: 'int' = 5) -> 'bool'" + }, + "build123d.topology.one_d.Edge.is_infinite": { + "name": "is_infinite", + "owner": "build123d.topology.one_d.Edge", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3497, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.one_d.Edge.make_bezier": { + "name": "make_bezier", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1722, + "signature": "(cls, *cntl_pnts: 'VectorLike', weights: 'list[float] | None' = None) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_bspline": { + "name": "make_bspline", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2566, + "signature": "(cls, control_points: 'Iterable[VectorLike]', knots: 'Iterable[float]', degree: 'int', weights: 'Iterable[float] | None' = None, periodic: 'bool' = False) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_circle": { + "name": "make_circle", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1771, + "signature": "(cls, radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), start_angle: 'float' = 360.0, end_angle: 'float' = 360, angular_direction: 'AngularDirection' = ) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_constrained_arcs": { + "name": "make_constrained_arcs", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 1942, + "signature": "(cls, *args, sagitta: 'Sagitta' = , **kwargs) -> 'ShapeList[Edge]'" + }, + "build123d.topology.one_d.Edge.make_constrained_lines": { + "name": "make_constrained_lines", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2119, + "signature": "(cls, *args, **kwargs) -> 'ShapeList[Edge]'" + }, + "build123d.topology.one_d.Edge.make_ellipse": { + "name": "make_ellipse", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2204, + "signature": "(cls, x_radius: 'float', y_radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), start_angle: 'float' = 360.0, end_angle: 'float' = 360.0, angular_direction: 'AngularDirection' = ) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_helix": { + "name": "make_helix", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2361, + "signature": "(cls, pitch: 'float', height: 'float', radius: 'float', center: 'VectorLike' = (0, 0, 0), normal: 'VectorLike' = (0, 0, 1), angle: 'float' = 0.0, lefthand: 'bool' = False) -> 'Wire'" + }, + "build123d.topology.one_d.Edge.make_hyperbola": { + "name": "make_hyperbola", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2308, + "signature": "(cls, x_radius: 'float', y_radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), start_angle: 'float' = 360.0, end_angle: 'float' = 360.0, angular_direction: 'AngularDirection' = ) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_line": { + "name": "make_line", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2425, + "signature": "(cls, point1: 'VectorLike', point2: 'VectorLike') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_mid_way": { + "name": "make_mid_way", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2443, + "signature": "(cls, first: 'Edge', second: 'Edge', middle: 'float' = 0.5) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_parabola": { + "name": "make_parabola", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2271, + "signature": "(cls, focal_length: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), start_angle: 'float' = 0.0, end_angle: 'float' = 90.0, angular_direction: 'AngularDirection' = ) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_spline": { + "name": "make_spline", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2468, + "signature": "(cls, points: 'list[VectorLike]', tangents: 'list[VectorLike] | None' = None, periodic: 'bool' = False, parameters: 'list[float] | None' = None, scale: 'bool' = True, tol: 'float' = 1e-06) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_spline_approx": { + "name": "make_spline_approx", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2645, + "signature": "(cls, points: 'list[VectorLike]', tol: 'float' = 0.001, smoothing: 'tuple[float, float, float] | None' = None, min_deg: 'int' = 1, max_deg: 'int' = 6) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_tangent_arc": { + "name": "make_tangent_arc", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2698, + "signature": "(cls, start: 'VectorLike', tangent: 'VectorLike', end: 'VectorLike') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.make_three_point_arc": { + "name": "make_three_point_arc", + "owner": "build123d.topology.one_d.Edge", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 2720, + "signature": "(cls, point1: 'VectorLike', point2: 'VectorLike', point3: 'VectorLike') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.param_at": { + "name": "param_at", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3156, + "signature": "(self, position: 'float') -> 'float'" + }, + "build123d.topology.one_d.Edge.param_at_point": { + "name": "param_at_point", + "owner": "build123d.topology.one_d.Edge", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3185, + "signature": "(self, point: 'VectorLike') -> 'float'" + }, + "build123d.topology.one_d.Edge.project_to_shape": { + "name": "project_to_shape", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3315, + "signature": "(self, target_object: 'Shape', direction: 'VectorLike | None' = None, center: 'VectorLike | None' = None) -> 'ShapeList[Edge]'" + }, + "build123d.topology.one_d.Edge.reversed": { + "name": "reversed", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3349, + "signature": "(self, reconstruct: 'bool' = False) -> 'Edge'" + }, + "build123d.topology.one_d.Edge.trim": { + "name": "trim", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3380, + "signature": "(self, start: 'float | VectorLike', end: 'float | VectorLike') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.trim_infinite": { + "name": "trim_infinite", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3502, + "signature": "(self, half_length: 'float') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.trim_to_length": { + "name": "trim_to_length", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3428, + "signature": "(self, start: 'float | VectorLike', length: 'float') -> 'Edge'" + }, + "build123d.topology.one_d.Edge.trim_to_other": { + "name": "trim_to_other", + "owner": "build123d.topology.one_d.Edge", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3472, + "signature": "(self: 'Edge', other: 'Shape | Axis | Location | Plane | VectorLike') -> 'Edge | None'" + }, + "build123d.topology.one_d.Wire.chamfer_2d": { + "name": "chamfer_2d", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4041, + "signature": "(self, distance: 'float', distance2: 'float', vertices: 'Iterable[Vertex]', edge: 'Edge | None' = None) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.close": { + "name": "close", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4111, + "signature": "(self) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.combine": { + "name": "combine", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3764, + "signature": "(cls, wires: 'Iterable[Wire | Edge]', tol: 'float' = 1e-09) -> 'ShapeList[Wire]'" + }, + "build123d.topology.one_d.Wire.edges": { + "name": "edges", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4121, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Edge]'" + }, + "build123d.topology.one_d.Wire.extrude": { + "name": "extrude", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3795, + "signature": "(cls, obj: 'Shape', direction: 'VectorLike') -> 'Wire'" + }, + "build123d.topology.one_d.Wire.fillet_2d": { + "name": "fillet_2d", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4135, + "signature": "(self, radius: 'float', vertices: 'Iterable[Vertex]') -> 'Wire'" + }, + "build123d.topology.one_d.Wire.fix_degenerate_edges": { + "name": "fix_degenerate_edges", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4203, + "signature": "(self, precision: 'float') -> 'Wire'" + }, + "build123d.topology.one_d.Wire.geom_adaptor": { + "name": "geom_adaptor", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4224, + "signature": "(self) -> 'BRepAdaptor_CompCurve'" + }, + "build123d.topology.one_d.Wire.geom_equal": { + "name": "geom_equal", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4246, + "signature": "(self, other: 'Wire', tol: 'float' = 1e-06, num_interpolation_points: 'int' = 5) -> 'bool'" + }, + "build123d.topology.one_d.Wire.make_circle": { + "name": "make_circle", + "owner": "build123d.topology.one_d.Wire", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3800, + "signature": "(cls, radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.make_convex_hull": { + "name": "make_convex_hull", + "owner": "build123d.topology.one_d.Wire", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3816, + "signature": "(cls, edges: 'Iterable[Edge]', tolerance: 'float' = 0.001) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.make_ellipse": { + "name": "make_ellipse", + "owner": "build123d.topology.one_d.Wire", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3933, + "signature": "(cls, x_radius: 'float', y_radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), start_angle: 'float' = 360.0, end_angle: 'float' = 360.0, angular_direction: 'AngularDirection' = , closed: 'bool' = True) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.make_polygon": { + "name": "make_polygon", + "owner": "build123d.topology.one_d.Wire", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3973, + "signature": "(cls, vertices: 'Iterable[VectorLike]', close: 'bool' = True) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.make_rect": { + "name": "make_rect", + "owner": "build123d.topology.one_d.Wire", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 3996, + "signature": "(cls, width: 'float', height: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'Wire'" + }, + "build123d.topology.one_d.Wire.order_chamfer_edges": { + "name": "order_chamfer_edges", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4025, + "signature": "(reference_edge: 'Edge | None', edges: 'tuple[Edge, Edge]') -> 'tuple[Edge, Edge]'" + }, + "build123d.topology.one_d.Wire.order_edges": { + "name": "order_edges", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4231, + "signature": "(self) -> 'ShapeList[Edge]'" + }, + "build123d.topology.one_d.Wire.param_at": { + "name": "param_at", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4280, + "signature": "(self, position: 'float') -> 'float'" + }, + "build123d.topology.one_d.Wire.param_at_point": { + "name": "param_at_point", + "owner": "build123d.topology.one_d.Wire", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4314, + "signature": "(self, point: 'VectorLike') -> 'float'" + }, + "build123d.topology.one_d.Wire.project_to_shape": { + "name": "project_to_shape", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4482, + "signature": "(self, target_object: 'Shape', direction: 'VectorLike | None' = None, center: 'VectorLike | None' = None) -> 'ShapeList[Wire]'" + }, + "build123d.topology.one_d.Wire.stitch": { + "name": "stitch", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4593, + "signature": "(self, other: 'Wire') -> 'Wire'" + }, + "build123d.topology.one_d.Wire.trim": { + "name": "trim", + "owner": "build123d.topology.one_d.Wire", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/one_d.py", + "source_line": 4675, + "signature": "(self: 'Wire', start: 'float | VectorLike', end: 'float | VectorLike') -> 'Wire'" + }, + "build123d.objects_curve.Airfoil.camber_line": { + "name": "camber_line", + "owner": "build123d.objects_curve.Airfoil", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 234, + "signature": "(self) -> 'Edge'" + }, + "build123d.objects_curve.Airfoil.parse_naca4": { + "name": "parse_naca4", + "owner": "build123d.objects_curve.Airfoil", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/objects_curve.py", + "source_line": 143, + "signature": "(value: 'str | float') -> 'tuple[float, float, float]'" + }, + "build123d.topology.composite.Compound.__add__": { + "name": "__add__", + "owner": "build123d.topology.composite.Compound", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/composite.py", + "source_line": 490, + "signature": "(self, other: 'None | Shape | Iterable[Shape]') -> 'Compound | Wire'" + }, + "build123d.drafting.Draft.is_metric": { + "name": "is_metric", + "owner": "build123d.drafting.Draft", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/drafting.py", + "source_line": 207, + "signature": "(self) -> bool" + }, + "build123d.geometry.BoundBox.add": { + "name": "add", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1165, + "signature": "(self, obj: 'tuple[float, float, float] | Vector | BoundBox', tol: 'float | None' = None) -> 'BoundBox'" + }, + "build123d.geometry.BoundBox.center": { + "name": "center", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1161, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.BoundBox.contains": { + "name": "contains", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1316, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.contains_properly": { + "name": "contains_properly", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1326, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.covered_by": { + "name": "covered_by", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1312, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.covers": { + "name": "covers", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1297, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.diagonal": { + "name": "diagonal", + "owner": "build123d.geometry.BoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1147, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.BoundBox.disjoint": { + "name": "disjoint", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1348, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.find_outside_box_2d": { + "name": "find_outside_box_2d", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "typing_extensions.py", + "source_line": 1204, + "signature": "(bb1: 'BoundBox', bb2: 'BoundBox') -> 'BoundBox | None'" + }, + "build123d.geometry.BoundBox.from_topo_ds": { + "name": "from_topo_ds", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1243, + "signature": "(cls, shape: 'TopoDS_Shape', tolerance: 'float | None' = None, optimal: 'bool' = True) -> 'BoundBox'" + }, + "build123d.geometry.BoundBox.intersects": { + "name": "intersects", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1342, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.is_inside": { + "name": "is_inside", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "typing_extensions.py", + "source_line": 1366, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.measure": { + "name": "measure", + "owner": "build123d.geometry.BoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1137, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.BoundBox.overlaps": { + "name": "overlaps", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1358, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.to_align_offset": { + "name": "to_align_offset", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1375, + "signature": "(self, align: 'Align2D | Align3D') -> 'Vector'" + }, + "build123d.geometry.BoundBox.touches": { + "name": "touches", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1352, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.BoundBox.within": { + "name": "within", + "owner": "build123d.geometry.BoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1338, + "signature": "(self, other: 'BoundBox', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.OrientedBoundBox.center": { + "name": "center", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2369, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.OrientedBoundBox.corners": { + "name": "corners", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2250, + "signature": "(self) -> 'list[Vector]'" + }, + "build123d.geometry.OrientedBoundBox.diagonal": { + "name": "diagonal", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2286, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.OrientedBoundBox.is_completely_inside": { + "name": "is_completely_inside", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2380, + "signature": "(self, other: 'OrientedBoundBox') -> 'bool'" + }, + "build123d.geometry.OrientedBoundBox.is_outside": { + "name": "is_outside", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2400, + "signature": "(self, point: 'Vector') -> 'bool'" + }, + "build123d.geometry.OrientedBoundBox.location": { + "name": "location", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2297, + "signature": "(self) -> 'Location'" + }, + "build123d.geometry.OrientedBoundBox.plane": { + "name": "plane", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2307, + "signature": "(self) -> 'Plane'" + }, + "build123d.geometry.OrientedBoundBox.size": { + "name": "size", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2320, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.OrientedBoundBox.wrapped": { + "name": "wrapped", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2245, + "signature": "(self)" + }, + "build123d.geometry.OrientedBoundBox.x_direction": { + "name": "x_direction", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2333, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.OrientedBoundBox.y_direction": { + "name": "y_direction", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2345, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.OrientedBoundBox.z_direction": { + "name": "z_direction", + "owner": "build123d.geometry.OrientedBoundBox", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2357, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Location.__and__": { + "name": "__and__", + "owner": "build123d.geometry.Location", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2101, + "signature": "(self, other: 'Axis | Location | Plane | VectorLike | Shape') -> 'Vector | Location | None'" + }, + "build123d.geometry.Location.__mul__": { + "name": "__mul__", + "owner": "build123d.geometry.Location", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2031, + "signature": "(self, other: 'Any') -> 'Any'" + }, + "build123d.geometry.Location.__neg__": { + "name": "__neg__", + "owner": "build123d.geometry.Location", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2097, + "signature": "(self) -> 'Location'" + }, + "build123d.geometry.Location.center": { + "name": "center", + "owner": "build123d.geometry.Location", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2107, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Location.intersect": { + "name": "intersect", + "owner": "build123d.geometry.Location", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2191, + "signature": "(self, *args, **kwargs)" + }, + "build123d.geometry.Location.inverse": { + "name": "inverse", + "owner": "build123d.geometry.Location", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2001, + "signature": "(self) -> 'Location'" + }, + "build123d.geometry.Location.mirror": { + "name": "mirror", + "owner": "build123d.geometry.Location", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2111, + "signature": "(self, mirror_plane: 'Plane') -> 'Location'" + }, + "build123d.geometry.Location.orientation": { + "name": "orientation", + "owner": "build123d.geometry.Location", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1951, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Location.position": { + "name": "position", + "owner": "build123d.geometry.Location", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1928, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Location.wrapped": { + "name": "wrapped", + "owner": "build123d.geometry.Location", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1923, + "signature": "(self) -> 'TopLoc_Location'" + }, + "build123d.geometry.Location.x_axis": { + "name": "x_axis", + "owner": "build123d.geometry.Location", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1983, + "signature": "(self) -> 'Axis'" + }, + "build123d.geometry.Location.y_axis": { + "name": "y_axis", + "owner": "build123d.geometry.Location", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1989, + "signature": "(self) -> 'Axis'" + }, + "build123d.geometry.Location.z_axis": { + "name": "z_axis", + "owner": "build123d.geometry.Location", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1995, + "signature": "(self) -> 'Axis'" + }, + "build123d.topology.shape_core.ShapeList.__add__": { + "name": "__add__", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3320, + "signature": "(self, other: 'Shape | Iterable[Shape]') -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.__and__": { + "name": "__and__", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3342, + "signature": "(self, other: 'ShapeList') -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.__sub__": { + "name": "__sub__", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3394, + "signature": "(self, other: 'ShapeList') -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.center": { + "name": "center", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3421, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.shape_core.ShapeList.compound": { + "name": "compound", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3429, + "signature": "(self) -> 'Compound'" + }, + "build123d.topology.shape_core.ShapeList.compounds": { + "name": "compounds", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3437, + "signature": "(self) -> 'ShapeList[Compound]'" + }, + "build123d.topology.shape_core.ShapeList.edge": { + "name": "edge", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3441, + "signature": "(self) -> 'Edge'" + }, + "build123d.topology.shape_core.ShapeList.edges": { + "name": "edges", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3449, + "signature": "(self) -> 'ShapeList[Edge]'" + }, + "build123d.topology.shape_core.ShapeList.expand": { + "name": "expand", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3398, + "signature": "(self) -> 'ShapeList'" + }, + "build123d.topology.shape_core.ShapeList.face": { + "name": "face", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3453, + "signature": "(self) -> 'Face'" + }, + "build123d.topology.shape_core.ShapeList.faces": { + "name": "faces", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3461, + "signature": "(self) -> 'ShapeList[Face]'" + }, + "build123d.topology.shape_core.ShapeList.filter_by": { + "name": "filter_by", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3465, + "signature": "(self, filter_by: 'Callable[[T], bool] | Axis | Plane | GeomType | Convexity | property', reverse: 'bool' = False, tolerance: 'float' = 1e-05) -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.filter_by_position": { + "name": "filter_by_position", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3610, + "signature": "(self, axis: 'Axis', minimum: 'float', maximum: 'float', inclusive: 'tuple[bool, bool]' = (True, True)) -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.first": { + "name": "first", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3308, + "signature": "(self) -> 'T'" + }, + "build123d.topology.shape_core.ShapeList.group_by": { + "name": "group_by", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3661, + "signature": "(self, group_by: 'Callable[[T], K] | Axis | Edge | Wire | SortBy | property' = Axis((0, 0, 0), (0, 0, 1)), reverse: 'bool' = False, tol_digits: 'int' = 6) -> 'GroupBy[T, K]'" + }, + "build123d.topology.shape_core.ShapeList.last": { + "name": "last", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3313, + "signature": "(self) -> 'T'" + }, + "build123d.topology.shape_core.ShapeList.shell": { + "name": "shell", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3767, + "signature": "(self) -> 'Shell'" + }, + "build123d.topology.shape_core.ShapeList.shells": { + "name": "shells", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3775, + "signature": "(self) -> 'ShapeList[Shell]'" + }, + "build123d.topology.shape_core.ShapeList.solid": { + "name": "solid", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3779, + "signature": "(self) -> 'Solid'" + }, + "build123d.topology.shape_core.ShapeList.solids": { + "name": "solids", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3787, + "signature": "(self) -> 'ShapeList[Solid]'" + }, + "build123d.topology.shape_core.ShapeList.sort_by": { + "name": "sort_by", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3791, + "signature": "(self, sort_by: 'Callable[[T], K] | Axis | Edge | Wire | SortBy | property' = Axis((0, 0, 0), (0, 0, 1)), reverse: 'bool' = False) -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.sort_by_distance": { + "name": "sort_by_distance", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3890, + "signature": "(self, other: 'Shape | VectorLike', reverse: 'bool' = False) -> 'ShapeList[T]'" + }, + "build123d.topology.shape_core.ShapeList.vertex": { + "name": "vertex", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3911, + "signature": "(self) -> 'Vertex'" + }, + "build123d.topology.shape_core.ShapeList.vertices": { + "name": "vertices", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3919, + "signature": "(self) -> 'ShapeList[Vertex]'" + }, + "build123d.topology.shape_core.ShapeList.wire": { + "name": "wire", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3923, + "signature": "(self) -> 'Wire'" + }, + "build123d.topology.shape_core.ShapeList.wires": { + "name": "wires", + "owner": "build123d.topology.shape_core.ShapeList", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3931, + "signature": "(self) -> 'ShapeList[Wire]'" + }, + "build123d.geometry.Axis.__and__": { + "name": "__and__", + "owner": "build123d.geometry.Axis", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 950, + "signature": "(self, other: 'Axis | Location | Plane | VectorLike | Shape') -> 'Vector | Location | Axis | None'" + }, + "build123d.geometry.Axis.__neg__": { + "name": "__neg__", + "owner": "build123d.geometry.Axis", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 946, + "signature": "(self) -> 'Axis'" + }, + "build123d.geometry.Axis.angle_between": { + "name": "angle_between", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 928, + "signature": "(self, other: 'Axis') -> 'float'" + }, + "build123d.geometry.Axis.direction": { + "name": "direction", + "owner": "build123d.geometry.Axis", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 757, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Axis.intersect": { + "name": "intersect", + "owner": "build123d.geometry.Axis", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1012, + "signature": "(self, *args, **kwargs)" + }, + "build123d.geometry.Axis.is_coaxial": { + "name": "is_coaxial", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 819, + "signature": "(self, other: 'Axis', angular_tolerance: 'float' = 1e-05, linear_tolerance: 'float' = 1e-05) -> 'bool'" + }, + "build123d.geometry.Axis.is_normal": { + "name": "is_normal", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 842, + "signature": "(self, other: 'Axis', angular_tolerance: 'float' = 1e-05) -> 'bool'" + }, + "build123d.geometry.Axis.is_opposite": { + "name": "is_opposite", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 857, + "signature": "(self, other: 'Axis', angular_tolerance: 'float' = 1e-05) -> 'bool'" + }, + "build123d.geometry.Axis.is_parallel": { + "name": "is_parallel", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 873, + "signature": "(self, other: 'Axis', angular_tolerance: 'float' = 1e-05) -> 'bool'" + }, + "build123d.geometry.Axis.is_skew": { + "name": "is_skew", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 889, + "signature": "(self, other: 'Axis', tolerance: 'float' = 1e-05) -> 'bool'" + }, + "build123d.geometry.Axis.located": { + "name": "located", + "owner": "build123d.geometry.Axis", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 812, + "signature": "(self, new_location: 'Location')" + }, + "build123d.geometry.Axis.location": { + "name": "location", + "owner": "build123d.geometry.Axis", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 767, + "signature": "(self) -> 'Location'" + }, + "build123d.geometry.Axis.position": { + "name": "position", + "owner": "build123d.geometry.Axis", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 747, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Axis.reverse": { + "name": "reverse", + "owner": "build123d.geometry.Axis", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 942, + "signature": "(self) -> 'Axis'" + }, + "build123d.geometry.Axis.wrapped": { + "name": "wrapped", + "owner": "build123d.geometry.Axis", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 742, + "signature": "(self)" + }, + "build123d.geometry.Color.categorical_set": { + "name": "categorical_set", + "owner": "build123d.geometry.Color", + "category": "query_or_utility", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1569, + "signature": "(cls, color_count: 'int', starting_hue: 'ColorLike | float' = 0.0, alpha: 'float | Iterable[float]' = 1.0) -> 'list[Color]'" + }, + "build123d.geometry.Vector.X": { + "name": "X", + "owner": "build123d.geometry.Vector", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 260, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.Vector.Y": { + "name": "Y", + "owner": "build123d.geometry.Vector", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 270, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.Vector.Z": { + "name": "Z", + "owner": "build123d.geometry.Vector", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 280, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.Vector.__add__": { + "name": "__add__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 334, + "signature": "(self, vec: 'VectorLike') -> 'Vector'" + }, + "build123d.geometry.Vector.__and__": { + "name": "__and__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 450, + "signature": "(self, other: 'Axis | Location | Plane | VectorLike | Shape')" + }, + "build123d.geometry.Vector.__mul__": { + "name": "__mul__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 347, + "signature": "(self, scale: 'float') -> 'Vector'" + }, + "build123d.geometry.Vector.__neg__": { + "name": "__neg__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 442, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Vector.__radd__": { + "name": "__radd__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 338, + "signature": "(self, vec: 'Vector') -> 'Vector'" + }, + "build123d.geometry.Vector.__rmul__": { + "name": "__rmul__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 355, + "signature": "(self, scale: 'float') -> 'Vector'" + }, + "build123d.geometry.Vector.__sub__": { + "name": "__sub__", + "owner": "build123d.geometry.Vector", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 319, + "signature": "(self, vec: 'VectorLike') -> 'Vector'" + }, + "build123d.geometry.Vector.add": { + "name": "add", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 323, + "signature": "(self, vec: 'VectorLike') -> 'Vector'" + }, + "build123d.geometry.Vector.center": { + "name": "center", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 367, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Vector.cross": { + "name": "cross", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 300, + "signature": "(self, vec: 'Vector') -> 'Vector'" + }, + "build123d.geometry.Vector.distance_to_plane": { + "name": "distance_to_plane", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 417, + "signature": "(self, plane: 'Plane') -> 'float'" + }, + "build123d.geometry.Vector.dot": { + "name": "dot", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 304, + "signature": "(self, vec: 'Vector') -> 'float'" + }, + "build123d.geometry.Vector.get_angle": { + "name": "get_angle", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 379, + "signature": "(self, vec: 'Vector') -> 'float'" + }, + "build123d.geometry.Vector.get_signed_angle": { + "name": "get_signed_angle", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 383, + "signature": "(self, vec: 'Vector', normal: 'Vector | None' = None) -> 'float'" + }, + "build123d.geometry.Vector.intersect": { + "name": "intersect", + "owner": "build123d.geometry.Vector", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 577, + "signature": "(self, *args, **kwargs)" + }, + "build123d.geometry.Vector.length": { + "name": "length", + "owner": "build123d.geometry.Vector", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 295, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.Vector.multiply": { + "name": "multiply", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 343, + "signature": "(self, scale: 'float') -> 'Vector'" + }, + "build123d.geometry.Vector.normalized": { + "name": "normalized", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 359, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Vector.project_to_line": { + "name": "project_to_line", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 402, + "signature": "(self, line: 'Vector') -> 'Vector'" + }, + "build123d.geometry.Vector.project_to_plane": { + "name": "project_to_plane", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 425, + "signature": "(self, plane: 'Plane') -> 'Vector'" + }, + "build123d.geometry.Vector.reverse": { + "name": "reverse", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 363, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Vector.rotate": { + "name": "rotate", + "owner": "build123d.geometry.Vector", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 543, + "signature": "(self, axis: 'Axis', angle: 'float') -> 'Vector'" + }, + "build123d.geometry.Vector.signed_distance_from_plane": { + "name": "signed_distance_from_plane", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 421, + "signature": "(self, plane: 'Plane') -> 'float'" + }, + "build123d.geometry.Vector.sub": { + "name": "sub", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 308, + "signature": "(self, vec: 'VectorLike') -> 'Vector'" + }, + "build123d.geometry.Vector.to_dir": { + "name": "to_dir", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 515, + "signature": "(self) -> 'gp_Dir'" + }, + "build123d.geometry.Vector.to_pnt": { + "name": "to_pnt", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 511, + "signature": "(self) -> 'gp_Pnt'" + }, + "build123d.geometry.Vector.transform": { + "name": "transform", + "owner": "build123d.geometry.Vector", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 519, + "signature": "(self, affine_transform: 'Matrix', is_direction: 'bool' = False) -> 'Vector'" + }, + "build123d.geometry.Vector.wrapped": { + "name": "wrapped", + "owner": "build123d.geometry.Vector", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 290, + "signature": "(self) -> 'gp_Vec'" + }, + "build123d.topology.zero_d.Vertex.X": { + "name": "X", + "owner": "build123d.topology.zero_d.Vertex", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 289, + "signature": "(self) -> 'float'" + }, + "build123d.topology.zero_d.Vertex.Y": { + "name": "Y", + "owner": "build123d.topology.zero_d.Vertex", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 294, + "signature": "(self) -> 'float'" + }, + "build123d.topology.zero_d.Vertex.Z": { + "name": "Z", + "owner": "build123d.topology.zero_d.Vertex", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 299, + "signature": "(self) -> 'float'" + }, + "build123d.topology.zero_d.Vertex.__add__": { + "name": "__add__", + "owner": "build123d.topology.zero_d.Vertex", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 350, + "signature": "(self, other: 'Vertex | Vector | tuple[float, float, float]') -> 'Vertex'" + }, + "build123d.topology.zero_d.Vertex.__and__": { + "name": "__and__", + "owner": "build123d.topology.zero_d.Vertex", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 385, + "signature": "(self, *args, **kwargs)" + }, + "build123d.topology.zero_d.Vertex.__sub__": { + "name": "__sub__", + "owner": "build123d.topology.zero_d.Vertex", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 403, + "signature": "(self, other: 'Vertex | Vector | tuple') -> 'Vertex'" + }, + "build123d.topology.zero_d.Vertex.center": { + "name": "center", + "owner": "build123d.topology.zero_d.Vertex", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 433, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.zero_d.Vertex.convexity": { + "name": "convexity", + "owner": "build123d.topology.zero_d.Vertex", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 163, + "signature": "(self) -> 'Convexity'" + }, + "build123d.topology.zero_d.Vertex.extrude": { + "name": "extrude", + "owner": "build123d.topology.zero_d.Vertex", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 306, + "signature": "(cls, obj: 'Shape', direction: 'VectorLike') -> 'Vertex'" + }, + "build123d.topology.zero_d.Vertex.mass": { + "name": "mass", + "owner": "build123d.topology.zero_d.Vertex", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 158, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.zero_d.Vertex.split": { + "name": "split", + "owner": "build123d.topology.zero_d.Vertex", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 437, + "signature": "(self, tool: 'TrimmingTool', keep: 'Keep' = )" + }, + "build123d.topology.zero_d.Vertex.transform_shape": { + "name": "transform_shape", + "owner": "build123d.topology.zero_d.Vertex", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 441, + "signature": "(self, t_matrix: 'Matrix') -> 'Vertex'" + }, + "build123d.topology.zero_d.Vertex.vertex": { + "name": "vertex", + "owner": "build123d.topology.zero_d.Vertex", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 456, + "signature": "(self) -> 'Vertex'" + }, + "build123d.topology.zero_d.Vertex.vertices": { + "name": "vertices", + "owner": "build123d.topology.zero_d.Vertex", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 460, + "signature": "(self, select: 'Select' = ) -> 'ShapeList[Vertex]'" + }, + "build123d.topology.zero_d.Vertex.volume": { + "name": "volume", + "owner": "build123d.topology.zero_d.Vertex", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/zero_d.py", + "source_line": 153, + "signature": "(self) -> 'float'" + }, + "build123d.topology.two_d.Face.area_without_holes": { + "name": "area_without_holes", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 914, + "signature": "(self) -> 'float'" + }, + "build123d.topology.two_d.Face.axes_of_symmetry": { + "name": "axes_of_symmetry", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 962, + "signature": "(self) -> 'list[Axis]'" + }, + "build123d.topology.two_d.Face.axis_of_rotation": { + "name": "axis_of_rotation", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 931, + "signature": "(self) -> 'None | Axis'" + }, + "build123d.topology.two_d.Face.center": { + "name": "center", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1952, + "signature": "(self, center_of: 'CenterOf' = ) -> 'Vector'" + }, + "build123d.topology.two_d.Face.center_location": { + "name": "center_location", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1091, + "signature": "(self) -> 'Location'" + }, + "build123d.topology.two_d.Face.chamfer_2d": { + "name": "chamfer_2d", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1985, + "signature": "(self, distance: 'float', distance2: 'float', vertices: 'Iterable[Vertex]', edge: 'Edge | None' = None) -> 'Face'" + }, + "build123d.topology.two_d.Face.convexity": { + "name": "convexity", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1126, + "signature": "(self) -> 'Convexity'" + }, + "build123d.topology.two_d.Face.extrude": { + "name": "extrude", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1328, + "signature": "(cls, obj: 'Edge', direction: 'VectorLike') -> 'Face'" + }, + "build123d.topology.two_d.Face.fillet_2d": { + "name": "fillet_2d", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2045, + "signature": "(self, radius: 'float', vertices: 'Iterable[Vertex]') -> 'Face'" + }, + "build123d.topology.two_d.Face.geom_adaptor": { + "name": "geom_adaptor", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2087, + "signature": "(self) -> 'Geom_Surface'" + }, + "build123d.topology.two_d.Face.geometry": { + "name": "geometry", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1097, + "signature": "(self) -> 'None | str'" + }, + "build123d.topology.two_d.Face.inner_wires": { + "name": "inner_wires", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2091, + "signature": "(self) -> 'ShapeList[Wire]'" + }, + "build123d.topology.two_d.Face.is_circular_concave": { + "name": "is_circular_concave", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1196, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.two_d.Face.is_circular_convex": { + "name": "is_circular_convex", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1187, + "signature": "(self) -> 'bool'" + }, + "build123d.topology.two_d.Face.is_coplanar": { + "name": "is_coplanar", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2099, + "signature": "(self, plane: 'Plane') -> 'bool'" + }, + "build123d.topology.two_d.Face.is_inside": { + "name": "is_inside", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2111, + "signature": "(self, point: 'VectorLike', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.topology.two_d.Face.is_planar": { + "name": "is_planar", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1205, + "signature": "(self) -> 'Plane | None'" + }, + "build123d.topology.two_d.Face.length": { + "name": "length", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1217, + "signature": "(self) -> 'None | float'" + }, + "build123d.topology.two_d.Face.location_at": { + "name": "location_at", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2148, + "signature": "(self, *args, **kwargs) -> 'Location'" + }, + "build123d.topology.two_d.Face.make_bezier_surface": { + "name": "make_bezier_surface", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1348, + "signature": "(cls, points: 'list[list[VectorLike]]', weights: 'list[list[float]] | None' = None) -> 'Face'" + }, + "build123d.topology.two_d.Face.make_gordon_surface": { + "name": "make_gordon_surface", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1397, + "signature": "(cls, profiles: 'Iterable[VectorLike | Edge]', guides: 'Iterable[VectorLike | Edge]', tolerance: 'float' = 0.0003) -> 'Face'" + }, + "build123d.topology.two_d.Face.make_holes": { + "name": "make_holes", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2239, + "signature": "(self, interior_wires: 'list[Wire]') -> 'Face'" + }, + "build123d.topology.two_d.Face.make_rect": { + "name": "make_rect", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1482, + "signature": "(cls, width: 'float', height: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'Face'" + }, + "build123d.topology.two_d.Face.make_surface": { + "name": "make_surface", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1555, + "signature": "(cls, exterior: 'Wire | Iterable[Edge]', surface_points: 'Iterable[VectorLike] | None' = None, interior_wires: 'Iterable[Wire] | None' = None) -> 'Face'" + }, + "build123d.topology.two_d.Face.make_surface_from_array_of_points": { + "name": "make_surface_from_array_of_points", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1647, + "signature": "(cls, points: 'list[list[VectorLike]]', tol: 'float' = 0.01, smoothing: 'tuple[float, float, float] | None' = None, min_deg: 'int' = 1, max_deg: 'int' = 3) -> 'Face'" + }, + "build123d.topology.two_d.Face.make_surface_from_curves": { + "name": "make_surface_from_curves", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1721, + "signature": "(cls, *args, **kwargs) -> 'Face'" + }, + "build123d.topology.two_d.Face.make_surface_patch": { + "name": "make_surface_patch", + "owner": "build123d.topology.two_d.Face", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1763, + "signature": "(cls, edge_face_constraints: 'Iterable[tuple[Edge, Face, ContinuityLevel]] | None' = None, edge_constraints: 'Iterable[Edge] | None' = None, point_constraints: 'Iterable[VectorLike] | None' = None) -> 'Face'" + }, + "build123d.topology.two_d.Face.mass": { + "name": "mass", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1310, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.two_d.Face.normal_at": { + "name": "normal_at", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2312, + "signature": "(self, *args, **kwargs) -> 'Vector'" + }, + "build123d.topology.two_d.Face.outer_wire": { + "name": "outer_wire", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2369, + "signature": "(self) -> 'Wire'" + }, + "build123d.topology.two_d.Face.position_at": { + "name": "position_at", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2375, + "signature": "(self, u: 'float', v: 'float') -> 'Vector'" + }, + "build123d.topology.two_d.Face.project_to_shape": { + "name": "project_to_shape", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2399, + "signature": "(self, target_object: 'Shape', direction: 'VectorLike') -> 'ShapeList[Face | Shell]'" + }, + "build123d.topology.two_d.Face.radii": { + "name": "radii", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1228, + "signature": "(self) -> 'None | tuple[float, float]'" + }, + "build123d.topology.two_d.Face.radius": { + "name": "radius", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1239, + "signature": "(self) -> 'None | float'" + }, + "build123d.topology.two_d.Face.revolve": { + "name": "revolve", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1835, + "signature": "(cls, profile: 'Edge', angle: 'float', axis: 'Axis') -> 'Face'" + }, + "build123d.topology.two_d.Face.seams": { + "name": "seams", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1248, + "signature": "(self: 'Face') -> 'ShapeList[Edge]'" + }, + "build123d.topology.two_d.Face.semi_angle": { + "name": "semi_angle", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1254, + "signature": "(self) -> 'None | float'" + }, + "build123d.topology.two_d.Face.sew_faces": { + "name": "sew_faces", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1863, + "signature": "(cls, faces: 'Iterable[Face]') -> 'list[ShapeList[Face]]'" + }, + "build123d.topology.two_d.Face.sweep": { + "name": "sweep", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1903, + "signature": "(cls, profile: 'Curve | Edge | Wire', path: 'Curve | Edge | Wire', transition=) -> 'Face'" + }, + "build123d.topology.two_d.Face.uv_face": { + "name": "uv_face", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1263, + "signature": "(self) -> 'Face'" + }, + "build123d.topology.two_d.Face.volume": { + "name": "volume", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1305, + "signature": "(self) -> 'float'" + }, + "build123d.topology.two_d.Face.width": { + "name": "width", + "owner": "build123d.topology.two_d.Face", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 1315, + "signature": "(self) -> 'None | float'" + }, + "build123d.topology.two_d.Face.wire": { + "name": "wire", + "owner": "build123d.topology.two_d.Face", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2484, + "signature": "(self) -> 'Wire'" + }, + "build123d.topology.two_d.Face.without_holes": { + "name": "without_holes", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2461, + "signature": "(self) -> 'Face'" + }, + "build123d.topology.two_d.Face.wrap": { + "name": "wrap", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2518, + "signature": "(self, planar_shape: 'T', surface_loc: 'Location', tolerance: 'float' = 0.001, extension_factor: 'float' = 0.1) -> 'T'" + }, + "build123d.topology.two_d.Face.wrap_faces": { + "name": "wrap_faces", + "owner": "build123d.topology.two_d.Face", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2577, + "signature": "(self, faces: 'Iterable[Face]', path: 'Wire | Edge', start: 'float' = 0.0) -> 'ShapeList[Face]'" + }, + "build123d.geometry.Matrix.inverse": { + "name": "inverse", + "owner": "build123d.geometry.Matrix", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2703, + "signature": "(self) -> 'Matrix'" + }, + "build123d.geometry.Matrix.multiply": { + "name": "multiply", + "owner": "build123d.geometry.Matrix", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2715, + "signature": "(self, other)" + }, + "build123d.geometry.Matrix.rotate": { + "name": "rotate", + "owner": "build123d.geometry.Matrix", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2697, + "signature": "(self, axis: 'Axis', angle: 'float')" + }, + "build123d.geometry.Matrix.transposed_list": { + "name": "transposed_list", + "owner": "build123d.geometry.Matrix", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2722, + "signature": "(self) -> 'Sequence[float]'" + }, + "build123d.topology.three_d.Solid.draft": { + "name": "draft", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1794, + "signature": "(self, faces: 'Iterable[Face]', neutral_plane: 'Plane', angle: 'float') -> 'Solid'" + }, + "build123d.topology.three_d.Solid.extrude": { + "name": "extrude", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1016, + "signature": "(cls, obj: 'Face', direction: 'VectorLike') -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "03_prismatic_solid", + "04_lines_and_arcs_profile", + "05_offset_cutouts", + "06_point_list_holes", + "07_polygon_holes", + "08_i_beam", + "11_grid_polygon_holes", + "12_spline_profile", + "15_mirrored_profile", + "16_mirrored_3d_parts", + "17_face_mirror", + "18_face_cutout", + "19_vertex_cutouts", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "25_offset_polygon_layers", + "27_split_plate", + "28_face_located_sphere_holes", + "29_occ_bottle", + "30_bezier_profile", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern", + "34_embossed_debossed_text", + "35_curved_slots", + "37_positioned_sketches", + "41_pillow_block", + "42_din_rail", + "43_lego", + "45_key_cap", + "48_circuit_board" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.extrude_linear_with_rotation": { + "name": "extrude_linear_with_rotation", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1034, + "signature": "(cls, section: 'Face | Wire', center: 'VectorLike', normal: 'VectorLike', angle: 'float', inner_wires: 'list[Wire] | None' = None) -> 'Solid'" + }, + "build123d.topology.three_d.Solid.extrude_taper": { + "name": "extrude_taper", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1128, + "signature": "(cls, profile: 'Face', direction: 'VectorLike', taper: 'float', flip_inner: 'bool' = True) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "45_key_cap" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.extrude_until": { + "name": "extrude_until", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1202, + "signature": "(cls, profile: 'Face', target: 'Compound | Solid', direction: 'VectorLike', until: 'Until' = ) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "36_extrude_until", + "45_key_cap" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.from_bounding_box": { + "name": "from_bounding_box", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1312, + "signature": "(cls, bbox: 'BoundBox | OrientedBoundBox') -> 'Solid'" + }, + "build123d.topology.three_d.Solid.make_box": { + "name": "make_box", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1320, + "signature": "(cls, length: 'float', width: 'float', height: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "01_rectangular_plate", + "02_plate_with_hole", + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "18_face_cutout", + "20_offset_plane_cylinder", + "22_angled_drill_holes", + "24_circle_rectangle_loft", + "26_thin_wall_box", + "27_split_plate", + "34_embossed_debossed_text", + "35_curved_slots", + "38_rotated_boxes", + "43_lego" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.make_cone": { + "name": "make_cone", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1348, + "signature": "(cls, base_radius: 'float', top_radius: 'float', height: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), angle: 'float' = 360) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "13_countersink_counterbore", + "49_holes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.make_cylinder": { + "name": "make_cylinder", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1383, + "signature": "(cls, radius: 'float', height: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), angle: 'float' = 360) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "02_plate_with_hole", + "10_filleted_hole", + "11_grid_polygon_holes", + "13_countersink_counterbore", + "28_face_located_sphere_holes", + "41_pillow_block", + "43_lego", + "49_holes" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.make_loft": { + "name": "make_loft", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1415, + "signature": "(cls, objs: 'Iterable[Vertex | Wire]', ruled: 'bool' = False) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "24_circle_rectangle_loft", + "44_loft" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.make_sphere": { + "name": "make_sphere", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1435, + "signature": "(cls, radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), angle1: 'float' = -90, angle2: 'float' = 90, angle3: 'float' = 360) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "28_face_located_sphere_holes", + "45_key_cap" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.make_torus": { + "name": "make_torus", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1470, + "signature": "(cls, major_radius: 'float', minor_radius: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)), start_angle: 'float' = 0, end_angle: 'float' = 360, major_angle: 'float' = 360) -> 'Solid'" + }, + "build123d.topology.three_d.Solid.make_wedge": { + "name": "make_wedge", + "owner": "build123d.topology.three_d.Solid", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1507, + "signature": "(cls, delta_x: 'float', delta_y: 'float', delta_z: 'float', min_x: 'float', min_z: 'float', max_x: 'float', max_z: 'float', plane: 'Plane' = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1))) -> 'Solid'" + }, + "build123d.topology.three_d.Solid.mass": { + "name": "mass", + "owner": "build123d.topology.three_d.Solid", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 774, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.three_d.Solid.revolve": { + "name": "revolve", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1549, + "signature": "(cls, section: 'Face | Wire', angle: 'float', axis: 'Axis', inner_wires: 'list[Wire] | None' = None) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "23_revolved_profile", + "36_extrude_until", + "47_vase" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.sweep": { + "name": "sweep", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1586, + "signature": "(cls, section: 'Face | Wire', path: 'Wire | Edge', inner_wires: 'list[Wire] | None' = None, make_solid: 'bool' = True, is_frenet: 'bool' = False, mode: 'Vector | Wire | Edge | None' = None, transition: 'Transition' = ) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "14_swept_square_tube", + "39_pegboard_j_hook", + "50_spring" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.sweep_multi": { + "name": "sweep_multi", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "verified_sampled_usage", + "source_file": "build123d/topology/three_d.py", + "source_line": 1668, + "signature": "(cls, profiles: 'Iterable[Wire | Face]', path: 'Wire | Edge', make_solid: 'bool' = True, is_frenet: 'bool' = False, binormal: 'Vector | Wire | Edge | None' = None) -> 'Solid'", + "validation_evidence": { + "accepted_models": [ + "40_handle" + ], + "scope": "Captured member call in accepted models; other argument variants remain unverified." + } + }, + "build123d.topology.three_d.Solid.thicken": { + "name": "thicken", + "owner": "build123d.topology.three_d.Solid", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 1734, + "signature": "(cls, surface: 'Face | Shell', depth: 'float', normal_override: 'VectorLike | None' = None) -> 'Solid'" + }, + "build123d.topology.three_d.Solid.touch": { + "name": "touch", + "owner": "build123d.topology.three_d.Solid", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 780, + "signature": "(self, other: 'Shape', tolerance: 'float' = 1e-06, found_solids: 'ShapeList | None' = None) -> 'ShapeList[Vertex | Edge | Face]'" + }, + "build123d.topology.three_d.Solid.volume": { + "name": "volume", + "owner": "build123d.topology.three_d.Solid", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/three_d.py", + "source_line": 767, + "signature": "(self) -> 'float'" + }, + "build123d.topology.two_d.Shell.center": { + "name": "center", + "owner": "build123d.topology.two_d.Shell", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 3015, + "signature": "(self) -> 'Vector'" + }, + "build123d.topology.two_d.Shell.extrude": { + "name": "extrude", + "owner": "build123d.topology.two_d.Shell", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2917, + "signature": "(cls, obj: 'Wire', direction: 'VectorLike') -> 'Shell'" + }, + "build123d.topology.two_d.Shell.location_at": { + "name": "location_at", + "owner": "build123d.topology.two_d.Shell", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 3021, + "signature": "(self, surface_point: 'VectorLike', *, x_dir: 'VectorLike | None' = None) -> 'Location'" + }, + "build123d.topology.two_d.Shell.make_loft": { + "name": "make_loft", + "owner": "build123d.topology.two_d.Shell", + "category": "factory", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2935, + "signature": "(cls, objs: 'Iterable[Vertex | Wire]', ruled: 'bool' = False) -> 'Shell'" + }, + "build123d.topology.two_d.Shell.mass": { + "name": "mass", + "owner": "build123d.topology.two_d.Shell", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2911, + "signature": "(self, mass_unit: 'Unit' = , length_unit: 'Unit' = ) -> 'float'" + }, + "build123d.topology.two_d.Shell.revolve": { + "name": "revolve", + "owner": "build123d.topology.two_d.Shell", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2955, + "signature": "(cls, profile: 'Curve | Wire', angle: 'float', axis: 'Axis') -> 'Face'" + }, + "build123d.topology.two_d.Shell.sweep": { + "name": "sweep", + "owner": "build123d.topology.two_d.Shell", + "category": "feature", + "descriptor": "classmethod", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2981, + "signature": "(cls, profile: 'Curve | Edge | Wire', path: 'Curve | Edge | Wire', transition=) -> 'Shell'" + }, + "build123d.topology.two_d.Shell.volume": { + "name": "volume", + "owner": "build123d.topology.two_d.Shell", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/two_d.py", + "source_line": 2899, + "signature": "(self) -> 'float'" + }, + "build123d.geometry.Plane.__and__": { + "name": "__and__", + "owner": "build123d.geometry.Plane", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3215, + "signature": "(self: 'Plane', other: 'Axis | Location | Plane | VectorLike | Shape')" + }, + "build123d.geometry.Plane.__mul__": { + "name": "__mul__", + "owner": "build123d.geometry.Plane", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3187, + "signature": "(self, other: 'Location | Plane | Iterable[Location | Plane]') -> 'Location | list[Location]'" + }, + "build123d.geometry.Plane.__neg__": { + "name": "__neg__", + "owner": "build123d.geometry.Plane", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3177, + "signature": "(self) -> 'Plane'" + }, + "build123d.geometry.Plane.__rmul__": { + "name": "__rmul__", + "owner": "build123d.geometry.Plane", + "category": "algebra_operator", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3210, + "signature": "(self, other: 'Location | Plane | Iterable[Location | Plane]') -> 'Plane | list[Plane]'" + }, + "build123d.geometry.Plane.contains": { + "name": "contains", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3511, + "signature": "(self, obj: 'VectorLike | Axis', tolerance: 'float' = 1e-06) -> 'bool'" + }, + "build123d.geometry.Plane.forward_transform": { + "name": "forward_transform", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3382, + "signature": "(self)" + }, + "build123d.geometry.Plane.from_local_coords": { + "name": "from_local_coords", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3489, + "signature": "(self, obj: 'tuple | Vector | Any | BoundBox')" + }, + "build123d.geometry.Plane.get_topods_face_normal": { + "name": "get_topods_face_normal", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 2905, + "signature": "(face: 'TopoDS_Face') -> 'Vector'" + }, + "build123d.geometry.Plane.intersect": { + "name": "intersect", + "owner": "build123d.geometry.Plane", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3590, + "signature": "(self, *args, **kwargs)" + }, + "build123d.geometry.Plane.location": { + "name": "location", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3400, + "signature": "(self) -> 'Location'" + }, + "build123d.geometry.Plane.location_between": { + "name": "location_between", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3502, + "signature": "(self, other: 'Plane') -> 'Location'" + }, + "build123d.geometry.Plane.move": { + "name": "move", + "owner": "build123d.geometry.Plane", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3370, + "signature": "(self, loc: 'Location | Plane') -> 'Plane'" + }, + "build123d.geometry.Plane.moved": { + "name": "moved", + "owner": "build123d.geometry.Plane", + "category": "transform", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3357, + "signature": "(self, loc: 'Location | Plane') -> 'Plane'" + }, + "build123d.geometry.Plane.offset": { + "name": "offset", + "owner": "build123d.geometry.Plane", + "category": "feature", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3149, + "signature": "(self, amount: 'float') -> 'Plane'" + }, + "build123d.geometry.Plane.origin": { + "name": "origin", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3244, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Plane.reverse": { + "name": "reverse", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3240, + "signature": "(self) -> 'Plane'" + }, + "build123d.geometry.Plane.reverse_transform": { + "name": "reverse_transform", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3391, + "signature": "(self)" + }, + "build123d.geometry.Plane.rotated": { + "name": "rotated", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3320, + "signature": "(self, rotation: 'VectorLike' = (0, 0, 0), ordering: 'Extrinsic | Intrinsic | None' = None) -> 'Plane'" + }, + "build123d.geometry.Plane.shift_origin": { + "name": "shift_origin", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3283, + "signature": "(self, locator: 'Axis | VectorLike | Vertex') -> 'Plane'" + }, + "build123d.geometry.Plane.to_gp_ax2": { + "name": "to_gp_ax2", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3409, + "signature": "(self) -> 'gp_Ax2'" + }, + "build123d.geometry.Plane.to_gp_ax3": { + "name": "to_gp_ax3", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3405, + "signature": "(self) -> 'gp_Ax3'" + }, + "build123d.geometry.Plane.to_local_coords": { + "name": "to_local_coords", + "owner": "build123d.geometry.Plane", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3476, + "signature": "(self, obj: 'VectorLike | Any | BoundBox')" + }, + "build123d.geometry.Plane.wrapped": { + "name": "wrapped", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3134, + "signature": "(self) -> 'gp_Pln'" + }, + "build123d.geometry.Plane.x_dir": { + "name": "x_dir", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3259, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Plane.y_dir": { + "name": "y_dir", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3271, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.Plane.z_dir": { + "name": "z_dir", + "owner": "build123d.geometry.Plane", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 3254, + "signature": "(self) -> 'Vector'" + }, + "build123d.geometry.GeomEncoder.default": { + "name": "default", + "owner": "build123d.geometry.GeomEncoder", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1714, + "signature": "(self, o)" + }, + "build123d.geometry.GeomEncoder.geometry_hook": { + "name": "geometry_hook", + "owner": "build123d.geometry.GeomEncoder", + "category": "query_or_utility", + "descriptor": "staticmethod", + "conversion_status": "unverified", + "source_file": "build123d/geometry.py", + "source_line": 1730, + "signature": "(json_dict)" + }, + "build123d.topology.shape_core.Joint.connect_to": { + "name": "connect_to", + "owner": "build123d.topology.shape_core.Joint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3986, + "signature": "(self, *args, **kwargs)" + }, + "build123d.topology.shape_core.Joint.location": { + "name": "location", + "owner": "build123d.topology.shape_core.Joint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3974, + "signature": "(self) -> 'Location'" + }, + "build123d.topology.shape_core.Joint.relative_to": { + "name": "relative_to", + "owner": "build123d.topology.shape_core.Joint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3990, + "signature": "(self, *args, **kwargs) -> 'Location'" + }, + "build123d.topology.shape_core.Joint.symbol": { + "name": "symbol", + "owner": "build123d.topology.shape_core.Joint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/topology/shape_core.py", + "source_line": 3979, + "signature": "(self) -> 'Compound'" + }, + "build123d.joints.RigidJoint.connect_to": { + "name": "connect_to", + "owner": "build123d.joints.RigidJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 130, + "signature": "(self, other: 'Joint', **kwargs)" + }, + "build123d.joints.RigidJoint.location": { + "name": "location", + "owner": "build123d.joints.RigidJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 68, + "signature": "(self) -> 'Location'" + }, + "build123d.joints.RigidJoint.relative_to": { + "name": "relative_to", + "owner": "build123d.joints.RigidJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 169, + "signature": "(self, other: 'Joint', **kwargs) -> 'Location'" + }, + "build123d.joints.RigidJoint.symbol": { + "name": "symbol", + "owner": "build123d.joints.RigidJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 73, + "signature": "(self) -> 'Compound'" + }, + "build123d.joints.RevoluteJoint.connect_to": { + "name": "connect_to", + "owner": "build123d.joints.RevoluteJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 291, + "signature": "(self, other: 'RigidJoint', *, angle: 'float | None' = None)" + }, + "build123d.joints.RevoluteJoint.location": { + "name": "location", + "owner": "build123d.joints.RevoluteJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 242, + "signature": "(self) -> 'Location'" + }, + "build123d.joints.RevoluteJoint.relative_to": { + "name": "relative_to", + "owner": "build123d.joints.RevoluteJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 304, + "signature": "(self, other: 'RigidJoint', *, angle: 'float | None' = None)" + }, + "build123d.joints.RevoluteJoint.symbol": { + "name": "symbol", + "owner": "build123d.joints.RevoluteJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 247, + "signature": "(self) -> 'Compound'" + }, + "build123d.joints.LinearJoint.connect_to": { + "name": "connect_to", + "owner": "build123d.joints.LinearJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 423, + "signature": "(self, other: 'Joint', **kwargs)" + }, + "build123d.joints.LinearJoint.location": { + "name": "location", + "owner": "build123d.joints.LinearJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 367, + "signature": "(self) -> 'Location'" + }, + "build123d.joints.LinearJoint.relative_to": { + "name": "relative_to", + "owner": "build123d.joints.LinearJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 452, + "signature": "(self, other, **kwargs)" + }, + "build123d.joints.LinearJoint.symbol": { + "name": "symbol", + "owner": "build123d.joints.LinearJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 372, + "signature": "(self) -> 'Compound'" + }, + "build123d.joints.CylindricalJoint.connect_to": { + "name": "connect_to", + "owner": "build123d.joints.CylindricalJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 601, + "signature": "(self, other: 'RigidJoint', *, position: 'float | None' = None, angle: 'float | None' = None)" + }, + "build123d.joints.CylindricalJoint.location": { + "name": "location", + "owner": "build123d.joints.CylindricalJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 547, + "signature": "(self) -> 'Location'" + }, + "build123d.joints.CylindricalJoint.relative_to": { + "name": "relative_to", + "owner": "build123d.joints.CylindricalJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 622, + "signature": "(self, other: 'RigidJoint', *, position: 'float | None' = None, angle: 'float | None' = None)" + }, + "build123d.joints.CylindricalJoint.symbol": { + "name": "symbol", + "owner": "build123d.joints.CylindricalJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 552, + "signature": "(self) -> 'Compound'" + }, + "build123d.joints.BallJoint.connect_to": { + "name": "connect_to", + "owner": "build123d.joints.BallJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 753, + "signature": "(self, other: 'RigidJoint', *, angles: 'RotationLike | None' = None)" + }, + "build123d.joints.BallJoint.location": { + "name": "location", + "owner": "build123d.joints.BallJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 695, + "signature": "(self) -> 'Location'" + }, + "build123d.joints.BallJoint.relative_to": { + "name": "relative_to", + "owner": "build123d.joints.BallJoint", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 767, + "signature": "(self, other: 'RigidJoint', *, angles: 'RotationLike | None' = None)" + }, + "build123d.joints.BallJoint.symbol": { + "name": "symbol", + "owner": "build123d.joints.BallJoint", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/joints.py", + "source_line": 700, + "signature": "(self) -> 'Compound'" + }, + "build123d.text.FontManager.available_fonts": { + "name": "available_fonts", + "owner": "build123d.text.FontManager", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 111, + "signature": "(self) -> list[build123d.text.FontInfo]" + }, + "build123d.text.FontManager.check_font": { + "name": "check_font", + "owner": "build123d.text.FontManager", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 132, + "signature": "(self, path: str) -> OCP.OCP.Font.Font_SystemFont | None" + }, + "build123d.text.FontManager.find_font": { + "name": "find_font", + "owner": "build123d.text.FontManager", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 136, + "signature": "(self, name: str, style: build123d.build_enums.FontStyle) -> OCP.OCP.Font.Font_SystemFont" + }, + "build123d.text.FontManager.register_folder": { + "name": "register_folder", + "owner": "build123d.text.FontManager", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 173, + "signature": "(self, path: str, override: bool = False, single_stroke=False) -> list[str]" + }, + "build123d.text.FontManager.register_font": { + "name": "register_font", + "owner": "build123d.text.FontManager", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 140, + "signature": "(self, path: str, override: bool = False, single_stroke=False) -> list[str]" + }, + "build123d.text.FontManager.register_system_fonts": { + "name": "register_system_fonts", + "owner": "build123d.text.FontManager", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/text.py", + "source_line": 187, + "signature": "(self)" + }, + "build123d.exporters.ExportDXF.add_layer": { + "name": "add_layer", + "owner": "build123d.exporters.ExportDXF", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 544, + "signature": "(self, name: str, *, color: build123d.exporters.ColorIndex | None = None, line_weight: float | None = None, line_type: build123d.exporters.LineType | None = None) -> Self" + }, + "build123d.exporters.ExportDXF.add_shape": { + "name": "add_shape", + "owner": "build123d.exporters.ExportDXF", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 607, + "signature": "(self, shape: build123d.topology.shape_core.Shape | collections.abc.Iterable[build123d.topology.shape_core.Shape], layer: str = '') -> Self" + }, + "build123d.exporters.ExportDXF.write": { + "name": "write", + "owner": "build123d.exporters.ExportDXF", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 643, + "signature": "(self, file_name: os.PathLike | str | bytes | _io.BytesIO, ascii_format: bool = True)" + }, + "build123d.exporters.ExportSVG.add_layer": { + "name": "add_layer", + "owner": "build123d.exporters.ExportSVG", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 1028, + "signature": "(self, name: str, *, fill_color: str | tuple[str, float | int] | tuple[float | int, float | int, float | int] | tuple[float | int, float | int, float | int, float | int] | int | tuple[int, int] | build123d.geometry.Color | OCP.OCP.Quantity.Quantity_ColorRGBA | build123d.exporters.ColorIndex | ezdxf.colors.RGB | None | typing_extensions.sentinel = INHERIT_COLOR, line_color: str | tuple[str, float | int] | tuple[float | int, float | int, float | int] | tuple[float | int, float | int, float | int, float | int] | int | tuple[int, int] | build123d.geometry.Color | OCP.OCP.Quantity.Quantity_ColorRGBA | build123d.exporters.ColorIndex | ezdxf.colors.RGB | None | typing_extensions.sentinel = INHERIT_COLOR, line_weight: float = 0.09, line_type: build123d.exporters.LineType = ) -> Self" + }, + "build123d.exporters.ExportSVG.add_shape": { + "name": "add_shape", + "owner": "build123d.exporters.ExportSVG", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 1077, + "signature": "(self, shape: build123d.topology.shape_core.Shape | collections.abc.Iterable[build123d.topology.shape_core.Shape], layer: str = '', reverse_wires: bool = False)" + }, + "build123d.exporters.ExportSVG.write": { + "name": "write", + "owner": "build123d.exporters.ExportSVG", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/exporters.py", + "source_line": 1554, + "signature": "(self, path: os.PathLike | str | bytes | _io.BytesIO)" + }, + "build123d.mesher.Mesher.add_code_to_metadata": { + "name": "add_code_to_metadata", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 217, + "signature": "(self)" + }, + "build123d.mesher.Mesher.add_meta_data": { + "name": "add_meta_data", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 188, + "signature": "(self, name_space: str, name: str, value: str, metadata_type: str, must_preserve: bool)" + }, + "build123d.mesher.Mesher.add_shape": { + "name": "add_shape", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 369, + "signature": "(self, shape: build123d.topology.shape_core.Shape | collections.abc.Iterable[build123d.topology.shape_core.Shape], linear_deflection: float = 0.001, angular_deflection: float = 0.1, mesh_type: build123d.build_enums.MeshType = , part_number: str | None = None, uuid_value: uuid.UUID | None = None)" + }, + "build123d.mesher.Mesher.get_mesh_properties": { + "name": "get_mesh_properties", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 256, + "signature": "(self) -> list[dict]" + }, + "build123d.mesher.Mesher.get_meta_data": { + "name": "get_meta_data", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 233, + "signature": "(self) -> list[dict]" + }, + "build123d.mesher.Mesher.get_meta_data_by_key": { + "name": "get_meta_data_by_key", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 247, + "signature": "(self, name_space: str, name: str) -> dict" + }, + "build123d.mesher.Mesher.library_version": { + "name": "library_version", + "owner": "build123d.mesher.Mesher", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 182, + "signature": "(self) -> str" + }, + "build123d.mesher.Mesher.mesh_count": { + "name": "mesh_count", + "owner": "build123d.mesher.Mesher", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 176, + "signature": "(self) -> int" + }, + "build123d.mesher.Mesher.model_unit": { + "name": "model_unit", + "owner": "build123d.mesher.Mesher", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 161, + "signature": "(self) -> build123d.build_enums.Unit" + }, + "build123d.mesher.Mesher.read": { + "name": "read", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 516, + "signature": "(self, file_name: os.PathLike | str | bytes) -> list[build123d.topology.shape_core.Shape]" + }, + "build123d.mesher.Mesher.triangle_counts": { + "name": "triangle_counts", + "owner": "build123d.mesher.Mesher", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 166, + "signature": "(self) -> list[int]" + }, + "build123d.mesher.Mesher.vertex_counts": { + "name": "vertex_counts", + "owner": "build123d.mesher.Mesher", + "category": "property", + "descriptor": "property", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 171, + "signature": "(self) -> list[int]" + }, + "build123d.mesher.Mesher.write": { + "name": "write", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 560, + "signature": "(self, file_name: os.PathLike | str | bytes)" + }, + "build123d.mesher.Mesher.write_stream": { + "name": "write_stream", + "owner": "build123d.mesher.Mesher", + "category": "query_or_utility", + "descriptor": "method", + "conversion_status": "unverified", + "source_file": "build123d/mesher.py", + "source_line": 576, + "signature": "(self, stream: _io.BytesIO, file_type: Literal['3mf', 'stl'])" + } + }, + "class_members": { + "build123d.topology.shape_core.Shape": [ + "build123d.topology.shape_core.Shape.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.shape_core.Shape.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.one_d.Mixin1D": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Mixin1D.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.two_d.Mixin2D": [ + "build123d.topology.shape_core.Shape.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.two_d.Mixin2D.__neg__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.two_d.Mixin2D.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.two_d.Mixin2D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.two_d.Mixin2D.location_at", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.two_d.Mixin2D.offset", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.two_d.Mixin2D.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.two_d.Mixin2D.split_by_perimeter", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.two_d.Mixin2D.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.three_d.Mixin3D": [ + "build123d.topology.shape_core.Shape.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.three_d.Mixin3D.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.three_d.Mixin3D.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.three_d.Mixin3D.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.build_common.Builder": [ + "build123d.build_common.Builder.__add__", + "build123d.build_common.Builder.__and__", + "build123d.build_common.Builder.__enter__", + "build123d.build_common.Builder.__exit__", + "build123d.build_common.Builder.__sub__", + "build123d.build_common.Builder.edge", + "build123d.build_common.Builder.edges", + "build123d.build_common.Builder.face", + "build123d.build_common.Builder.faces", + "build123d.build_common.Builder.label", + "build123d.build_common.Builder.lasts", + "build123d.build_common.Builder.max_dimension", + "build123d.build_common.Builder.new_edges", + "build123d.build_common.Builder.news", + "build123d.build_common.Builder.solid", + "build123d.build_common.Builder.solids", + "build123d.build_common.Builder.validate_inputs", + "build123d.build_common.Builder.vertex", + "build123d.build_common.Builder.vertices", + "build123d.build_common.Builder.wire", + "build123d.build_common.Builder.wires" + ], + "build123d.build_common.BaseObjectMeta": [ + "build123d.build_common.BaseObjectMeta.__call__" + ], + "build123d.build_enums.Align": [], + "build123d.build_enums.ApproxOption": [], + "build123d.build_enums.AngularDirection": [], + "build123d.build_enums.CenterOf": [], + "build123d.build_enums.ContinuityLevel": [], + "build123d.build_enums.Convexity": [], + "build123d.build_enums.Extrinsic": [], + "build123d.build_enums.FontStyle": [], + "build123d.build_enums.FrameMethod": [], + "build123d.build_enums.GeomType": [], + "build123d.build_enums.HeadType": [], + "build123d.build_enums.Intrinsic": [], + "build123d.build_enums.Keep": [], + "build123d.build_enums.Kind": [], + "build123d.build_enums.Sagitta": [], + "build123d.build_enums.LengthMode": [], + "build123d.build_enums.MeshType": [], + "build123d.build_enums.Mode": [], + "build123d.build_enums.NumberDisplay": [], + "build123d.build_enums.PageSize": [], + "build123d.build_enums.Tangency": [], + "build123d.build_enums.PositionMode": [], + "build123d.build_enums.PrecisionMode": [], + "build123d.build_enums.Select": [], + "build123d.build_enums.Side": [], + "build123d.build_enums.SortBy": [], + "build123d.build_enums.TextAlign": [], + "build123d.build_enums.Transition": [], + "build123d.build_enums.Unit": [], + "build123d.build_enums.Until": [], + "build123d.build_common.HexLocations": [ + "build123d.build_common.LocationList.__enter__", + "build123d.build_common.LocationList.__exit__", + "build123d.build_common.LocationList.locations" + ], + "build123d.build_common.PolarLocations": [ + "build123d.build_common.LocationList.__enter__", + "build123d.build_common.LocationList.__exit__", + "build123d.build_common.LocationList.locations" + ], + "build123d.build_common.Locations": [ + "build123d.build_common.LocationList.__enter__", + "build123d.build_common.LocationList.__exit__", + "build123d.build_common.LocationList.locations" + ], + "build123d.build_common.GridLocations": [ + "build123d.build_common.LocationList.__enter__", + "build123d.build_common.LocationList.__exit__", + "build123d.build_common.LocationList.locations" + ], + "build123d.build_line.BuildLine": [ + "build123d.build_common.Builder.__add__", + "build123d.build_common.Builder.__and__", + "build123d.build_common.Builder.__enter__", + "build123d.build_common.Builder.__exit__", + "build123d.build_common.Builder.__sub__", + "build123d.build_common.Builder.edge", + "build123d.build_common.Builder.edges", + "build123d.build_line.BuildLine.face", + "build123d.build_line.BuildLine.faces", + "build123d.build_common.Builder.label", + "build123d.build_common.Builder.lasts", + "build123d.build_line.BuildLine.line", + "build123d.build_line.BuildLine.line_local", + "build123d.build_common.Builder.max_dimension", + "build123d.build_common.Builder.new_edges", + "build123d.build_common.Builder.news", + "build123d.build_line.BuildLine.solid", + "build123d.build_line.BuildLine.solids", + "build123d.build_common.Builder.validate_inputs", + "build123d.build_common.Builder.vertex", + "build123d.build_common.Builder.vertices", + "build123d.build_common.Builder.wire", + "build123d.build_common.Builder.wires" + ], + "build123d.build_part.BuildPart": [ + "build123d.build_common.Builder.__add__", + "build123d.build_common.Builder.__and__", + "build123d.build_common.Builder.__enter__", + "build123d.build_common.Builder.__exit__", + "build123d.build_common.Builder.__sub__", + "build123d.build_common.Builder.edge", + "build123d.build_common.Builder.edges", + "build123d.build_common.Builder.face", + "build123d.build_common.Builder.faces", + "build123d.build_common.Builder.label", + "build123d.build_common.Builder.lasts", + "build123d.build_part.BuildPart.location", + "build123d.build_common.Builder.max_dimension", + "build123d.build_common.Builder.new_edges", + "build123d.build_common.Builder.news", + "build123d.build_part.BuildPart.part", + "build123d.build_part.BuildPart.part_local", + "build123d.build_part.BuildPart.pending_edges_as_wire", + "build123d.build_common.Builder.solid", + "build123d.build_common.Builder.solids", + "build123d.build_common.Builder.validate_inputs", + "build123d.build_common.Builder.vertex", + "build123d.build_common.Builder.vertices", + "build123d.build_common.Builder.wire", + "build123d.build_common.Builder.wires" + ], + "build123d.build_sketch.BuildSketch": [ + "build123d.build_common.Builder.__add__", + "build123d.build_common.Builder.__and__", + "build123d.build_common.Builder.__enter__", + "build123d.build_common.Builder.__exit__", + "build123d.build_common.Builder.__sub__", + "build123d.build_sketch.BuildSketch.consolidate_edges", + "build123d.build_common.Builder.edge", + "build123d.build_common.Builder.edges", + "build123d.build_common.Builder.face", + "build123d.build_common.Builder.faces", + "build123d.build_common.Builder.label", + "build123d.build_common.Builder.lasts", + "build123d.build_common.Builder.max_dimension", + "build123d.build_common.Builder.new_edges", + "build123d.build_common.Builder.news", + "build123d.build_sketch.BuildSketch.sketch", + "build123d.build_sketch.BuildSketch.sketch_local", + "build123d.build_sketch.BuildSketch.solid", + "build123d.build_sketch.BuildSketch.solids", + "build123d.build_common.Builder.validate_inputs", + "build123d.build_common.Builder.vertex", + "build123d.build_common.Builder.vertices", + "build123d.build_common.Builder.wire", + "build123d.build_common.Builder.wires" + ], + "build123d.objects_curve.BaseCurveObject": [ + "build123d.topology.composite.Curve.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.composite.Curve.__matmul__", + "build123d.topology.composite.Curve.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.composite.Curve.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.composite.Curve.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.BaseEdgeObject": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.BaseLineObject": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.one_d.Wire.chamfer_2d", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Wire.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.one_d.Wire.combine", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.one_d.Wire.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Wire.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Wire.fillet_2d", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.one_d.Wire.fix_degenerate_edges", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Wire.geom_adaptor", + "build123d.topology.one_d.Wire.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Wire.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Wire.make_convex_hull", + "build123d.topology.one_d.Wire.make_ellipse", + "build123d.topology.one_d.Wire.make_polygon", + "build123d.topology.one_d.Wire.make_rect", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.one_d.Wire.order_chamfer_edges", + "build123d.topology.one_d.Wire.order_edges", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Wire.param_at", + "build123d.topology.one_d.Wire.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Wire.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Wire.stitch", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Wire.trim", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.Airfoil": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.objects_curve.Airfoil.camber_line", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.one_d.Wire.chamfer_2d", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Wire.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.one_d.Wire.combine", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.one_d.Wire.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Wire.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Wire.fillet_2d", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.one_d.Wire.fix_degenerate_edges", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Wire.geom_adaptor", + "build123d.topology.one_d.Wire.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Wire.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Wire.make_convex_hull", + "build123d.topology.one_d.Wire.make_ellipse", + "build123d.topology.one_d.Wire.make_polygon", + "build123d.topology.one_d.Wire.make_rect", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.one_d.Wire.order_chamfer_edges", + "build123d.topology.one_d.Wire.order_edges", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Wire.param_at", + "build123d.topology.one_d.Wire.param_at_point", + "build123d.objects_curve.Airfoil.parse_naca4", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Wire.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Wire.stitch", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Wire.trim", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.Bezier": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.BlendCurve": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.BSpline": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.CenterArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.ConstrainedArcs": [ + "build123d.topology.composite.Curve.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.composite.Curve.__matmul__", + "build123d.topology.composite.Curve.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.composite.Curve.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.composite.Curve.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.ConstrainedLines": [ + "build123d.topology.composite.Curve.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.composite.Curve.__matmul__", + "build123d.topology.composite.Curve.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.composite.Curve.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.composite.Curve.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.DoubleTangentArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.EllipticalCenterArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.EllipticalStartArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.ParabolicCenterArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.HyperbolicCenterArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.FilletPolyline": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.one_d.Wire.chamfer_2d", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Wire.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.one_d.Wire.combine", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.one_d.Wire.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Wire.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Wire.fillet_2d", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.one_d.Wire.fix_degenerate_edges", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Wire.geom_adaptor", + "build123d.topology.one_d.Wire.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Wire.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Wire.make_convex_hull", + "build123d.topology.one_d.Wire.make_ellipse", + "build123d.topology.one_d.Wire.make_polygon", + "build123d.topology.one_d.Wire.make_rect", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.one_d.Wire.order_chamfer_edges", + "build123d.topology.one_d.Wire.order_edges", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Wire.param_at", + "build123d.topology.one_d.Wire.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Wire.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Wire.stitch", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Wire.trim", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.Helix": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.IntersectingLine": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.Line": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.PolarLine": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.Polyline": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.one_d.Wire.chamfer_2d", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Wire.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.one_d.Wire.combine", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.one_d.Wire.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Wire.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Wire.fillet_2d", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.one_d.Wire.fix_degenerate_edges", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Wire.geom_adaptor", + "build123d.topology.one_d.Wire.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Wire.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Wire.make_convex_hull", + "build123d.topology.one_d.Wire.make_ellipse", + "build123d.topology.one_d.Wire.make_polygon", + "build123d.topology.one_d.Wire.make_rect", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.one_d.Wire.order_chamfer_edges", + "build123d.topology.one_d.Wire.order_edges", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Wire.param_at", + "build123d.topology.one_d.Wire.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Wire.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Wire.stitch", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Wire.trim", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.RadiusArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.SagittaArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.Spline": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.TangentArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.JernArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_curve.ThreePointArc": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.drafting.ArrowHead": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.drafting.Arrow": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.BaseSketchObject": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Circle": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.drafting.Draft": [ + "build123d.drafting.Draft.is_metric" + ], + "build123d.drafting.DimensionLine": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Ellipse": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.drafting.ExtensionLine": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Polygon": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Rectangle": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.RectangleRounded": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.RegularPolygon": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.SlotArc": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.SlotCenterPoint": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.SlotCenterToCenter": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.SlotOverall": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Superellipse": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Text": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.drafting.TechnicalDrawing": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Trapezoid": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_sketch.Triangle": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.BasePartObject": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Box": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Cone": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.ConvexPolyhedron": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.CounterBoreHole": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.CounterSinkHole": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Cylinder": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Hole": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Sphere": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Torus": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.objects_part.Wedge": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.geometry.BoundBox": [ + "build123d.geometry.BoundBox.add", + "build123d.geometry.BoundBox.center", + "build123d.geometry.BoundBox.contains", + "build123d.geometry.BoundBox.contains_properly", + "build123d.geometry.BoundBox.covered_by", + "build123d.geometry.BoundBox.covers", + "build123d.geometry.BoundBox.diagonal", + "build123d.geometry.BoundBox.disjoint", + "build123d.geometry.BoundBox.find_outside_box_2d", + "build123d.geometry.BoundBox.from_topo_ds", + "build123d.geometry.BoundBox.intersects", + "build123d.geometry.BoundBox.is_inside", + "build123d.geometry.BoundBox.measure", + "build123d.geometry.BoundBox.overlaps", + "build123d.geometry.BoundBox.to_align_offset", + "build123d.geometry.BoundBox.touches", + "build123d.geometry.BoundBox.within" + ], + "build123d.geometry.OrientedBoundBox": [ + "build123d.geometry.OrientedBoundBox.center", + "build123d.geometry.OrientedBoundBox.corners", + "build123d.geometry.OrientedBoundBox.diagonal", + "build123d.geometry.OrientedBoundBox.is_completely_inside", + "build123d.geometry.OrientedBoundBox.is_outside", + "build123d.geometry.OrientedBoundBox.location", + "build123d.geometry.OrientedBoundBox.plane", + "build123d.geometry.OrientedBoundBox.size", + "build123d.geometry.OrientedBoundBox.wrapped", + "build123d.geometry.OrientedBoundBox.x_direction", + "build123d.geometry.OrientedBoundBox.y_direction", + "build123d.geometry.OrientedBoundBox.z_direction" + ], + "build123d.geometry.Rotation": [ + "build123d.geometry.Location.__and__", + "build123d.geometry.Location.__mul__", + "build123d.geometry.Location.__neg__", + "build123d.geometry.Location.center", + "build123d.geometry.Location.intersect", + "build123d.geometry.Location.inverse", + "build123d.geometry.Location.mirror", + "build123d.geometry.Location.orientation", + "build123d.geometry.Location.position", + "build123d.geometry.Location.wrapped", + "build123d.geometry.Location.x_axis", + "build123d.geometry.Location.y_axis", + "build123d.geometry.Location.z_axis" + ], + "build123d.geometry.Pos": [ + "build123d.geometry.Location.__and__", + "build123d.geometry.Location.__mul__", + "build123d.geometry.Location.__neg__", + "build123d.geometry.Location.center", + "build123d.geometry.Location.intersect", + "build123d.geometry.Location.inverse", + "build123d.geometry.Location.mirror", + "build123d.geometry.Location.orientation", + "build123d.geometry.Location.position", + "build123d.geometry.Location.wrapped", + "build123d.geometry.Location.x_axis", + "build123d.geometry.Location.y_axis", + "build123d.geometry.Location.z_axis" + ], + "build123d.topology.shape_core.ShapeList": [ + "build123d.topology.shape_core.ShapeList.__add__", + "build123d.topology.shape_core.ShapeList.__and__", + "build123d.topology.shape_core.ShapeList.__sub__", + "build123d.topology.shape_core.ShapeList.center", + "build123d.topology.shape_core.ShapeList.compound", + "build123d.topology.shape_core.ShapeList.compounds", + "build123d.topology.shape_core.ShapeList.edge", + "build123d.topology.shape_core.ShapeList.edges", + "build123d.topology.shape_core.ShapeList.expand", + "build123d.topology.shape_core.ShapeList.face", + "build123d.topology.shape_core.ShapeList.faces", + "build123d.topology.shape_core.ShapeList.filter_by", + "build123d.topology.shape_core.ShapeList.filter_by_position", + "build123d.topology.shape_core.ShapeList.first", + "build123d.topology.shape_core.ShapeList.group_by", + "build123d.topology.shape_core.ShapeList.last", + "build123d.topology.shape_core.ShapeList.shell", + "build123d.topology.shape_core.ShapeList.shells", + "build123d.topology.shape_core.ShapeList.solid", + "build123d.topology.shape_core.ShapeList.solids", + "build123d.topology.shape_core.ShapeList.sort_by", + "build123d.topology.shape_core.ShapeList.sort_by_distance", + "build123d.topology.shape_core.ShapeList.vertex", + "build123d.topology.shape_core.ShapeList.vertices", + "build123d.topology.shape_core.ShapeList.wire", + "build123d.topology.shape_core.ShapeList.wires" + ], + "build123d.geometry.Axis": [ + "build123d.geometry.Axis.__and__", + "build123d.geometry.Axis.__neg__", + "build123d.geometry.Axis.angle_between", + "build123d.geometry.Axis.direction", + "build123d.geometry.Axis.intersect", + "build123d.geometry.Axis.is_coaxial", + "build123d.geometry.Axis.is_normal", + "build123d.geometry.Axis.is_opposite", + "build123d.geometry.Axis.is_parallel", + "build123d.geometry.Axis.is_skew", + "build123d.geometry.Axis.located", + "build123d.geometry.Axis.location", + "build123d.geometry.Axis.position", + "build123d.geometry.Axis.reverse", + "build123d.geometry.Axis.wrapped" + ], + "build123d.geometry.Color": [ + "build123d.geometry.Color.categorical_set" + ], + "build123d.topology.composite.Curve": [ + "build123d.topology.composite.Curve.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.composite.Curve.__matmul__", + "build123d.topology.composite.Curve.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.composite.Curve.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.composite.Curve.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.geometry.Vector": [ + "build123d.geometry.Vector.X", + "build123d.geometry.Vector.Y", + "build123d.geometry.Vector.Z", + "build123d.geometry.Vector.__add__", + "build123d.geometry.Vector.__and__", + "build123d.geometry.Vector.__mul__", + "build123d.geometry.Vector.__neg__", + "build123d.geometry.Vector.__radd__", + "build123d.geometry.Vector.__rmul__", + "build123d.geometry.Vector.__sub__", + "build123d.geometry.Vector.add", + "build123d.geometry.Vector.center", + "build123d.geometry.Vector.cross", + "build123d.geometry.Vector.distance_to_plane", + "build123d.geometry.Vector.dot", + "build123d.geometry.Vector.get_angle", + "build123d.geometry.Vector.get_signed_angle", + "build123d.geometry.Vector.intersect", + "build123d.geometry.Vector.length", + "build123d.geometry.Vector.multiply", + "build123d.geometry.Vector.normalized", + "build123d.geometry.Vector.project_to_line", + "build123d.geometry.Vector.project_to_plane", + "build123d.geometry.Vector.reverse", + "build123d.geometry.Vector.rotate", + "build123d.geometry.Vector.signed_distance_from_plane", + "build123d.geometry.Vector.sub", + "build123d.geometry.Vector.to_dir", + "build123d.geometry.Vector.to_pnt", + "build123d.geometry.Vector.transform", + "build123d.geometry.Vector.wrapped" + ], + "build123d.topology.zero_d.Vertex": [ + "build123d.topology.zero_d.Vertex.X", + "build123d.topology.zero_d.Vertex.Y", + "build123d.topology.zero_d.Vertex.Z", + "build123d.topology.zero_d.Vertex.__add__", + "build123d.topology.zero_d.Vertex.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.zero_d.Vertex.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.zero_d.Vertex.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.zero_d.Vertex.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.zero_d.Vertex.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.zero_d.Vertex.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.zero_d.Vertex.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.zero_d.Vertex.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.zero_d.Vertex.vertex", + "build123d.topology.zero_d.Vertex.vertices", + "build123d.topology.zero_d.Vertex.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.one_d.Edge": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.one_d.Edge.arc_center", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Edge.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.one_d.Edge.distribute_locations", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Edge.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Edge.find_intersection_points", + "build123d.topology.one_d.Edge.find_tangent", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Edge.geom_adaptor", + "build123d.topology.one_d.Edge.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Edge.is_infinite", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Edge.make_bezier", + "build123d.topology.one_d.Edge.make_bspline", + "build123d.topology.one_d.Edge.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Edge.make_constrained_arcs", + "build123d.topology.one_d.Edge.make_constrained_lines", + "build123d.topology.one_d.Edge.make_ellipse", + "build123d.topology.one_d.Edge.make_helix", + "build123d.topology.one_d.Edge.make_hyperbola", + "build123d.topology.one_d.Edge.make_line", + "build123d.topology.one_d.Edge.make_mid_way", + "build123d.topology.one_d.Edge.make_parabola", + "build123d.topology.one_d.Edge.make_spline", + "build123d.topology.one_d.Edge.make_spline_approx", + "build123d.topology.one_d.Edge.make_tangent_arc", + "build123d.topology.one_d.Edge.make_three_point_arc", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Edge.param_at", + "build123d.topology.one_d.Edge.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Edge.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.one_d.Edge.reversed", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Edge.trim", + "build123d.topology.one_d.Edge.trim_infinite", + "build123d.topology.one_d.Edge.trim_to_length", + "build123d.topology.one_d.Edge.trim_to_other", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.one_d.Wire": [ + "build123d.topology.one_d.Mixin1D.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.one_d.Mixin1D.__matmul__", + "build123d.topology.one_d.Mixin1D.__mod__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.one_d.Mixin1D.__xor__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.one_d.Mixin1D.center", + "build123d.topology.one_d.Wire.chamfer_2d", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.one_d.Wire.close", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.one_d.Wire.combine", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.one_d.Mixin1D.common_plane", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.one_d.Mixin1D.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.one_d.Mixin1D.curvature_comb", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.one_d.Mixin1D.derivative_at", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.one_d.Wire.edges", + "build123d.topology.one_d.Mixin1D.end_point", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.one_d.Wire.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.one_d.Wire.fillet_2d", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.one_d.Wire.fix_degenerate_edges", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.one_d.Wire.geom_adaptor", + "build123d.topology.one_d.Wire.geom_equal", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.one_d.Mixin1D.is_closed", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.one_d.Mixin1D.is_forward", + "build123d.topology.one_d.Mixin1D.is_interior", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.one_d.Mixin1D.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.one_d.Mixin1D.location_at", + "build123d.topology.one_d.Mixin1D.locations", + "build123d.topology.one_d.Wire.make_circle", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.one_d.Wire.make_convex_hull", + "build123d.topology.one_d.Wire.make_ellipse", + "build123d.topology.one_d.Wire.make_polygon", + "build123d.topology.one_d.Wire.make_rect", + "build123d.topology.one_d.Mixin1D.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.one_d.Mixin1D.normal", + "build123d.topology.one_d.Mixin1D.offset_2d", + "build123d.topology.one_d.Wire.order_chamfer_edges", + "build123d.topology.one_d.Wire.order_edges", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.one_d.Wire.param_at", + "build123d.topology.one_d.Wire.param_at_point", + "build123d.topology.one_d.Mixin1D.perpendicular_line", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.one_d.Mixin1D.position_at", + "build123d.topology.one_d.Mixin1D.positions", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.one_d.Mixin1D.project", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.one_d.Wire.project_to_shape", + "build123d.topology.one_d.Mixin1D.project_to_viewport", + "build123d.topology.one_d.Mixin1D.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.one_d.Mixin1D.start_point", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.one_d.Wire.stitch", + "build123d.topology.one_d.Mixin1D.tangent_angle_at", + "build123d.topology.one_d.Mixin1D.tangent_at", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.shape_core.Shape.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.one_d.Wire.trim", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.one_d.Mixin1D.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.two_d.Face": [ + "build123d.topology.shape_core.Shape.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.two_d.Mixin2D.__neg__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.two_d.Face.area_without_holes", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.two_d.Face.axes_of_symmetry", + "build123d.topology.two_d.Face.axis_of_rotation", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.two_d.Face.center", + "build123d.topology.two_d.Face.center_location", + "build123d.topology.two_d.Face.chamfer_2d", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.two_d.Face.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.two_d.Face.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.two_d.Face.fillet_2d", + "build123d.topology.two_d.Mixin2D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.two_d.Face.geom_adaptor", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.two_d.Face.geometry", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.two_d.Face.inner_wires", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.two_d.Face.is_circular_concave", + "build123d.topology.two_d.Face.is_circular_convex", + "build123d.topology.two_d.Face.is_coplanar", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.two_d.Face.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.two_d.Face.is_planar", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.two_d.Face.length", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.two_d.Face.location_at", + "build123d.topology.two_d.Face.make_bezier_surface", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.two_d.Face.make_gordon_surface", + "build123d.topology.two_d.Face.make_holes", + "build123d.topology.two_d.Face.make_rect", + "build123d.topology.two_d.Face.make_surface", + "build123d.topology.two_d.Face.make_surface_from_array_of_points", + "build123d.topology.two_d.Face.make_surface_from_curves", + "build123d.topology.two_d.Face.make_surface_patch", + "build123d.topology.two_d.Face.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.two_d.Face.normal_at", + "build123d.topology.two_d.Mixin2D.offset", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.two_d.Face.outer_wire", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.two_d.Face.position_at", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.two_d.Face.project_to_shape", + "build123d.topology.two_d.Mixin2D.project_to_viewport", + "build123d.topology.two_d.Face.radii", + "build123d.topology.two_d.Face.radius", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.two_d.Face.revolve", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.two_d.Face.seams", + "build123d.topology.two_d.Face.semi_angle", + "build123d.topology.two_d.Face.sew_faces", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.two_d.Mixin2D.split_by_perimeter", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.two_d.Face.sweep", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.two_d.Mixin2D.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.two_d.Face.uv_face", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.two_d.Face.volume", + "build123d.topology.two_d.Face.width", + "build123d.topology.two_d.Face.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.two_d.Face.without_holes", + "build123d.topology.two_d.Face.wrap", + "build123d.topology.two_d.Face.wrap_faces", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.geometry.Matrix": [ + "build123d.geometry.Matrix.inverse", + "build123d.geometry.Matrix.multiply", + "build123d.geometry.Matrix.rotate", + "build123d.geometry.Matrix.transposed_list" + ], + "build123d.topology.three_d.Solid": [ + "build123d.topology.shape_core.Shape.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.three_d.Mixin3D.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.three_d.Solid.draft", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.three_d.Solid.extrude", + "build123d.topology.three_d.Solid.extrude_linear_with_rotation", + "build123d.topology.three_d.Solid.extrude_taper", + "build123d.topology.three_d.Solid.extrude_until", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.three_d.Solid.from_bounding_box", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.three_d.Solid.make_box", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.three_d.Solid.make_cone", + "build123d.topology.three_d.Solid.make_cylinder", + "build123d.topology.three_d.Solid.make_loft", + "build123d.topology.three_d.Solid.make_sphere", + "build123d.topology.three_d.Solid.make_torus", + "build123d.topology.three_d.Solid.make_wedge", + "build123d.topology.three_d.Solid.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.three_d.Mixin3D.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.three_d.Solid.revolve", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.three_d.Solid.sweep", + "build123d.topology.three_d.Solid.sweep_multi", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.three_d.Solid.thicken", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.three_d.Solid.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.three_d.Solid.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.two_d.Shell": [ + "build123d.topology.shape_core.Shape.__add__", + "build123d.topology.shape_core.Shape.__and__", + "build123d.topology.two_d.Mixin2D.__neg__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.shape_core.Shape.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.two_d.Shell.center", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.shape_core.Shape.compound", + "build123d.topology.shape_core.Shape.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.two_d.Shell.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.two_d.Mixin2D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.two_d.Shell.location_at", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.two_d.Shell.make_loft", + "build123d.topology.two_d.Shell.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.two_d.Mixin2D.offset", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.two_d.Mixin2D.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.two_d.Shell.revolve", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.two_d.Mixin2D.split_by_perimeter", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.two_d.Shell.sweep", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.two_d.Mixin2D.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.two_d.Shell.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.topology.composite.Part": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.geometry.Plane": [ + "build123d.geometry.Plane.__and__", + "build123d.geometry.Plane.__mul__", + "build123d.geometry.Plane.__neg__", + "build123d.geometry.Plane.__rmul__", + "build123d.geometry.Plane.contains", + "build123d.geometry.Plane.forward_transform", + "build123d.geometry.Plane.from_local_coords", + "build123d.geometry.Plane.get_topods_face_normal", + "build123d.geometry.Plane.intersect", + "build123d.geometry.Plane.location", + "build123d.geometry.Plane.location_between", + "build123d.geometry.Plane.move", + "build123d.geometry.Plane.moved", + "build123d.geometry.Plane.offset", + "build123d.geometry.Plane.origin", + "build123d.geometry.Plane.reverse", + "build123d.geometry.Plane.reverse_transform", + "build123d.geometry.Plane.rotated", + "build123d.geometry.Plane.shift_origin", + "build123d.geometry.Plane.to_gp_ax2", + "build123d.geometry.Plane.to_gp_ax3", + "build123d.geometry.Plane.to_local_coords", + "build123d.geometry.Plane.wrapped", + "build123d.geometry.Plane.x_dir", + "build123d.geometry.Plane.y_dir", + "build123d.geometry.Plane.z_dir" + ], + "build123d.topology.composite.Compound": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.geometry.Location": [ + "build123d.geometry.Location.__and__", + "build123d.geometry.Location.__mul__", + "build123d.geometry.Location.__neg__", + "build123d.geometry.Location.center", + "build123d.geometry.Location.intersect", + "build123d.geometry.Location.inverse", + "build123d.geometry.Location.mirror", + "build123d.geometry.Location.orientation", + "build123d.geometry.Location.position", + "build123d.geometry.Location.wrapped", + "build123d.geometry.Location.x_axis", + "build123d.geometry.Location.y_axis", + "build123d.geometry.Location.z_axis" + ], + "build123d.geometry.GeomEncoder": [ + "build123d.geometry.GeomEncoder.default", + "build123d.geometry.GeomEncoder.geometry_hook" + ], + "build123d.topology.shape_core.Joint": [ + "build123d.topology.shape_core.Joint.connect_to", + "build123d.topology.shape_core.Joint.location", + "build123d.topology.shape_core.Joint.relative_to", + "build123d.topology.shape_core.Joint.symbol" + ], + "build123d.joints.RigidJoint": [ + "build123d.joints.RigidJoint.connect_to", + "build123d.joints.RigidJoint.location", + "build123d.joints.RigidJoint.relative_to", + "build123d.joints.RigidJoint.symbol" + ], + "build123d.joints.RevoluteJoint": [ + "build123d.joints.RevoluteJoint.connect_to", + "build123d.joints.RevoluteJoint.location", + "build123d.joints.RevoluteJoint.relative_to", + "build123d.joints.RevoluteJoint.symbol" + ], + "build123d.topology.composite.Sketch": [ + "build123d.topology.composite.Compound.__add__", + "build123d.topology.composite.Compound.__and__", + "build123d.topology.shape_core.Shape.__rmul__", + "build123d.topology.composite.Compound.__sub__", + "build123d.topology.shape_core.Shape.area", + "build123d.topology.shape_core.Shape.as_shape", + "build123d.topology.shape_core.Shape.bounding_box", + "build123d.topology.shape_core.Shape.cast", + "build123d.topology.composite.Compound.center", + "build123d.topology.three_d.Mixin3D.chamfer", + "build123d.topology.shape_core.Shape.clean", + "build123d.topology.shape_core.Shape.closest_points", + "build123d.topology.shape_core.Shape.color", + "build123d.topology.shape_core.Shape.combined_center", + "build123d.topology.composite.Compound.compound", + "build123d.topology.composite.Compound.compounds", + "build123d.topology.shape_core.Shape.compute_mass", + "build123d.topology.shape_core.Shape.compute_volume", + "build123d.topology.shape_core.Shape.convexity", + "build123d.topology.shape_core.Shape.copy_attributes_to", + "build123d.topology.shape_core.Shape.cut", + "build123d.topology.shape_core.Shape.distance", + "build123d.topology.shape_core.Shape.distance_to", + "build123d.topology.shape_core.Shape.distance_to_with_closest_points", + "build123d.topology.shape_core.Shape.distances", + "build123d.topology.composite.Compound.do_children_intersect", + "build123d.topology.three_d.Mixin3D.dprism", + "build123d.topology.shape_core.Shape.edge", + "build123d.topology.shape_core.Shape.edges", + "build123d.topology.shape_core.Shape.entities", + "build123d.topology.composite.Compound.extrude", + "build123d.topology.shape_core.Shape.face", + "build123d.topology.shape_core.Shape.faces", + "build123d.topology.shape_core.Shape.faces_intersected_by_axis", + "build123d.topology.three_d.Mixin3D.fillet", + "build123d.topology.three_d.Mixin3D.find_intersection_points", + "build123d.topology.shape_core.Shape.fix", + "build123d.topology.shape_core.Shape.fuse", + "build123d.topology.shape_core.Shape.geom_type", + "build123d.topology.shape_core.Shape.get_shape_list", + "build123d.topology.shape_core.Shape.get_single_shape", + "build123d.topology.shape_core.Shape.get_top_level_shapes", + "build123d.topology.composite.Compound.get_type", + "build123d.topology.shape_core.Shape.global_location", + "build123d.topology.three_d.Mixin3D.hollow", + "build123d.topology.shape_core.Shape.intersect", + "build123d.topology.shape_core.Shape.is_equal", + "build123d.topology.three_d.Mixin3D.is_inside", + "build123d.topology.shape_core.Shape.is_manifold", + "build123d.topology.shape_core.Shape.is_null", + "build123d.topology.shape_core.Shape.is_same", + "build123d.topology.shape_core.Shape.is_valid", + "build123d.topology.shape_core.Shape.locate", + "build123d.topology.shape_core.Shape.located", + "build123d.topology.shape_core.Shape.location", + "build123d.topology.shape_core.Shape.make_composite", + "build123d.topology.composite.Compound.make_text", + "build123d.topology.composite.Compound.make_triad", + "build123d.topology.composite.Compound.mass", + "build123d.topology.shape_core.Shape.material", + "build123d.topology.shape_core.Shape.matrix_of_inertia", + "build123d.topology.three_d.Mixin3D.max_fillet", + "build123d.topology.shape_core.Shape.mesh", + "build123d.topology.shape_core.Shape.mirror", + "build123d.topology.shape_core.Shape.move", + "build123d.topology.shape_core.Shape.moved", + "build123d.topology.three_d.Mixin3D.offset_3d", + "build123d.topology.shape_core.Shape.orientation", + "build123d.topology.shape_core.Shape.oriented_bounding_box", + "build123d.topology.shape_core.Shape.position", + "build123d.topology.shape_core.Shape.principal_properties", + "build123d.topology.shape_core.Shape.project_faces", + "build123d.topology.composite.Compound.project_to_viewport", + "build123d.topology.shape_core.Shape.radius_of_gyration", + "build123d.topology.shape_core.Shape.register_composite_factory", + "build123d.topology.shape_core.Shape.register_geometry_constructor", + "build123d.topology.shape_core.Shape.register_shape_constructor", + "build123d.topology.composite.Compound.resolve_font", + "build123d.topology.shape_core.Shape.rotate", + "build123d.topology.shape_core.Shape.scale", + "build123d.topology.shape_core.Shape.shape_type", + "build123d.topology.shape_core.Shape.shell", + "build123d.topology.shape_core.Shape.shells", + "build123d.topology.shape_core.Shape.show_topology", + "build123d.topology.shape_core.Shape.solid", + "build123d.topology.shape_core.Shape.solids", + "build123d.topology.shape_core.Shape.split", + "build123d.topology.shape_core.Shape.static_moments", + "build123d.topology.shape_core.Shape.tessellate", + "build123d.topology.shape_core.Shape.tessellate_with_uvs", + "build123d.topology.shape_core.Shape.to_splines", + "build123d.topology.shape_core.Shape.topo_owner", + "build123d.topology.shape_core.Shape.topo_parent", + "build123d.topology.composite.Compound.touch", + "build123d.topology.shape_core.Shape.transform_geometry", + "build123d.topology.shape_core.Shape.transform_shape", + "build123d.topology.shape_core.Shape.transformed", + "build123d.topology.shape_core.Shape.translate", + "build123d.topology.composite.Compound.unwrap", + "build123d.topology.shape_core.Shape.vertex", + "build123d.topology.shape_core.Shape.vertices", + "build123d.topology.composite.Compound.volume", + "build123d.topology.shape_core.Shape.wire", + "build123d.topology.shape_core.Shape.wires", + "build123d.topology.shape_core.Shape.wrapped" + ], + "build123d.joints.LinearJoint": [ + "build123d.joints.LinearJoint.connect_to", + "build123d.joints.LinearJoint.location", + "build123d.joints.LinearJoint.relative_to", + "build123d.joints.LinearJoint.symbol" + ], + "build123d.joints.CylindricalJoint": [ + "build123d.joints.CylindricalJoint.connect_to", + "build123d.joints.CylindricalJoint.location", + "build123d.joints.CylindricalJoint.relative_to", + "build123d.joints.CylindricalJoint.symbol" + ], + "build123d.joints.BallJoint": [ + "build123d.joints.BallJoint.connect_to", + "build123d.joints.BallJoint.location", + "build123d.joints.BallJoint.relative_to", + "build123d.joints.BallJoint.symbol" + ], + "build123d.topology.three_d.DraftAngleError": [], + "build123d.text.FontManager": [ + "build123d.text.FontManager.available_fonts", + "build123d.text.FontManager.check_font", + "build123d.text.FontManager.find_font", + "build123d.text.FontManager.register_folder", + "build123d.text.FontManager.register_font", + "build123d.text.FontManager.register_system_fonts" + ], + "build123d.exporters.Export2D": [], + "build123d.exporters.ExportDXF": [ + "build123d.exporters.ExportDXF.add_layer", + "build123d.exporters.ExportDXF.add_shape", + "build123d.exporters.ExportDXF.write" + ], + "build123d.exporters.ExportSVG": [ + "build123d.exporters.ExportSVG.add_layer", + "build123d.exporters.ExportSVG.add_shape", + "build123d.exporters.ExportSVG.write" + ], + "build123d.exporters.LineType": [], + "build123d.exporters.DotLength": [], + "build123d.mesher.Mesher": [ + "build123d.mesher.Mesher.add_code_to_metadata", + "build123d.mesher.Mesher.add_meta_data", + "build123d.mesher.Mesher.add_shape", + "build123d.mesher.Mesher.get_mesh_properties", + "build123d.mesher.Mesher.get_meta_data", + "build123d.mesher.Mesher.get_meta_data_by_key", + "build123d.mesher.Mesher.library_version", + "build123d.mesher.Mesher.mesh_count", + "build123d.mesher.Mesher.model_unit", + "build123d.mesher.Mesher.read", + "build123d.mesher.Mesher.triangle_counts", + "build123d.mesher.Mesher.vertex_counts", + "build123d.mesher.Mesher.write", + "build123d.mesher.Mesher.write_stream" + ] + }, + "corpus": { + "counting_note": "AST call sites, not runtime execution counts. Constant-false GEN_DOCS branches and __main__ export blocks are omitted; loops/functions are counted syntactically. Operator counts include numeric arithmetic.", + "top_level_calls": { + "BuildSketch": 57, + "BuildPart": 55, + "extrude": 40, + "Circle": 29, + "Rectangle": 25, + "Locations": 23, + "Box": 18, + "Line": 18, + "BuildLine": 16, + "fillet": 15, + "RegularPolygon": 14, + "GridLocations": 11, + "Plane": 10, + "make_face": 9, + "Cylinder": 8, + "mirror": 8, + "offset": 8, + "Spline": 6, + "sweep": 6, + "Polyline": 5, + "PolarLocations": 5, + "JernArc": 5, + "insert": 5, + "Hole": 4, + "CounterBoreHole": 4, + "split": 4, + "revolve": 4, + "RadiusArc": 4, + "Vector": 4, + "chamfer": 3, + "CounterSinkHole": 3, + "PolarLine": 3, + "ThreePointArc": 2, + "Axis": 2, + "loft": 2, + "Sphere": 2, + "Text": 2, + "SlotArc": 2, + "Rot": 2, + "Bezier": 1, + "SlotCenterToCenter": 1, + "Ellipse": 1, + "Location": 1, + "SlotOverall": 1, + "scale": 1, + "section": 1, + "RectangleRounded": 1, + "Color": 1, + "Compound": 1, + "Helix": 1 + }, + "method_calls": { + "faces": 26, + "sort_by": 19, + "edges": 15, + "group_by": 11, + "offset": 11, + "vertices": 9, + "filter_by": 7, + "filter_by_position": 5, + "center": 2, + "wires": 2, + "rotated": 1, + "solids": 1, + "shift_origin": 1, + "rotate": 1, + "reverse": 1, + "basicConfig": 1, + "resolve": 1, + "Path": 1, + "info": 1, + "face": 1, + "bounding_box": 1, + "inner_wires": 1, + "find_intersection_points": 1, + "to_local_coords": 1 + }, + "models": [ + { + "id": 1, + "name": "矩形板", + "file": "models/01_rectangular_plate.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L56-L59", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": {} + }, + { + "id": 2, + "name": "带孔板", + "file": "models/02_plate_with_hole.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L69-L74", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "Cylinder": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Div": 1 + } + }, + { + "id": 3, + "name": "棱柱实体", + "file": "models/03_prismatic_solid.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L83-L89", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "Circle": 1, + "Rectangle": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Div": 2, + "Mult": 1 + } + }, + { + "id": 4, + "name": "直线与圆弧轮廓", + "file": "models/04_lines_and_arcs_profile.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L98-L108", + "builder_context_present": true, + "top_level_call_sites": { + "Line": 3, + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "ThreePointArc": 1, + "make_face": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Mult": 1 + } + }, + { + "id": 5, + "name": "偏置孔与方孔", + "file": "models/05_offset_cutouts.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L117-L126", + "builder_context_present": true, + "top_level_call_sites": { + "Circle": 2, + "Locations": 2, + "BuildPart": 1, + "BuildSketch": 1, + "Rectangle": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": {} + }, + { + "id": 6, + "name": "点阵圆孔", + "file": "models/06_point_list_holes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L135-L142", + "builder_context_present": true, + "top_level_call_sites": { + "Circle": 2, + "BuildPart": 1, + "BuildSketch": 1, + "Locations": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": {} + }, + { + "id": 7, + "name": "多边形孔板", + "file": "models/07_polygon_holes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L150-L157", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "Rectangle": 1, + "Locations": 1, + "RegularPolygon": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Mult": 3 + } + }, + { + "id": 8, + "name": "工字梁", + "file": "models/08_i_beam.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L166-L184", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "Polyline": 1, + "mirror": 1, + "make_face": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Div": 14, + "Sub": 4 + } + }, + { + "id": 9, + "name": "圆角与倒角板", + "file": "models/09_fillets_and_chamfers.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L193-L198", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "chamfer": 1, + "fillet": 1 + }, + "method_call_sites": { + "edges": 2, + "group_by": 1, + "filter_by": 1 + }, + "operator_syntax_counts": {} + }, + { + "id": 10, + "name": "圆角孔板", + "file": "models/10_filleted_hole.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L207-L212", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "Hole": 1, + "fillet": 1 + }, + "method_call_sites": { + "group_by": 1, + "edges": 1 + }, + "operator_syntax_counts": { + "Div": 1 + } + }, + { + "id": 11, + "name": "网格多边形孔", + "file": "models/11_grid_polygon_holes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L221-L232", + "builder_context_present": true, + "top_level_call_sites": { + "fillet": 2, + "BuildPart": 1, + "Box": 1, + "chamfer": 1, + "Hole": 1, + "BuildSketch": 1, + "GridLocations": 1, + "RegularPolygon": 1, + "extrude": 1 + }, + "method_call_sites": { + "edges": 3, + "sort_by": 2, + "group_by": 1, + "filter_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Div": 3 + } + }, + { + "id": 12, + "name": "样条轮廓实体", + "file": "models/12_spline_profile.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L241-L259", + "builder_context_present": true, + "top_level_call_sites": { + "Line": 3, + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "Spline": 1, + "make_face": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": {} + }, + { + "id": 13, + "name": "沉头孔与沉孔盘", + "file": "models/13_countersink_counterbore.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L269-L276", + "builder_context_present": true, + "top_level_call_sites": { + "PolarLocations": 2, + "BuildPart": 1, + "Cylinder": 1, + "Locations": 1, + "CounterSinkHole": 1, + "CounterBoreHole": 1 + }, + "method_call_sites": { + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Mult": 2 + } + }, + { + "id": 14, + "name": "方形截面扫掠", + "file": "models/14_swept_square_tube.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L285-L294", + "builder_context_present": true, + "top_level_call_sites": { + "JernArc": 2, + "BuildPart": 1, + "BuildLine": 1, + "Line": 1, + "BuildSketch": 1, + "Rectangle": 1, + "sweep": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "MatMult": 3, + "Mod": 1, + "Add": 1 + } + }, + { + "id": 15, + "name": "对称轮廓实体", + "file": "models/15_mirrored_profile.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L304-L316", + "builder_context_present": true, + "top_level_call_sites": { + "Line": 5, + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "mirror": 1, + "make_face": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "MatMult": 8, + "Add": 3 + } + }, + { + "id": 16, + "name": "三维镜像组合", + "file": "models/16_mirrored_3d_parts.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L326-L342", + "builder_context_present": true, + "top_level_call_sites": { + "mirror": 4, + "BuildPart": 2, + "Rectangle": 2, + "BuildSketch": 1, + "fillet": 1, + "GridLocations": 1, + "Circle": 1, + "extrude": 1, + "insert": 1 + }, + "method_call_sites": { + "offset": 4, + "vertices": 1 + }, + "operator_syntax_counts": { + "Div": 3 + } + }, + { + "id": 17, + "name": "面镜像棱柱", + "file": "models/17_face_mirror.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L351-L357", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "RegularPolygon": 1, + "extrude": 1, + "mirror": 1, + "Plane": 1 + }, + "method_call_sites": { + "group_by": 1, + "faces": 1 + }, + "operator_syntax_counts": {} + }, + { + "id": 18, + "name": "面定位方孔板", + "file": "models/18_face_cutout.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L367-L376", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "chamfer": 1, + "fillet": 1, + "BuildSketch": 1, + "Rectangle": 1, + "extrude": 1 + }, + "method_call_sites": { + "edges": 2, + "group_by": 1, + "filter_by": 1, + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Mult": 2 + } + }, + { + "id": 19, + "name": "顶点定位缺口板", + "file": "models/19_vertex_cutouts.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L385-L398", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "extrude": 2, + "BuildPart": 1, + "RegularPolygon": 1, + "Axis": 1, + "Locations": 1, + "Circle": 1 + }, + "method_call_sites": { + "sort_by": 2, + "vertices": 2, + "faces": 1, + "group_by": 1 + }, + "operator_syntax_counts": { + "Div": 2 + } + }, + { + "id": 20, + "name": "偏置平面圆柱组合", + "file": "models/20_offset_plane_cylinder.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L407-L414", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "Plane": 1, + "BuildSketch": 1, + "Circle": 1, + "extrude": 1 + }, + "method_call_sites": { + "group_by": 1, + "faces": 1, + "offset": 1 + }, + "operator_syntax_counts": { + "Mult": 1, + "Div": 1 + } + }, + { + "id": 21, + "name": "交叉圆柱", + "file": "models/21_crossed_cylinders.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L423-L431", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "Circle": 2, + "extrude": 2, + "BuildPart": 1, + "Plane": 1 + }, + "method_call_sites": { + "center": 1 + }, + "operator_syntax_counts": { + "Div": 2 + } + }, + { + "id": 22, + "name": "倾斜钻孔板", + "file": "models/22_angled_drill_holes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L440-L448", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "Plane": 1, + "BuildSketch": 1, + "GridLocations": 1, + "Circle": 1, + "extrude": 1 + }, + "method_call_sites": { + "rotated": 1, + "group_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Div": 3 + } + }, + { + "id": 23, + "name": "旋转轮廓实体", + "file": "models/23_revolved_profile.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L457-L475", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "Polyline": 1, + "Line": 1, + "make_face": 1, + "Locations": 1, + "Circle": 1, + "split": 1, + "revolve": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "MatMult": 2 + } + }, + { + "id": 24, + "name": "圆到矩形放样", + "file": "models/24_circle_rectangle_loft.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L484-L492", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "BuildPart": 1, + "Box": 1, + "Circle": 1, + "Rectangle": 1, + "loft": 1 + }, + "method_call_sites": { + "faces": 2, + "group_by": 1, + "offset": 1 + }, + "operator_syntax_counts": { + "Div": 4 + } + }, + { + "id": 25, + "name": "多边形偏置层", + "file": "models/25_offset_polygon_layers.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L501-L512", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 3, + "RegularPolygon": 3, + "offset": 2, + "BuildPart": 1, + "extrude": 1 + }, + "method_call_sites": { + "offset": 2 + }, + "operator_syntax_counts": {} + }, + { + "id": 26, + "name": "薄壁盒", + "file": "models/26_thin_wall_box.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L521-L526", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "offset": 1 + }, + "method_call_sites": { + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": {} + }, + { + "id": 27, + "name": "剖切孔板", + "file": "models/27_split_plate.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L535-L542", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "Box": 1, + "BuildSketch": 1, + "Circle": 1, + "extrude": 1, + "split": 1, + "Plane": 1 + }, + "method_call_sites": { + "sort_by": 2, + "faces": 2, + "offset": 1 + }, + "operator_syntax_counts": { + "Div": 2 + } + }, + { + "id": 28, + "name": "面定位球体钻孔", + "file": "models/28_face_located_sphere_holes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L551-L561", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "RegularPolygon": 1, + "extrude": 1, + "Sphere": 1, + "Locations": 1, + "Hole": 1 + }, + "method_call_sites": { + "group_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Div": 3 + } + }, + { + "id": 29, + "name": "OCC 瓶子", + "file": "models/29_occ_bottle.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L570-L586", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "Line": 2, + "extrude": 2, + "BuildPart": 1, + "BuildLine": 1, + "ThreePointArc": 1, + "mirror": 1, + "make_face": 1, + "fillet": 1, + "Circle": 1, + "offset": 1 + }, + "method_call_sites": { + "sort_by": 2, + "faces": 2, + "offset": 1, + "edges": 1, + "solids": 1 + }, + "operator_syntax_counts": { + "Div": 5, + "MatMult": 3, + "Add": 2 + } + }, + { + "id": 30, + "name": "贝塞尔轮廓实体", + "file": "models/30_bezier_profile.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L595-L621", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "Polyline": 1, + "Bezier": 1, + "make_face": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": {} + }, + { + "id": 31, + "name": "嵌套定位阵列", + "file": "models/31_nested_location_pattern.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L630-L639", + "builder_context_present": true, + "top_level_call_sites": { + "RegularPolygon": 3, + "BuildPart": 1, + "BuildSketch": 1, + "PolarLocations": 1, + "GridLocations": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Mult": 3, + "Div": 1 + } + }, + { + "id": 32, + "name": "变高度阵列", + "file": "models/32_variable_height_pattern.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L648-L657", + "builder_context_present": true, + "top_level_call_sites": { + "RegularPolygon": 2, + "BuildPart": 1, + "BuildSketch": 1, + "PolarLocations": 1, + "insert": 1, + "extrude": 1 + }, + "method_call_sites": { + "faces": 1 + }, + "operator_syntax_counts": { + "Mult": 2, + "Div": 1, + "Add": 1 + } + }, + { + "id": 33, + "name": "函数生成阵列", + "file": "models/33_function_generated_pattern.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L666-L683", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "insert": 2, + "Locations": 1, + "RegularPolygon": 1, + "BuildPart": 1, + "PolarLocations": 1, + "extrude": 1 + }, + "method_call_sites": { + "faces": 1 + }, + "operator_syntax_counts": { + "Add": 2, + "Mult": 2, + "Div": 1 + } + }, + { + "id": 34, + "name": "凸字与凹字板", + "file": "models/34_embossed_debossed_text.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L692-L702", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "Text": 2, + "extrude": 2, + "BuildPart": 1, + "Box": 1 + }, + "method_call_sites": { + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": {} + }, + { + "id": 35, + "name": "直槽与圆弧槽板", + "file": "models/35_curved_slots.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L711-L724", + "builder_context_present": true, + "top_level_call_sites": { + "BuildLine": 2, + "RadiusArc": 2, + "SlotArc": 2, + "BuildPart": 1, + "Box": 1, + "BuildSketch": 1, + "SlotCenterToCenter": 1, + "extrude": 1 + }, + "method_call_sites": { + "edges": 2, + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Div": 7 + } + }, + { + "id": 36, + "name": "拉伸到曲面", + "file": "models/36_extrude_until.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L733-L742", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "BuildPart": 1, + "Locations": 1, + "Circle": 1, + "revolve": 1, + "Rectangle": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": {} + }, + { + "id": 37, + "name": "平面定位复合轮廓", + "file": "models/37_positioned_sketches.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L751-L760", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 3, + "BuildPart": 1, + "Rectangle": 1, + "Circle": 1, + "Plane": 1, + "Ellipse": 1, + "extrude": 1 + }, + "method_call_sites": { + "shift_origin": 1, + "sort_by": 1, + "group_by": 1, + "vertices": 1 + }, + "operator_syntax_counts": {} + }, + { + "id": 38, + "name": "旋转盒体阵列", + "file": "models/38_rotated_boxes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/docs/general_examples.py#L769-L780", + "builder_context_present": true, + "top_level_call_sites": { + "Box": 4, + "Locations": 3, + "BuildPart": 2, + "Rot": 1, + "insert": 1, + "Location": 1 + }, + "method_call_sites": { + "rotate": 1 + }, + "operator_syntax_counts": { + "Mult": 2 + } + }, + { + "id": 39, + "name": "洞洞板 J 形挂钩", + "file": "models/39_pegboard_j_hook.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/pegboard_j_hook.py", + "builder_context_present": true, + "top_level_call_sites": { + "JernArc": 3, + "PolarLine": 3, + "BuildLine": 2, + "Line": 2, + "BuildSketch": 2, + "Circle": 2, + "sweep": 2, + "split": 2, + "Plane": 2, + "BuildPart": 1 + }, + "method_call_sites": { + "wires": 2, + "reverse": 1 + }, + "operator_syntax_counts": { + "Div": 8, + "MatMult": 6, + "Mod": 6, + "Sub": 3, + "Add": 2, + "Mult": 2 + } + }, + { + "id": 40, + "name": "抽屉把手", + "file": "models/40_handle.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/handle.py", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildLine": 1, + "Spline": 1, + "BuildSketch": 1, + "Circle": 1, + "Rectangle": 1, + "fillet": 1, + "sweep": 1 + }, + "method_call_sites": { + "vertices": 1 + }, + "operator_syntax_counts": { + "Add": 1, + "BitXor": 1, + "Div": 1, + "Mod": 1, + "Sub": 1 + } + }, + { + "id": 41, + "name": "轴承座", + "file": "models/41_pillow_block.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/pillow_block.py", + "builder_context_present": true, + "top_level_call_sites": { + "CounterBoreHole": 2, + "BuildPart": 1, + "BuildSketch": 1, + "Rectangle": 1, + "fillet": 1, + "extrude": 1, + "Locations": 1, + "GridLocations": 1 + }, + "method_call_sites": { + "vertices": 1, + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Sub": 2, + "Mult": 2 + } + }, + { + "id": 42, + "name": "DIN 导轨", + "file": "models/42_din_rail.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/din_rail.py", + "builder_context_present": true, + "top_level_call_sites": { + "Rectangle": 3, + "BuildSketch": 2, + "fillet": 2, + "extrude": 2, + "BuildPart": 1, + "GridLocations": 1, + "SlotOverall": 1 + }, + "method_call_sites": { + "filter_by_position": 2, + "vertices": 2, + "basicConfig": 1, + "resolve": 1, + "Path": 1, + "info": 1 + }, + "operator_syntax_counts": { + "Div": 7, + "Sub": 3, + "Mult": 1, + "Add": 1, + "FloorDiv": 1 + } + }, + { + "id": 43, + "name": "积木块", + "file": "models/43_lego.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/lego.py", + "builder_context_present": true, + "top_level_call_sites": { + "Rectangle": 4, + "GridLocations": 4, + "BuildPart": 2, + "Circle": 2, + "BuildSketch": 1, + "offset": 1, + "extrude": 1, + "Locations": 1, + "Box": 1, + "Cylinder": 1 + }, + "method_call_sites": { + "sort_by": 2, + "vertices": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Sub": 5, + "Mult": 3, + "Add": 3, + "Div": 3 + } + }, + { + "id": 44, + "name": "多截面放样壳体", + "file": "models/44_loft.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/loft.py", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 1, + "BuildSketch": 1, + "Plane": 1, + "Circle": 1, + "loft": 1, + "offset": 1 + }, + "method_call_sites": { + "filter_by": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Mult": 4, + "Add": 2, + "Div": 1, + "Sub": 1 + } + }, + { + "id": 45, + "name": "Cherry MX 键帽", + "file": "models/45_key_cap.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/key_cap.py", + "builder_context_present": true, + "top_level_call_sites": { + "Rectangle": 5, + "BuildSketch": 3, + "extrude": 3, + "Circle": 2, + "BuildPart": 1, + "Locations": 1, + "Sphere": 1, + "fillet": 1, + "scale": 1, + "section": 1, + "Plane": 1 + }, + "method_call_sites": { + "filter_by_position": 2, + "edges": 1, + "face": 1, + "offset": 1, + "bounding_box": 1, + "inner_wires": 1, + "faces": 1 + }, + "operator_syntax_counts": { + "Mult": 21, + "Div": 2, + "Sub": 1 + } + }, + { + "id": 46, + "name": "茶杯", + "file": "models/46_tea_cup.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/tea_cup.py", + "builder_context_present": true, + "top_level_call_sites": { + "BuildSketch": 2, + "BuildLine": 2, + "Spline": 2, + "BuildPart": 1, + "Polyline": 1, + "make_face": 1, + "revolve": 1, + "offset": 1, + "Locations": 1, + "Cylinder": 1, + "fillet": 1, + "Axis": 1, + "RectangleRounded": 1, + "sweep": 1, + "Color": 1 + }, + "method_call_sites": { + "filter_by": 1, + "faces": 1, + "edges": 1, + "find_intersection_points": 1, + "to_local_coords": 1 + }, + "operator_syntax_counts": { + "Mult": 15, + "MatMult": 6, + "Add": 3, + "Sub": 3, + "Div": 2, + "BitXor": 1 + } + }, + { + "id": 47, + "name": "花瓶", + "file": "models/47_vase.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/vase.py", + "builder_context_present": true, + "top_level_call_sites": { + "Vector": 4, + "RadiusArc": 2, + "Spline": 2, + "fillet": 2, + "BuildPart": 1, + "BuildSketch": 1, + "BuildLine": 1, + "Line": 1, + "Polyline": 1, + "make_face": 1, + "revolve": 1, + "offset": 1, + "Rot": 1 + }, + "method_call_sites": { + "filter_by": 2, + "edges": 2, + "faces": 1, + "filter_by_position": 1, + "sort_by": 1 + }, + "operator_syntax_counts": { + "MatMult": 11, + "Add": 5, + "Mod": 2, + "Mult": 1 + } + }, + { + "id": 48, + "name": "带孔电路板", + "file": "models/48_circuit_board.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/circuit_board.py", + "builder_context_present": true, + "top_level_call_sites": { + "Circle": 3, + "Locations": 2, + "BuildPart": 1, + "BuildSketch": 1, + "Rectangle": 1, + "GridLocations": 1, + "extrude": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Mult": 5, + "Sub": 3, + "FloorDiv": 2 + } + }, + { + "id": 49, + "name": "四种孔型组合", + "file": "models/49_holes.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/holes.py", + "builder_context_present": true, + "top_level_call_sites": { + "BuildPart": 4, + "Cylinder": 4, + "Locations": 4, + "CounterSinkHole": 2, + "Hole": 1, + "CounterBoreHole": 1, + "Compound": 1 + }, + "method_call_sites": { + "center": 1, + "sort_by": 1, + "faces": 1 + }, + "operator_syntax_counts": {} + }, + { + "id": 50, + "name": "螺旋压缩弹簧", + "file": "models/50_spring.py", + "source_url": "https://github.com/gumyr/build123d/blob/38effd343e1508964673faf59300a5cc68fbedc1/examples/spring.py", + "builder_context_present": false, + "top_level_call_sites": { + "Helix": 1, + "Circle": 1, + "sweep": 1 + }, + "method_call_sites": {}, + "operator_syntax_counts": { + "Mult": 1, + "BitXor": 1 + } + } + ] + }, + "verification": { + "source_report": { + "path": "tools/research/build123dftc/out/validate.json", + "sha256": "a0927de455277a4878768d6eba21c912e2f5af273b1f9c5817653ff80019fe18" + }, + "accepted_models": [ + "01_rectangular_plate", + "02_plate_with_hole", + "03_prismatic_solid", + "04_lines_and_arcs_profile", + "05_offset_cutouts", + "06_point_list_holes", + "07_polygon_holes", + "08_i_beam", + "09_fillets_and_chamfers", + "10_filleted_hole", + "11_grid_polygon_holes", + "12_spline_profile", + "13_countersink_counterbore", + "14_swept_square_tube", + "15_mirrored_profile", + "16_mirrored_3d_parts", + "17_face_mirror", + "18_face_cutout", + "19_vertex_cutouts", + "20_offset_plane_cylinder", + "21_crossed_cylinders", + "22_angled_drill_holes", + "23_revolved_profile", + "24_circle_rectangle_loft", + "25_offset_polygon_layers", + "26_thin_wall_box", + "27_split_plate", + "28_face_located_sphere_holes", + "29_occ_bottle", + "30_bezier_profile", + "31_nested_location_pattern", + "32_variable_height_pattern", + "33_function_generated_pattern", + "34_embossed_debossed_text", + "35_curved_slots", + "36_extrude_until", + "37_positioned_sketches", + "38_rotated_boxes", + "39_pegboard_j_hook", + "40_handle", + "41_pillow_block", + "42_din_rail", + "43_lego", + "44_loft", + "45_key_cap", + "47_vase", + "48_circuit_board", + "49_holes", + "50_spring" + ], + "rejected_or_unverified_models": [ + "46_tea_cup" + ], + "scope": "Sample-level acceptance is not a claim of all-API or all-parameter coverage.", + "verified_top_level_entries": 48, + "verified_public_member_entries": 23 + } +} diff --git a/tools/research/build123dftc/b3ftc/_reference_worker.py b/tools/research/build123dftc/b3ftc/_reference_worker.py new file mode 100644 index 00000000..d034791a --- /dev/null +++ b/tools/research/build123dftc/b3ftc/_reference_worker.py @@ -0,0 +1,44 @@ +"""Compare two STEP files in an independently selected OCCT environment.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from _validate_worker import _compare_reference, _read_step, _solid_count, _volume + + +def run(actual_step, reference_step): + import OCP + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + actual = _read_step(actual_step) + bounds = Bnd_Box() + BRepBndLib.AddOptimal_s(actual, bounds, False, False) + low, high = bounds.CornerMin(), bounds.CornerMax() + bbox = [float(getattr(point, axis)()) for point in (low, high) for axis in "XYZ"] + return {"status": "ok", **_compare_reference([actual], reference_step), + "reference_kernel": getattr(OCP, "__version__", "unknown"), + "roundtrip_actual_volume": _volume(actual), + "roundtrip_actual_bbox": bbox, + "roundtrip_actual_solid_count": _solid_count(actual), + "roundtrip_actual_valid": True} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--actual-step", type=Path, required=True) + parser.add_argument("--reference-step", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + result = run(args.actual_step, args.reference_step) + except Exception as exc: + result = {"status": "reference_error", "reference_status": "error", + "reference_error": f"{type(exc).__name__}: {exc}"} + args.output.write_text(json.dumps(result, ensure_ascii=False, allow_nan=False) + "\n") + return 0 if result["status"] == "ok" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/research/build123dftc/b3ftc/_trace_worker.py b/tools/research/build123dftc/b3ftc/_trace_worker.py new file mode 100644 index 00000000..8a5267ef --- /dev/null +++ b/tools/research/build123dftc/b3ftc/_trace_worker.py @@ -0,0 +1,13 @@ +"""Private process boundary. Model output goes to the parent's log, not JSON.""" +import json +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from b3ftc.tracer import trace_file + + +if __name__ == "__main__": + source, output = map(Path, sys.argv[1:]) + payload = trace_file(source) + output.write_text(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8") diff --git a/tools/research/build123dftc/b3ftc/_validate_worker.py b/tools/research/build123dftc/b3ftc/_validate_worker.py new file mode 100644 index 00000000..fd6371f6 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/_validate_worker.py @@ -0,0 +1,296 @@ +"""Isolated CAD worker; communicates only through an explicit JSON file.""" +from __future__ import annotations + +import argparse +import importlib.util +import json +import math +from pathlib import Path +import shutil +import traceback +import uuid + + +def _volume(shape): + if not _solid_count(shape): + from OCP.TopAbs import TopAbs_FACE + from OCP.TopExp import TopExp_Explorer + if TopExp_Explorer(shape, TopAbs_FACE).More(): + raise ValueError("volume result has faces but no closed solid") + return 0.0 + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + properties = GProp_GProps() + error = BRepGProp.VolumePropertiesGK_s(shape, properties, 1e-9, True, True) + if error < 0: + raise ValueError("Gauss-Kronrod volume integration failed") + return float(properties.Mass()) + + +def _area(shape): + from OCP.BRepGProp import BRepGProp + from OCP.BRepBuilderAPI import BRepBuilderAPI_Copy + from OCP.GeomAbs import GeomAbs_C1 + from OCP.GProp import GProp_GProps + from OCP.ShapeUpgrade import ShapeUpgrade_ShapeDivideContinuity + # Integration must respect C0 knot boundaries. Work on independent geometry + # so this numerical conditioning never changes the FTC or exported STEP. + copied = BRepBuilderAPI_Copy(shape, True, False).Shape() + divide = ShapeUpgrade_ShapeDivideContinuity(copied) + divide.SetSurfaceCriterion(GeomAbs_C1) + divide.SetBoundaryCriterion(GeomAbs_C1) + divide.SetPCurveCriterion(GeomAbs_C1) + divide.Perform() + measurement = divide.Result() + if measurement.IsNull(): + raise ValueError("surface integration continuity split produced no shape") + properties = GProp_GProps() + BRepGProp.SurfaceProperties_s(S=measurement, SProps=properties, Eps=1e-10, SkipShared=False) + return float(properties.Mass()) + + +def _solid_count(shape): + from OCP.TopAbs import TopAbs_SOLID + from OCP.TopExp import TopExp_Explorer + explorer = TopExp_Explorer(shape, TopAbs_SOLID) + count = 0 + while explorer.More(): + count += 1 + explorer.Next() + return count + + +def _read_step(step): + from OCP.BRepCheck import BRepCheck_Analyzer + from OCP.IFSelect import IFSelect_RetDone + from OCP.STEPControl import STEPControl_Reader + if not step.is_file(): + raise FileNotFoundError(f"reference STEP does not exist: {step}") + reader = STEPControl_Reader() + if reader.ReadFile(str(step)) != IFSelect_RetDone or reader.TransferRoots() == 0: + raise ValueError("reference STEP could not be read") + reference = reader.OneShape() + if reference.IsNull() or not BRepCheck_Analyzer(reference).IsValid() or not _solid_count(reference): + raise ValueError("reference STEP contains no valid solids") + from OCP.BRepClass3d import BRepClass3d_SolidClassifier + from OCP.TopAbs import TopAbs_SOLID, TopAbs_OUT + from OCP.TopExp import TopExp_Explorer + explorer = TopExp_Explorer(reference, TopAbs_SOLID) + while explorer.More(): + classifier = BRepClass3d_SolidClassifier(explorer.Current()) + classifier.PerformInfinitePoint(1e-7) + if classifier.State() != TopAbs_OUT: + raise ValueError("reference STEP solid orientation includes the infinite exterior") + explorer.Next() + return reference + + +def _compare_reference(shapes, step): + return _compare_shapes(shapes, _read_step(step)) + + +def _compare_shapes(shapes, reference): + from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse + from OCP.BRepCheck import BRepCheck_Analyzer + # Treat body groups as a geometric set. Fuse disjoint bodies too, avoiding double + # counting overlap when comparing a multi-body result to STEP assemblies. + actual = shapes[0] + for shape in shapes[1:]: + fusion = BRepAlgoAPI_Fuse(actual, shape) + fusion.Build() + if not fusion.IsDone(): + raise ValueError("could not combine actual bodies for comparison") + actual = fusion.Shape() + tolerance = _kernel_tolerance(actual, reference) + # Configure before the first Build: the two-shape constructor would already + # run the intersection and retain a pave filler computed at default tolerance. + missing = _cut(reference, actual, tolerance) + extra = _cut(actual, reference, tolerance) + for operation in (missing, extra): + if not operation.IsDone() or operation.Shape().IsNull(): + return {"reference_status": "error", "reference_error": "STEP symmetric difference failed", + "reference_boolean_fuzzy_tolerance": tolerance} + if not BRepCheck_Analyzer(operation.Shape()).IsValid(): + return {"reference_status": "error", "reference_error": "STEP symmetric difference produced invalid topology", + "reference_boolean_fuzzy_tolerance": tolerance} + return {"reference_status": "ok", "reference_volume": _volume(reference), + "reference_boolean_fuzzy_tolerance": tolerance, + "symmetric_difference_volume": abs(_volume(missing.Shape())) + abs(_volume(extra.Shape()))} + + +def _cut(left, right, tolerance): + from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut + try: + from OCP.TopTools import TopTools_ListOfShape as ShapeList + except ImportError: + from OCP.collections import List_TopoDS_Shape as ShapeList + arguments, tools = ShapeList(), ShapeList() + arguments.Append(left) + tools.Append(right) + operation = BRepAlgoAPI_Cut() + operation.SetArguments(arguments) + operation.SetTools(tools) + operation.SetNonDestructive(True) + operation.SetFuzzyValue(tolerance) + operation.Build() + return operation + + +def _kernel_tolerance(*shapes): + from OCP.BRep import BRep_Tool + from OCP.Precision import Precision + from OCP.TopAbs import TopAbs_VERTEX, TopAbs_EDGE + from OCP.TopExp import TopExp_Explorer + from OCP.TopoDS import TopoDS + tolerance = float(Precision.Confusion_s()) + for shape in shapes: + for kind, name in ((TopAbs_VERTEX, "Vertex"), (TopAbs_EDGE, "Edge")): + cast = getattr(TopoDS, name + "_s", None) or getattr(TopoDS, name) + explorer = TopExp_Explorer(shape, kind) + while explorer.More(): + value = float(BRep_Tool.Tolerance_s(cast(explorer.Current()))) + if not math.isfinite(value) or value < 0: + raise ValueError("invalid reference geometry tolerance") + tolerance = max(tolerance, value) + explorer.Next() + return tolerance + + +def _export_step(shapes, path, options=None): + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + from OCP.Interface import Interface_Static + from OCP.IFSelect import IFSelect_RetDone + writer = STEPControl_Writer() + options = options or {} + pcurves = options.get("write_pcurves", True) + precision = options.get("precision_mode", "AVERAGE") + modes = {"AVERAGE": 0, "GREATEST": 1, "LEAST": -1, "SESSION": 2} + if not isinstance(pcurves, bool) or precision not in modes: + raise ValueError("invalid source STEP export options") + # Honor the declared corpus export contract: spherical seams retain their + # UV curves; freeform sweeps can explicitly request rebuilding on import. + if not Interface_Static.SetIVal_s("write.surfacecurve.mode", int(pcurves)): + raise ValueError("could not configure STEP parameter-curve export") + if not Interface_Static.SetIVal_s("write.precision.mode", modes[precision]): + raise ValueError("could not configure STEP precision mode") + for shape in shapes: + if writer.Transfer(shape, STEPControl_AsIs) != IFSelect_RetDone: + raise ValueError("could not transfer actual geometry for external comparison") + if writer.Write(str(path)) != IFSelect_RetDone: + raise ValueError("could not export actual geometry for external comparison") + return {"write_pcurves": pcurves, "precision_mode": precision, + "rebuild_pcurves_on_import": not pcurves} + + +def _returned_shapes(value, placement=None): + """Extract world-space topology from the actual build result.""" + from simplecadapi import Solid, Part, Assembly, PartBuildResult, AssemblyBuildResult + from simplecadapi.product.placement import identity_placement + from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform + from OCP.gp import gp_Trsf + if isinstance(value, (PartBuildResult, AssemblyBuildResult)): + value = value.value + if isinstance(value, Part): + value = value.body + if isinstance(value, Assembly): + parent = placement or identity_placement() + return [shape for component in value.components + for shape in _returned_shapes(component.item, parent.compose(component.placement))] + if isinstance(value, Solid): + if placement is None or placement == identity_placement(): + return [value.wrapped] + x, y, z, o = placement.x_axis, placement.y_axis, placement.z_axis, placement.origin + transform = gp_Trsf() + transform.SetValues(x[0], y[0], z[0], o[0], x[1], y[1], z[1], o[1], x[2], y[2], z[2], o[2]) + return [BRepBuilderAPI_Transform(value.wrapped, transform, True).Shape()] + raise ValueError("build() must return an actual Solid, Part or Assembly build result") + + +def run(ftc_path, staging_dir, reference_step=None, export_step=None, export_options=None): + import OCP + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + from OCP.BRepCheck import BRepCheck_Analyzer + from OCP.TopoDS import TopoDS_Shape + # An isolated path keeps imports/artifacts away from original outputs and + # provides a fresh module/global BODIES namespace on every validation. + staged = staging_dir / (ftc_path.stem + "_" + uuid.uuid4().hex + ".py") + shutil.copyfile(ftc_path, staged) + spec = importlib.util.spec_from_file_location("ftc_validation_" + uuid.uuid4().hex, staged) + if spec is None or spec.loader is None: + raise ValueError("cannot import FTC module") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + build = getattr(module, "build", None) + if not callable(build): + raise ValueError("FTC module must define callable build()") + built = build() + bodies = getattr(module, "BODIES", None) + if not isinstance(bodies, (list, tuple)) or not bodies: + return {"status": "ok", "body_count": 0, "solid_count": 0, + "is_valid": False, "volume": None, "bbox": None} + for index, body in enumerate(bodies): + wrapped = getattr(body, "wrapped", None) + if not isinstance(wrapped, TopoDS_Shape) or wrapped.IsNull(): + raise ValueError(f"BODIES[{index}] is not actual nonempty CAD topology") + if _solid_count(wrapped) < 1: + raise ValueError(f"BODIES[{index}] contains no solid") + shapes = _returned_shapes(built) + if not shapes or len(shapes) != len(bodies): + raise ValueError("build() result body count does not match BODIES") + if any(shape.IsNull() or not _solid_count(shape) for shape in shapes): + raise ValueError("build() returned empty CAD topology") + if not all(body.wrapped.IsSame(shape) for body, shape in zip(bodies, shapes)): + from simplecadapi.kernel.ocp_export import make_compound_always + consistency = _compare_shapes(shapes, make_compound_always([body.wrapped for body in bodies])) + if (consistency["reference_status"] != "ok" or + consistency["symmetric_difference_volume"] > max(1e-7, abs(consistency["reference_volume"]) * 1e-7)): + raise ValueError("BODIES geometry does not match the actual build() result") + bounds = Bnd_Box() + for shape in shapes: + BRepBndLib.AddOptimal_s(shape, bounds, False, False) + volume = sum(_volume(shape) for shape in shapes) + low, high = bounds.CornerMin(), bounds.CornerMax() + bbox = [float(getattr(point, axis)()) for point in (low, high) for axis in "XYZ"] + if not math.isfinite(volume) or not all(map(math.isfinite, bbox)): + raise ValueError("nonfinite actual geometry metrics") + result = {"status": "ok", "body_count": len(bodies), + "solid_count": sum(_solid_count(shape) for shape in shapes), + "is_valid": all(BRepCheck_Analyzer(shape).IsValid() for shape in shapes), + "volume": volume, "area": sum(_area(shape) for shape in shapes), "bbox": bbox, + "build_kernel": getattr(OCP, "__version__", "unknown"), + "volume_method": "Gauss-Kronrod, epsilon=1e-9, closed shells, BSpline spans", + "area_method": "Independent geometry copy, C1 continuity spans, adaptive epsilon=1e-10"} + if reference_step is not None: + try: + result.update(_compare_reference(shapes, reference_step)) + except Exception as exc: + result.update(reference_status="error", reference_error=f"{type(exc).__name__}: {exc}") + if export_step is not None: + try: + result["reference_export_options"] = _export_step(shapes, export_step, export_options) + except Exception as exc: + result.update(reference_status="error", reference_error=f"{type(exc).__name__}: {exc}") + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--ftc", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--staging-dir", type=Path, required=True) + parser.add_argument("--reference-step", type=Path) + parser.add_argument("--export-step", type=Path) + parser.add_argument("--export-options", type=json.loads) + args = parser.parse_args() + try: + result = run(args.ftc, args.staging_dir, args.reference_step, args.export_step, args.export_options) + except BaseException as exc: + result = {"status": "build_error", "error": f"{type(exc).__name__}: {exc}", + "traceback": traceback.format_exc(limit=5)} + args.output.write_text(json.dumps(result, ensure_ascii=False, allow_nan=False) + "\n") + return 0 if result.get("status") == "ok" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/research/build123dftc/b3ftc/emit.py b/tools/research/build123dftc/b3ftc/emit.py new file mode 100644 index 00000000..c1abdddb --- /dev/null +++ b/tools/research/build123dftc/b3ftc/emit.py @@ -0,0 +1,152 @@ +"""Deterministic FTC formatting and topology-fingerprint QL selectors. + +This module has no CAD dependency: generated programs use only SimpleCADAPI. +""" +from __future__ import annotations +import math +import re +from typing import Any, Mapping, Sequence + +ROLES = frozenset({'build', 'add', 'subtract', 'intersect', 'modify', 'pattern', 'annotate'}) + + +def fmt_num(value: float) -> str: + number = float(value) + if not math.isfinite(number): + raise ValueError('non-finite geometry parameter') + if abs(number) < 1e-12: + number = 0.0 + return format(number, '.15g') + + +def fmt_vec(vector: Sequence[float]) -> str: + return '(' + ', '.join(fmt_num(v) for v in vector) + (',' if len(vector) == 1 else '') + ')' + + +def fmt_literal(value: Any) -> str: + if value is None or isinstance(value, (bool, str)): + return repr(value) + if isinstance(value, (int, float)): + return fmt_num(value) + if isinstance(value, (list, tuple)): + return '[' + ', '.join(fmt_literal(v) for v in value) + ']' + if isinstance(value, Mapping): + return '{' + ', '.join(repr(k) + ': ' + fmt_literal(v) for k, v in value.items()) + '}' + raise ValueError(f'unsupported literal type: {type(value).__name__}') + + +class SlugAllocator: + def __init__(self) -> None: + self.counts: dict[str, int] = {} + + def allocate(self, name: str) -> str: + slug = re.sub('[^a-z0-9]+', '-', str(name).lower()).strip('-') or 'feature' + count = self.counts.get(slug, 0) + 1 + self.counts[slug] = count + return slug if count == 1 else f'{slug}-{count}' + + +def block_header(slug: str, role: str, tiers: Sequence[str] = ()) -> str: + if role not in ROLES: + raise ValueError(f'invalid FTC role: {role}') + return f" # ---- feature: {slug} ({', '.join([role, *tiers])}) ----" + + +def fingerprint_selector(fingerprints: Sequence[Mapping[str, Any]], kind: str, tolerance: float = 1e-4) -> str: + """Union individual fingerprint predicates; never select a wide bounding band. + + Each traced edge/face gets its own conjunction. This preserves a disjoint + selected set whose bounding box could otherwise include unrelated topology. + """ + predicates = [] + seen = set() + for fp in fingerprints: + if fp.get('kind', kind) != kind: + continue + terms = [] + geom_type = fp.get('geom_type') or fp.get('type') + if geom_type: + terms.append(f"ql.prop('geom.type', '==', {str(geom_type).upper()!r})") + if kind == 'solid' and fp.get('bbox') is not None: + for index, value in enumerate(fp['bbox']): + side, axis = ('min' if index < 3 else 'max'), 'xyz'[index % 3] + terms.extend([ + f"ql.prop('geom.bbox.{side}.{axis}', '>=', {fmt_num(float(value) - tolerance)})", + f"ql.prop('geom.bbox.{side}.{axis}', '<=', {fmt_num(float(value) + tolerance)})", + ]) + for name in ('center', 'normal'): + if kind == 'solid': + continue + values = fp.get(name) + if values is not None: + for axis, value in zip('xyz', values): + tolerance_here = 1e-5 if name == 'normal' else tolerance + value = round(float(value), 10) + terms.extend([ + f"ql.prop('geom.{name}.{axis}', '>=', {fmt_num(value - tolerance_here)})", + f"ql.prop('geom.{name}.{axis}', '<=', {fmt_num(value + tolerance_here)})", + ]) + for name in ('length', 'area', 'radius', 'volume'): + if kind == 'solid' and name != 'volume': + continue + if (kind == 'edge' and name == 'area') or (kind == 'face' and name == 'length'): + continue + if fp.get(name) is not None: + value = float(fp[name]) + delta = max(tolerance, abs(value) * 1e-8) + terms.extend([ + f"ql.prop('geom.{name}', '>=', {fmt_num(value - delta)})", + f"ql.prop('geom.{name}', '<=', {fmt_num(value + delta)})", + ]) + if not terms: + raise ValueError(f'{kind} selection lacks geometric fingerprints') + predicate = 'ql.and_(' + ', '.join(terms) + ')' + if predicate not in seen: + seen.add(predicate) + predicates.append(predicate) + if not predicates: + raise ValueError(f'{kind} selection is empty') + combined = predicates[0] if len(predicates) == 1 else 'ql.or_(' + ', '.join(predicates) + ')' + count = len([fp for fp in fingerprints if fp.get('kind', kind) == kind]) + limit = '' if kind == 'solid' else f'.take({count})' + return f'ql.{kind}s().where({combined}){limit}.exactly({count})' + + +def emit_module(stem: str, lines: Sequence[str], refs: Sequence[str], unsupported: Sequence[str], uses_ql: bool, solid_count: int = 1) -> str: + part_id = 'b3d-' + (re.sub('[^A-Za-z0-9._-]+', '-', stem).strip('-.') or 'part') + result = [f'"""FTC generated from build123d feature trace: {stem.replace(chr(34), chr(39))}."""', + '', 'from pathlib import Path', '', 'import simplecadapi as scad'] + if uses_ql: + result.append('from simplecadapi import ql') + result.extend(['', 'BODIES = None', '', '']) + if solid_count <= 1: + result.extend([f"@scad.part(id={part_id!r}, revision='1.0.0', project_root=Path(__file__).parent)", + 'def build() -> scad.Part:']) + else: + result.append('def _build_bodies():') + result.extend(lines) + for note in unsupported: + result.append(' # untranslated: ' + str(note).replace('\n', ' ')) + if refs: + result.extend([f' bodies = [_b for _group in [{", ".join(refs)}] for _b in _group]', + ' global BODIES', ' BODIES = list(bodies)', + ' return bodies[0]' if solid_count <= 1 else ' return bodies']) + else: + result.append(" raise RuntimeError('feature tree produced no solid')") + if solid_count > 1: + for index in range(solid_count): + result.extend(['', '', f"@scad.part(id={f'{part_id}-body-{index + 1}'!r}, revision='1.0.0', project_root=Path(__file__).parent)", + f'def _build_body_{index + 1}() -> scad.Part:', + f' return _build_bodies()[{index}]']) + result.extend(['', '', 'def build():', + ' parts = [' + ', '.join(f'_build_body_{i + 1}()' for i in range(solid_count)) + ']', + f" @scad.assemble(id={part_id!r}, revision='1.0.0', definitions=parts, project_root=Path(__file__).parent)", + ' def _build_assembly():', + ' # ---- feature: body-components (build) ----', + f' assembly = scad.make_assembly_rassembly(assembly_id={part_id!r})', + ' for index, item in enumerate(parts):', + " component_id = f'body-{index + 1}'", + ' assembly = scad.add_component_rassembly(assembly=assembly, item=item.value, component_id=component_id, placement=scad.identity_placement_rplacement())', + ' assembly = scad.ground_component_rassembly(assembly=assembly, component_id=component_id)', + ' return assembly', ' return _build_assembly()']) + return '\n'.join(result) + '\n' diff --git a/tools/research/build123dftc/b3ftc/geometry.py b/tools/research/build123dftc/b3ftc/geometry.py new file mode 100644 index 00000000..7cf96f89 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/geometry.py @@ -0,0 +1,224 @@ +"""Kernel-independent JSON descriptions of build123d profile geometry. + +The trace contains analytic curves and NURBS control data, never a saved BRep. +Coordinates are in the coordinate system of the actual feature input. +""" +from __future__ import annotations + +from enum import Enum +import math +from typing import Any + + +def vector(value: Any) -> list[float]: + if hasattr(value, "X"): + return [float(getattr(value, n)() if callable(getattr(value, n)) else getattr(value, n)) for n in "XYZ"] + if hasattr(value, "x"): + return [float(value.x), float(value.y), float(value.z)] + return [float(value.X()), float(value.Y()), float(value.Z())] + + +def plane(value: Any) -> dict[str, Any]: + return {"origin": vector(value.origin), "x_dir": vector(value.x_dir), + "y_dir": vector(value.y_dir), "normal": vector(value.z_dir)} + + +def location_matrix(value: Any) -> list[list[float]]: + trsf = value.wrapped.Transformation() + return [[float(trsf.Value(i, j)) for j in range(1, 5)] for i in range(1, 4)] + [[0., 0., 0., 1.]] + + +def serialize(value: Any) -> Any: + if value is None or isinstance(value, (str, bool, int, float)): + return value + if isinstance(value, Enum): + return value.name + if isinstance(value, dict): + return {str(k): serialize(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [serialize(v) for v in value] + name = type(value).__name__ + if name == "Plane": + return plane(value) + if name == "Axis": + return {"origin": vector(value.position), "direction": vector(value.direction)} + if name in {"Location", "Rotation", "Pos", "Rot"} or hasattr(value, "wrapped") and hasattr(value.wrapped, "Transformation"): + return {"matrix": location_matrix(value)} + if name == "Matrix": + return {"matrix": [[float(value.wrapped.Value(i, j)) for j in range(1, 5)] for i in range(1, 4)] + [[0., 0., 0., 1.]]} + if name == "Vector": + return vector(value) + if hasattr(value, "wrapped"): + return {"shape_type": name} + return {"type": name, "repr": repr(value)} + + +def metrics(shape: Any) -> dict[str, Any]: + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + shapes = shape if isinstance(shape, (list, tuple)) else [shape] + solids = [s for item in shapes if item is not None for s in item.solids()] + if not solids: + return {"volume": 0., "bbox": None, "solid_count": 0} + boxes = [s.bounding_box() for s in solids] + low = [min(vector(box.min)[i] for box in boxes) for i in range(3)] + high = [max(vector(box.max)[i] for box in boxes) for i in range(3)] + volumes, areas = [], [] + for solid in solids: + properties = GProp_GProps() + BRepGProp.VolumePropertiesGK_s(solid.wrapped, properties, 1e-9, True, True) + volumes.append(float(properties.Mass())) + areas.append(surface_area(solid.wrapped)) + return {"volume": sum(volumes), "volume_gk": sum(volumes), "area": sum(areas), "default_volume": sum(float(s.volume) for s in solids), "bbox": low + high, + "solid_count": len(solids)} + + +def surface_area(wrapped: Any) -> float: + """Integrate per smooth span on a geometry copy, preserving the source model. + + OCCT's adaptive surface integrator can miss rational spline knot breaks. + Splitting C0 boundaries/surfaces into C1 spans resolves those breaks before + integration. This temporary topology is used only for the numeric metric. + """ + from OCP.BRepBuilderAPI import BRepBuilderAPI_Copy + from OCP.ShapeUpgrade import ShapeUpgrade_ShapeDivideContinuity + from OCP.GeomAbs import GeomAbs_C1 + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + copied = BRepBuilderAPI_Copy(wrapped, True, False).Shape() + divider = ShapeUpgrade_ShapeDivideContinuity(copied) + divider.SetSurfaceCriterion(GeomAbs_C1) + divider.SetBoundaryCriterion(GeomAbs_C1) + divider.SetPCurveCriterion(GeomAbs_C1) + divider.Perform() + properties = GProp_GProps() + BRepGProp.SurfaceProperties_s(S=divider.Result(), SProps=properties, Eps=1e-10, SkipShared=False) + return float(properties.Mass()) + + +def fingerprint(shape: Any, parent: Any = None) -> dict[str, Any]: + from build123d import CenterOf + kind = type(shape).__name__.lower() + payload = {"kind": kind, "geom_type": shape.geom_type.name, + "center": vector(shape.center(CenterOf.MASS)) if kind in {"edge", "face"} else vector(shape.center()), + "bbox": vector(shape.bounding_box().min) + vector(shape.bounding_box().max)} + if kind in {"edge", "wire"}: + payload["length"] = float(shape.length) + payload["start"] = vector(shape.position_at(0)) + payload["mid"] = vector(shape.position_at(.5)) + payload["end"] = vector(shape.position_at(1)) + if kind == "face": + payload["area"] = float(shape.area) + if shape.geom_type.name == "PLANE": + try: + payload["normal"] = vector(shape.normal_at()) + except Exception: + pass + if parent is not None: + children = parent.edges() if kind == "edge" else parent.faces() if kind == "face" else parent.vertices() + for index, child in enumerate(children): + if child.wrapped.IsSame(shape.wrapped): + payload["index"] = index + break + return payload + + +def edge(value: Any) -> dict[str, Any]: + from OCP.BRepAdaptor import BRepAdaptor_Curve + adaptor = BRepAdaptor_Curve(value.wrapped) + kind = value.geom_type.name.lower() + first, last = adaptor.FirstParameter(), adaptor.LastParameter() + common = {"start": vector(value.position_at(0)), "end": vector(value.position_at(1)), + "parameter_range": [float(first), float(last)], "reversed": not value.is_forward} + if kind == "line": + return {"kind": "line", **common} + if kind == "circle": + circle = adaptor.Circle() + data = {"center": vector(circle.Location()), "radius": float(circle.Radius()), + "normal": vector(circle.Axis().Direction()), "x_dir": vector(circle.XAxis().Direction())} + if abs(last - first - 2 * math.pi) < 1e-7: + return {"kind": "circle", **common, **data} + return {"kind": "arc", "mid": vector(value.position_at(.5)), **common, **data} + if kind == "ellipse" and abs(last - first - 2 * math.pi) < 1e-7: + ellipse = adaptor.Ellipse() + return {"kind": "ellipse", **common, "center": vector(ellipse.Location()), + "major_radius": float(ellipse.MajorRadius()), "minor_radius": float(ellipse.MinorRadius()), + "x_dir": vector(ellipse.XAxis().Direction()), "y_dir": vector(ellipse.YAxis().Direction()), + "normal": vector(ellipse.Axis().Direction())} + if kind in {"bspline", "bezier", "ellipse", "parabola", "hyperbola"}: + # Segmentation preserves the exact trimmed curve, including NURBS weights. + # Conic arcs become exact rational BSplines rather than sampled polylines. + if kind not in {"bspline", "bezier"}: + from OCP.Geom import Geom_TrimmedCurve + from OCP.GeomConvert import GeomConvert + from OCP.BRep import BRep_Tool + curve = BRep_Tool.Curve_s(value.wrapped, first, last) + spline = GeomConvert.CurveToBSplineCurve_s(Geom_TrimmedCurve(curve, first, last)) + kind = "bspline" + else: + spline = adaptor.BSpline() if kind == "bspline" else adaptor.Bezier() + unclamped = kind == "bspline" and not spline.IsPeriodic() and (spline.Multiplicity(1) < spline.Degree()+1 or spline.Multiplicity(spline.NbKnots()) < spline.Degree()+1) + if unclamped or abs(first - spline.FirstParameter()) > 1e-9 or abs(last - spline.LastParameter()) > 1e-9: + spline = spline.Copy() + spline.Segment(first, last) + # Converted/segmented poles already describe exactly the edge extent. + common["parameter_range"] = [float(spline.FirstParameter()), float(spline.LastParameter())] + data = {"kind": kind, **common, "degree": spline.Degree(), + "poles": [vector(spline.Pole(i)) for i in range(1, spline.NbPoles() + 1)], + "weights": [float(spline.Weight(i)) for i in range(1, spline.NbPoles() + 1)]} + if kind == "bspline": + data.update(knots=[float(spline.Knot(i)) for i in range(1, spline.NbKnots() + 1)], + multiplicities=[spline.Multiplicity(i) for i in range(1, spline.NbKnots() + 1)], + periodic=spline.IsPeriodic()) + return data + # Conics are kept analytic as a full parameterized curve description. + if kind in {"parabola", "hyperbola"}: + conic = adaptor.Parabola() if kind == "parabola" else adaptor.Hyperbola() + data = {"kind": kind, **common, "center": vector(conic.Location()), + "x_dir": vector(conic.XAxis().Direction()), "normal": vector(conic.Axis().Direction())} + if kind == "parabola": + data["focal"] = float(conic.Focal()) + else: + data.update(major_radius=float(conic.MajorRadius()), minor_radius=float(conic.MinorRadius())) + return data + raise ValueError(f"Unsupported profile curve: {value.geom_type.name}") + + +def wire(value: Any) -> dict[str, Any]: + from build123d import Edge + from OCP.BRepTools import BRepTools_WireExplorer + explorer = BRepTools_WireExplorer(value.wrapped) + edges = [] + while explorer.More(): + edges.append(edge(Edge(explorer.Current()))) + explorer.Next() + return {"edges": edges, "closed": bool(value.is_closed)} + + +def profile(value: Any) -> dict[str, Any]: + from build123d import Face, Plane, Vertex, Wire + if isinstance(value, Vertex): + return {"kind": "vertex", "point": vector(value.center())} + if isinstance(value, Wire): + value = Face(value) + if not isinstance(value, Face): + raise ValueError(f"A planar profile was expected, got {type(value).__name__}") + frame = Plane(value) + return {"kind": "face", **plane(frame), + "wires": [wire(value.outer_wire())] + [wire(w) for w in value.inner_wires()]} + + +def path(value: Any) -> dict[str, Any]: + from build123d import Edge, Wire + if isinstance(value, Edge): + return {"edges": [edge(value)], "closed": bool(value.is_closed)} + if not isinstance(value, Wire): + value = Wire(value.edges()) + result = wire(value) + try: + frame = value.find_plane() + if frame is not None: + result.update(planar=True, **plane(frame)) + except Exception: + pass + return result diff --git a/tools/research/build123dftc/b3ftc/profiles.py b/tools/research/build123dftc/b3ftc/profiles.py new file mode 100644 index 00000000..2fe77107 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/profiles.py @@ -0,0 +1,258 @@ +"""Exact profile reconstruction through native sketch/curve APIs. + +Planar input curves are transcribed into a local sketch, including rational +B-splines and ellipses. Nonplanar sweep paths remain geometry-tier wires. +No tessellation or final-body boundary reconstruction is used. +""" +from __future__ import annotations +import math +from typing import Any, Mapping, Sequence +from .emit import fmt_literal, fmt_num, fmt_vec + + +def dot(a, b): + return sum(float(x) * float(y) for x, y in zip(a, b)) + + +def cross(a, b): + return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) + + +def unit(a): + length = math.sqrt(dot(a, a)) + if length < 1e-12: + raise ValueError('degenerate frame axis') + return tuple(float(x) / length for x in a) + + +def frame(profile): + normal = unit(profile.get('normal', (0, 0, 1))) + u = profile.get('u') or profile.get('x_dir') + if u is None: + seed = (0, 1, 0) if abs(normal[0]) > .8 else (1, 0, 0) + u = unit(cross(seed, normal)) + else: + u = unit(u) + v = unit(profile.get('v') or profile.get('y_dir') or cross(normal, u)) + return profile.get('origin', (0, 0, 0)), u, v, normal + + +def circumcenter(a, b, c): + determinant = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])) + if abs(determinant) < 1e-15: + raise ValueError('arc points are collinear') + squared = [dot(p, p) for p in (a, b, c)] + return ((squared[0] * (b[1] - c[1]) + squared[1] * (c[1] - a[1]) + squared[2] * (a[1] - b[1])) / determinant, + (squared[0] * (c[0] - b[0]) + squared[1] * (a[0] - c[0]) + squared[2] * (b[0] - a[0])) / determinant) + + +def oriented_spline(edge): + """Align basis poles with traced edge traversal before binding endpoints. + + OCCT adaptor poles retain increasing parameter order even when a wire + traverses the edge backwards. Swapping only its endpoint refs distorts + cubic and higher-order curves, so reverse the entire exact basis. + """ + poles = edge.get('poles') or edge.get('control_points') + start, end = edge.get('start'), edge.get('end') + if not poles or start is None or end is None or math.dist(start, end) < 1e-9: + return edge + if math.dist(start, poles[-1]) > 1e-7 or math.dist(end, poles[0]) > 1e-7: + return edge + changed = dict(edge, poles=list(reversed(poles))) + if edge.get('weights'): + changed['weights'] = list(reversed(edge['weights'])) + if edge.get('knots'): + total = edge['knots'][0] + edge['knots'][-1] + changed['knots'] = [total - knot for knot in reversed(edge['knots'])] + if edge.get('multiplicities'): + changed['multiplicities'] = list(reversed(edge['multiplicities'])) + return changed + + +class Sketch: + def __init__(self, name, profile): + self.name = name + self.origin, self.u, self.v, self.normal = frame(profile) + plane_expr = "{'origin': " + fmt_vec(self.origin) + ", 'x_axis': " + fmt_vec(self.u) + ", 'y_axis': " + fmt_vec(self.v) + "}" + self.lines = [f" {name} = scad.make_sketch_rsketch(name={name!r}, plane={plane_expr})"] + self.points = {} + self.counter = 0 + self.entities = 0 + self.snapshot_entities = [] + + def local(self, point): + delta = [float(x) - float(y) for x, y in zip(point, self.origin)] + if abs(dot(delta, self.normal)) > 1e-5: + raise ValueError('nonplanar curve cannot be represented by sketch') + return dot(delta, self.u), dot(delta, self.v) + + def point(self, coordinates): + key = tuple(round(x, 9) for x in coordinates) + if key not in self.points: + self.counter += 1 + point_id = f'p{self.counter}' + self.points[key] = point_id + self.lines.append(f' {self.name} = scad.add_point_rsketch(sketch={self.name}, point_id={point_id!r}, x={fmt_num(coordinates[0])}, y={fmt_num(coordinates[1])})') + self.snapshot_entities.append({'id': point_id, 'kind': 'point', 'xy': list(coordinates)}) + return self.points[key] + + def entity(self, edge): + self.entities += 1 + name = f'e{self.entities}' + prefix = f' {self.name} = scad.' + base = f'sketch={self.name}, entity_id={name!r}' + kind = edge['kind'].lower() + if kind == 'line': + a, b = self.point(self.local(edge['start'])), self.point(self.local(edge['end'])) + self.lines.append(prefix + f'add_line_rsketch({base}, start={a!r}, end={b!r})') + self.snapshot_entities.append({'id': name, 'kind': 'line', 'start': a, 'end': b}) + elif kind in ('arc', 'arc3') or (kind == 'circle' and edge.get('mid') is not None): + a, b, c = [self.local(edge[x]) for x in ('start', 'mid', 'end')] + center = circumcenter(a, b, c) + angles = [math.atan2(p[1] - center[1], p[0] - center[0]) for p in (a, b, c)] + if (angles[1] - angles[0]) % (2 * math.pi) > (angles[2] - angles[0]) % (2 * math.pi): + a, c = c, a + start, end, center_ref = self.point(a), self.point(c), self.point(center) + self.lines.append(prefix + f'add_arc_rsketch({base}, start={start!r}, end={end!r}, center={center_ref!r})') + self.snapshot_entities.append({'id': name, 'kind': 'arc', 'start': start, 'end': end, 'center': center_ref}) + elif kind == 'circle': + center = self.point(self.local(edge['center'])) + self.lines.append(prefix + f'add_circle_rsketch({base}, center={center!r}, radius={fmt_num(edge["radius"])})') + self.snapshot_entities.append({'id': name, 'kind': 'circle', 'center': center, 'radius': edge['radius']}) + elif kind == 'ellipse': + if edge.get('trimmed'): + raise ValueError('trimmed ellipse requires exact trimmed B-spline data') + center3 = edge['center'] + x_dir = edge.get('x_dir') or self.u + y_dir = edge.get('y_dir') or cross(edge.get('normal', self.normal), x_dir) + major3 = [c + edge['major_radius'] * d for c, d in zip(center3, x_dir)] + minor3 = [c + edge['minor_radius'] * d for c, d in zip(center3, y_dir)] + center, major, minor = [self.point(self.local(p)) for p in (center3, major3, minor3)] + self.lines.append(prefix + f'add_ellipse_rsketch({base}, center={center!r}, major_point={major!r}, minor_point={minor!r})') + self.snapshot_entities.append({'id': name, 'kind': 'ellipse', 'center': center, 'major_point': major, 'minor_point': minor}) + elif kind in ('bspline', 'bezier'): + edge = oriented_spline(edge) + poles = edge.get('poles') or edge.get('control_points') + if not poles: + raise ValueError('B-spline poles missing') + a = self.point(self.local(edge.get('start', poles[0]))) + b = self.point(self.local(edge.get('end', poles[-1]))) + degree = int(edge.get('degree', len(poles) - 1 if kind == 'bezier' else 3)) + knots = edge.get('knots', [0, 1] if kind == 'bezier' else None) + mults = edge.get('multiplicities', [degree + 1, degree + 1] if kind == 'bezier' else None) + self.lines.append(prefix + f'add_bspline_rsketch({base}, start={a!r}, end={b!r}, control_points={fmt_literal([self.local(p) for p in poles])}, degree={degree}, knots={fmt_literal(knots)}, multiplicities={fmt_literal(mults)}, weights={fmt_literal(edge.get("weights"))}, periodic={bool(edge.get("periodic", False))!r})') + self.snapshot_entities.append({'id': name, 'kind': 'bspline', 'start': a, 'end': b, + 'control_points': [self.local(p) for p in poles], 'degree': degree, + 'knots': knots, 'multiplicities': mults, 'weights': edge.get('weights'), 'periodic': edge.get('periodic', False)}) + else: + raise ValueError(f'unsupported exact curve kind: {kind}') + + +def _polygon_loops(profile, wires, name, snapshots): + """Compact native sketch construction for a large set of closed polygons.""" + sketches = [Sketch(f'{name}_loop{i}_sketch', profile) for i in range(len(wires))] + points = [] + for sketch, wire in zip(sketches, wires): + edges = wire['edges'] + if any(edge['kind'] != 'line' or math.dist(edge['end'], following['start']) > 1e-7 + for edge, following in zip(edges, edges[1:] + edges[:1])): + return None + points.append([sketch.local(edge['start']) for edge in edges]) + if snapshots is not None: + for sketch, wire in zip(sketches, wires): + edges = wire['edges'] + for edge in edges: + sketch.entity(edge) + snapshots.append({'name': sketch.name, 'entities': sketch.snapshot_entities, + 'closed': True, 'compact': True, + 'plane': {'origin': list(sketch.origin), 'x_axis': list(sketch.u), 'y_axis': list(sketch.v)}}) + origin, u, v, normal = frame(profile) + plane_expr = "{'origin': " + fmt_vec(origin) + ", 'x_axis': " + fmt_vec(u) + ", 'y_axis': " + fmt_vec(v) + "}" + data, refs = name + '_polygon_points', name + '_loop_wires' + lines = [f' {data} = [', *[f' {fmt_literal(loop)},' for loop in points], ' ]', + f' {refs} = []', f' for _loop_index, _points in enumerate({data}):', + f' _loop_sketch = scad.make_sketch_rsketch(name={name!r} + f"_loop{{_loop_index}}_sketch", plane={plane_expr})', + ' for _point_index, (_x, _y) in enumerate(_points):', + ' _loop_sketch = scad.add_point_rsketch(sketch=_loop_sketch, point_id=f"p{_point_index + 1}", x=_x, y=_y)', + ' for _edge_index in range(len(_points)):', + ' _loop_sketch = scad.add_line_rsketch(sketch=_loop_sketch, entity_id=f"e{_edge_index + 1}", start=f"p{_edge_index + 1}", end=f"p{(_edge_index + 1) % len(_points) + 1}")', + f' {refs}.append(scad.make_wire_from_sketch_rwire(sketch=_loop_sketch, profile=0))'] + output = name + '_face' + lines.append(f' {output} = scad.make_face_from_wires_rface(outer_wire={refs}[0], inner_wires={refs}[1:], normal={fmt_vec(normal)})') + return lines, output + + +def emit_profile(profile: Mapping[str, Any], name: str, *, as_wire: bool = False, snapshots=None): + """Return source lines + face/wire expression; outer-first wire ordering.""" + wires = profile.get('wires') or [{'edges': profile.get('edges', [])}] + if not wires or not wires[0].get('edges'): + raise ValueError('empty profile') + if as_wire and len(wires) > 1: + raise ValueError('a wire section cannot represent multiple disconnected loops') + if not as_wire and len(wires) > 1 and sum(len(wire.get('edges', [])) for wire in wires) > 256: + # A many-hole face can exceed libslvs' per-system unknown limit even + # without constraints. Promote each supplied loop in its own small + # sketch, then construct the same face from all its exact wires. + if all(wire.get('edges') and all(edge['kind'] == 'line' for edge in wire['edges']) for wire in wires): + compact = _polygon_loops(profile, wires, name, snapshots) + if compact is not None: + return compact + lines, refs = [], [] + for index, wire in enumerate(wires): + loop_lines, ref = emit_profile({**profile, 'wires': [wire]}, f'{name}_loop{index}', + as_wire=True, snapshots=snapshots) + lines.extend(loop_lines) + refs.append(ref) + output = name + '_face' + lines.append(f' {output} = scad.make_face_from_wires_rface(outer_wire={refs[0]}, inner_wires=[{", ".join(refs[1:])}], normal={fmt_vec(frame(profile)[3])})') + return lines, output + sketch = Sketch(name + '_sketch', profile) + profile_ids = [] + for wire in wires: + if not wire.get('edges'): + raise ValueError('empty inner profile') + profile_ids.append(f'e{sketch.entities + 1}') + for edge in wire['edges']: + sketch.entity(edge) + if snapshots is not None: + snapshots.append({'name': sketch.name, 'entities': sketch.snapshot_entities, + 'closed': wires[0].get('closed', not as_wire), + 'plane': {'origin': list(sketch.origin), 'x_axis': list(sketch.u), 'y_axis': list(sketch.v)}}) + output = name + ('_wire' if as_wire else '_face') + api = 'make_wire_from_sketch_rwire' if as_wire else 'make_face_from_sketch_rface' + outer = '0' if len(wires) == 1 else repr(profile_ids[0]) + holes = '' if len(wires) == 1 else f', inner_profiles={profile_ids[1:]!r}' + sketch.lines.append(f' {output} = scad.{api}(sketch={sketch.name}, profile={outer}{holes})') + return sketch.lines, output + + +def emit_path(path: Mapping[str, Any], name: str, *, snapshots=None): + if path.get('normal') is not None and path.get('planar', True): + return emit_profile(path, name, as_wire=True, snapshots=snapshots) + lines, edges = [], [] + path_edges = path.get('edges') or [edge for wire in path.get('wires', []) for edge in wire.get('edges', [])] + for index, edge in enumerate(path_edges): + ref = f'{name}_edge{index}' + kind = edge['kind'] + if kind == 'line': + expression = f'scad.make_line_redge(start={fmt_vec(edge["start"])}, end={fmt_vec(edge["end"])})' + elif kind in ('arc', 'arc3'): + expression = f'scad.make_three_point_arc_redge(start={fmt_vec(edge["start"])}, middle={fmt_vec(edge["mid"])}, end={fmt_vec(edge["end"])})' + elif kind == 'circle': + expression = f'scad.make_circle_redge(center={fmt_vec(edge["center"])}, radius={fmt_num(edge["radius"])}, normal={fmt_vec(edge.get("normal", (0, 0, 1)))})' + elif kind in ('bspline', 'bezier'): + edge = oriented_spline(edge) + degree = int(edge.get('degree', len(edge['poles']) - 1 if kind == 'bezier' else 3)) + expression = (f'scad.make_spline_redge(control_points={fmt_literal(edge["poles"])}, degree={degree}, ' + f'knots={fmt_literal(edge.get("knots"))}, multiplicities={fmt_literal(edge.get("multiplicities"))}, ' + f'weights={fmt_literal(edge.get("weights"))}, periodic={bool(edge.get("periodic", False))!r})') + else: + raise ValueError(f'unsupported nonplanar path curve: {kind}') + lines.append(f' {ref} = {expression}') + edges.append(ref) + if not edges: + raise ValueError('empty sweep path') + output = name + '_wire' + lines.append(f' {output} = scad.make_wire_from_edges_rwire(edges=[{", ".join(edges)}])') + return lines, output diff --git a/tools/research/build123dftc/b3ftc/replay.py b/tools/research/build123dftc/b3ftc/replay.py new file mode 100644 index 00000000..3c903688 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/replay.py @@ -0,0 +1,552 @@ +"""Translate a portable build123d operation trace into explicit FTC dataflow. + +Only recorded feature inputs are reconstructed. Unknown operations block their +own dependent chain instead of being replaced by final-result geometry. +""" +from __future__ import annotations +import math +import re +from typing import Any, Mapping +from .emit import SlugAllocator, block_header, emit_module, fingerprint_selector, fmt_literal, fmt_num, fmt_vec +from .profiles import cross, dot, emit_path, emit_profile, frame, unit + + +def _named(parameters, *names, default=None): + for name in names: + if name in parameters and parameters[name] is not None: + return parameters[name] + return default + + +def _variable(name): + return 'body_' + re.sub('[^a-zA-Z0-9_]', '_', str(name)) + + +def _axis(parameters): + value = parameters.get('axis', (0, 0, 1)) + if isinstance(value, dict): + return value.get('direction', (0, 0, 1)), value.get('origin', (0, 0, 0)) + return value, parameters.get('origin', (0, 0, 0)) + + +def _profiles(step): + profiles = step.get('profiles') or [step.get('profile')] + holes = step.get('inner_wires') or [] + if holes and len(profiles) == 1 and profiles[0]: + profile = dict(profiles[0]) + wires = list(profile.get('wires', [])) + for hole in holes: + wires.extend(hole.get('wires', [hole]) if isinstance(hole, dict) else []) + profile['wires'] = wires + profiles = [profile] + return profiles + + +def _matrix_operations(matrix): + """Decompose an affine matrix into local scale, rotation, translation.""" + if isinstance(matrix, dict): + matrix = matrix.get('matrix') + if len(matrix) == 16 and not isinstance(matrix[0], (list, tuple)): + matrix = [matrix[i:i + 4] for i in range(0, 16, 4)] + columns = [tuple(float(matrix[i][j]) for i in range(3)) for j in range(3)] + scales = [math.sqrt(dot(c, c)) for c in columns] + if min(scales) < 1e-12: + raise ValueError('singular affine transform') + axes = [tuple(x / scale for x in column) for column, scale in zip(columns, scales)] + if any(abs(dot(axes[i], axes[j])) > 1e-8 for i in range(3) for j in range(i + 1, 3)): + raise ValueError('affine shear cannot be represented by native scale/rotation') + if dot(cross(axes[0], axes[1]), axes[2]) < 0: + scales[0] = -scales[0] + axes[0] = tuple(-x for x in axes[0]) + r = [[axes[j][i] for j in range(3)] for i in range(3)] + cosine = max(-1., min(1., (r[0][0] + r[1][1] + r[2][2] - 1) / 2)) + angle = math.acos(cosine) + if angle < 1e-9: + axis = (0, 0, 1) + elif abs(angle - math.pi) < 1e-7: + diagonal = [max(0., (r[i][i] + 1) / 2) for i in range(3)] + pivot = max(range(3), key=lambda i: diagonal[i]) + axis_list = [0., 0., 0.] + axis_list[pivot] = math.sqrt(diagonal[pivot]) + for j in range(3): + if j != pivot: + axis_list[j] = (r[pivot][j] + r[j][pivot]) / (4 * axis_list[pivot]) + axis = unit(axis_list) + else: + axis = unit((r[2][1] - r[1][2], r[0][2] - r[2][0], r[1][0] - r[0][1])) + return scales, axis, math.degrees(angle), tuple(float(matrix[i][3]) for i in range(3)) + + +class TraceReplayer: + def __init__(self, trace: Mapping[str, Any], stem: str, constraints_mode: bool = False): + self.trace = trace + self.stem = stem + self.lines = [] + self.variables = {} + self.steps = {} + self.unsupported = list(trace.get('unsupported', [])) + self.slugs = SlugAllocator() + self.feature_count = 0 + self.uses_ql = False + self.snapshots = [] + self.constraints_mode = constraints_mode + self.constraint_reports = [] + + def profile(self, profile, name, as_wire=False): + if profile.get('kind') == 'vertex': + if not as_wire: + raise ValueError('vertex cannot be a solid profile') + output = name + '_vertex' + self.lines.append(f' {output} = scad.make_point_rvertex({", ".join(f"{axis}={fmt_num(x)}" for axis, x in zip("xyz", profile["point"]))})') + return output + lines, expression = emit_profile(profile, name, as_wire=as_wire, snapshots=self.snapshots) + self.lines.extend(lines) + return expression + + def source(self, step): + ids = step.get('inputs', []) + if isinstance(ids, str): + ids = [ids] + missing = [ref for ref in ids if ref not in self.variables] + if missing: + raise ValueError(f'input feature unavailable: {missing}') + return [self.variables[ref] for ref in ids] + + def build_step(self, step): + op = step['op'].lower() + p = step.get('parameters') or step.get('params') or {} + ident = step.get('id', f's{self.feature_count + 1}') + var = _variable(ident) + inputs = self.source(step) + create = op in {'box', 'cylinder', 'cone', 'sphere', 'torus', 'extrude', 'revolve', 'loft', 'sweep', 'sew_solid', 'extrude_linear', 'extrude_linear_with_rotation', 'extrude_taper', 'extrude_twist', 'extrude_until', 'sweep_multi'} + role = 'build' if create else {'union': 'add', 'fuse': 'add', 'cut': 'subtract', 'intersect': 'intersect', 'compound': 'pattern'}.get(op, 'modify') + tiers = ['profile=geometry'] if op in {'extrude', 'revolve', 'loft', 'sweep', 'sew_solid', 'extrude_linear', 'extrude_linear_with_rotation', 'torus', 'extrude_taper', 'extrude_twist', 'extrude_until', 'sweep_multi'} else [] + if op in {'sweep', 'sweep_multi'}: + tiers.append('path=geometry') + self.lines.append(block_header(self.slugs.allocate(op), role, tiers)) + if op in {'box', 'cylinder', 'cone', 'sphere', 'torus'}: + self.primitive(op, p, var) + elif op in {'extrude', 'extrude_linear'}: + profiles = _profiles(step) + if not all(profiles): + raise ValueError('extrusion profile missing') + direction = _named(p, 'direction', 'vector', 'dir', default=(0, 0, 1)) + distance = _named(p, 'distance', 'amount', 'height') + if distance is None: + distance = math.sqrt(dot(direction, direction)) + if distance < 0: + direction = [-float(x) for x in direction] + distance = -distance + refs = [self.profile(profile, f'{var}_profile{i}') for i, profile in enumerate(profiles)] + self.lines.append(f' {var} = [scad.extrude_rsolid(profile=_profile, direction={fmt_vec(direction)}, distance={fmt_num(distance)}) for _profile in [{", ".join(refs)}]]') + elif op in {'extrude_taper', 'extrude_until', 'extrude_twist', 'extrude_linear_with_rotation'}: + refs = [self.profile(profile, f'{var}_profile{i}') for i, profile in enumerate(_profiles(step))] + if op == 'extrude_taper': + vector = _named(p, 'direction', 'vector', default=(0, 0, 1)) + expression = f'scad.extrude_taper_rsolid(profile=_profile, vector={fmt_vec(vector)}, taper={fmt_num(p["taper"])}, flip_inner={bool(p.get("flip_inner", True))!r})' + elif op == 'extrude_until': + if not inputs: + raise ValueError('until extrusion target missing') + target = f'[_b for _group in [{", ".join(inputs)}] for _b in _group]' + expression = f'scad.extrude_until_rsolid(profile=_profile, target={target}, direction={fmt_vec(p.get("direction", (0, 0, 1)))}, until={str(p.get("until", "NEXT"))!r})' + else: + direction = _named(p, 'normal', 'direction', 'vector', default=(0, 0, 1)) + distance = math.sqrt(dot(direction, direction)) + origin = list(_named(p, 'center', 'origin', default=(0, 0, 0))) + if len(origin) == 2: + origin.append(0) + expression = f'scad.twisted_sweep_rsolid(profile=_profile, distance={fmt_num(distance)}, twist_angle={fmt_num(_named(p, "angle", "twist_angle", default=0))}, axis={fmt_vec(direction)}, origin={fmt_vec(origin)}, guide_radius=1)' + self.lines.append(f' {var} = [{expression} for _profile in [{", ".join(refs)}]]') + elif op == 'revolve': + profiles = _profiles(step) + refs = [self.profile(profile, f'{var}_profile{i}') for i, profile in enumerate(profiles)] + axis, origin = _axis(p) + angle = _named(p, 'angle', 'arc', 'revolution_arc', default=360) + if angle < 0: + angle, axis = -angle, [-float(x) for x in axis] + self.lines.append(f' {var} = [scad.revolve_rsolid(profile=_profile, axis={fmt_vec(axis)}, angle={fmt_num(angle)}, origin={fmt_vec(origin)}) for _profile in [{", ".join(refs)}]]') + elif op == 'loft': + profiles = step.get('profiles') or [] + refs = [self.profile(profile, f'{var}_section{i}', as_wire=True) for i, profile in enumerate(profiles)] + if len(refs) < 2: + raise ValueError('loft needs at least two recorded sections') + self.lines.append(f' {var} = [scad.loft_rsolid(profiles=[{", ".join(refs)}], ruled={bool(p.get("ruled", False))!r})]') + elif op == 'sew_solid': + if step.get('geometry_error'): + raise ValueError(step['geometry_error']) + profiles = step.get('profiles') or [] + if not profiles: + raise ValueError('solid shell requires recorded planar faces') + refs = [self.profile(profile, f'{var}_face{i}') for i, profile in enumerate(profiles)] + self.lines.append(f' {var}_shell = scad.sew_faces_rshell(faces=[{", ".join(refs)}])') + self.lines.append(f' {var} = [scad.make_solid_from_shell_rsolid(shell={var}_shell)]') + elif op in {'sweep', 'sweep_multi'}: + profiles = _profiles(step) + path = step.get('path') + if not path: + raise ValueError('sweep path missing') + path_lines, path_expr = emit_path(path, var + '_path', snapshots=self.snapshots) + self.lines.extend(path_lines) + refs = ([self.profile(profile, f'{var}_profile{i}') for i, profile in enumerate(profiles)] + if op == 'sweep_multi' or not any(len(profile.get('wires', [])) > 1 for profile in profiles) + else []) + frenet = bool(_named(p, 'is_frenet', 'frenet', default=False)) + if op == 'sweep_multi': + binormal = p.get('binormal') + if isinstance(binormal, dict): + raise ValueError('multisection sweep binormal wire missing from portable trace') + self.lines.append(f' {var} = [scad.sweep_multisection_rsolid(profiles=[{", ".join(refs)}], path={path_expr}, is_frenet={frenet!r}, binormal={fmt_literal(binormal)})]') + else: + if p.get('mode') is not None or str(p.get('transition', 'TRANSFORMED')) != 'TRANSFORMED': + raise ValueError('sweep mode/transition needs a native advanced sweep operator') + if any(len(profile.get('wires', [])) > 1 for profile in profiles): + pieces = [] + for index, profile in enumerate(profiles): + loops = [self.profile({**profile, 'wires': [wire]}, f'{var}_loop{index}_{i}') + for i, wire in enumerate(profile['wires'])] + outer = var + f'_outer{index}' + self.lines.append(f' {outer} = [scad.sweep_rsolid(profile={loops[0]}, path={path_expr}, is_frenet={frenet!r})]') + tools = ', '.join(f'scad.sweep_rsolid(profile={loop}, path={path_expr}, is_frenet={frenet!r})' for loop in loops[1:]) + pieces.append(f'scad.boolean_rsolidlist(left={outer}, right=[{tools}], operation="cut")') + self.lines.append(f' {var} = [_b for _group in [{", ".join(pieces)}] for _b in _group]') + else: + self.lines.append(f' {var} = [scad.sweep_rsolid(profile=_profile, path={path_expr}, is_frenet={frenet!r}) for _profile in [{", ".join(refs)}]]') + elif op == 'component': + if len(inputs) != 1: + raise ValueError('component requires one parent feature') + fingerprint = p.get('fingerprint', {}) + if fingerprint.get('bbox') is not None: + self.uses_ql = True + selector = fingerprint_selector([fingerprint], 'solid') + self.lines.append(f' {var} = {selector}.resolve({inputs[0]})') + else: + self.lines.append(f' {var} = [{inputs[0]}[{int(p["index"])}]]') + elif op in {'compound', 'copy', 'clean'}: + self.lines.append(f' {var} = [ _b for _group in [{", ".join(inputs)}] for _b in _group ]') + elif op in {'union', 'fuse', 'cut', 'intersect'}: + if not inputs: + raise ValueError('boolean inputs missing') + if len(inputs) == 1: + self.lines.append(f' {var} = list({inputs[0]})') + elif op == 'cut': + tools = f'[_b for _group in [{", ".join(inputs[1:])}] for _b in _group]' + self.lines.append(f' {var} = scad.boolean_rsolidlist(left={inputs[0]}, right={tools}, operation="cut")') + elif op == 'intersect': + self.lines.append(f' {var} = list({inputs[0]})') + for group in inputs[1:]: + self.lines.append(f' {var} = scad.boolean_rsolidlist(left={var}, right={group}, operation="intersect")') + else: + groups = step.get('result_groups') + if groups: + values = [] + for group in groups: + group_refs = [self.variables[x] for x in group] + values.append(f'scad.union_rsolid([{", ".join(group_refs)}], tracking_policy="graph")' if len(group_refs) > 1 else f'{group_refs[0]}[0]') + self.lines.append(f' {var} = [{", ".join(values)}]') + elif step.get('result', {}).get('solid_count', 1) > 1: + raise ValueError('disconnected union requires recorded connected operand groups') + else: + self.lines.append(f' {var} = [scad.union_rsolid([{", ".join(inputs)}], tracking_policy="graph")]') + elif op in {'fillet', 'chamfer'}: + selector = fingerprint_selector(step.get('selection', []), 'edge') + self.uses_ql = True + if len(inputs) != 1: + raise ValueError('blend requires one parent feature') + if op == 'fillet': + extras = f'radius={fmt_num(p["radius"])}' + else: + extras = f'distance={fmt_num(_named(p, "length", "distance", "length1"))}' + second = _named(p, 'length2', 'distance2') + if second is not None and abs(float(second) - float(_named(p, 'length', 'distance', 'length1'))) > 1e-9: + extras += ', angle=' + fmt_num(math.degrees(math.atan2(float(second), float(_named(p, 'length', 'distance', 'length1'))))) + direction = p.get('reference_direction') + if direction is None: + if p.get('reference_directions'): + raise ValueError('per-edge chamfer reference normals vary; the native operator requires one reference direction') + raise ValueError('asymmetric chamfer requires reference-face normal') + extras += ', reference_direction=' + fmt_vec(direction) + groups = step.get('component_selections') + if groups is not None: + self.lines.append(f' {var} = []') + for index, selection in enumerate(groups): + body = f'{inputs[0]}[{index}]' + if selection: + local = fingerprint_selector(selection, 'edge') + self.lines.append(f' {var}.append(scad.{op}_rsolid(solid={body}, edges={local}, {extras}))') + else: + self.lines.append(f' {var}.append({body})') + elif self.steps[step['inputs'][0]].get('result', {}).get('solid_count', 1) > 1: + raise ValueError('multi-body blend requires per-component selections') + else: + self.lines.append(f' {var} = [scad.{op}_rsolid(solid=_b, edges={selector}, {extras}) for _b in {inputs[0]}]') + elif op == 'draft': + if len(inputs) != 1: + raise ValueError('draft requires one parent feature') + selector = fingerprint_selector(step.get('selection', []), 'face') + self.uses_ql = True + plane = p.get('neutral_plane') or p.get('plane') or {} + angle = _named(p, 'angle', 'draft_angle') + if angle is None or not isinstance(plane, dict): + raise ValueError('draft angle or neutral plane missing') + self.lines.append(f' {var} = [scad.draft_rsolid(solid=_b, faces_to_draft={selector}, angle={fmt_num(angle)}, plane_origin={fmt_vec(plane.get("origin", (0, 0, 0)))}, plane_normal={fmt_vec(plane.get("normal", (0, 0, 1)))}) for _b in {inputs[0]}]') + elif op in {'offset', 'shell'}: + if len(inputs) != 1: + raise ValueError('offset requires one parent feature') + selections = step.get('selection', []) + selector = fingerprint_selector(selections, 'face') if selections else '[]' + self.uses_ql |= bool(selections) + amount = _named(p, 'offset', 'amount', 'thickness', 'distance') + if amount is None: + raise ValueError('offset distance missing') + self.lines.append(f' {var} = [scad.offset_rsolid(solid=_b, amount={fmt_num(amount)}, openings={selector}, tolerance={fmt_num(p.get("tolerance", 1e-4))}, kind={str(p.get("kind", "ARC"))!r}) for _b in {inputs[0]}]') + elif op == 'split': + self.split(step, p, inputs, var) + elif op in {'transform', 'translate', 'rotate', 'mirror', 'scale', 'move', 'moved', 'located', 'locate', 'transform_shape', 'transform_geometry'}: + self.transform(op, p, inputs, var) + else: + raise ValueError(f'unsupported op: {op}') + components = step.get('result_components', []) + if components and all(fp.get('bbox') is not None for fp in components): + # Kernel output order is not stable across environments. Restore the + # traced component order before indexed selections and modifications. + self.uses_ql = True + selectors = [fingerprint_selector([fp], 'solid') for fp in components] + self.lines.append(f' {var} = [{", ".join(f"{selector}.resolve({var})[0]" for selector in selectors)}]') + self.lines.append('') + self.variables[ident] = var + self.steps[ident] = step + self.feature_count += 1 + + def primitive(self, op, p, var): + plane = p.get('plane') or {} + origin = _named(p, 'origin', 'center', default=plane.get('origin', (0, 0, 0))) + axis = _named(p, 'axis', default=plane.get('normal', (0, 0, 1))) + if isinstance(axis, dict): + origin, axis = axis.get('origin', origin), axis.get('direction', (0, 0, 1)) + x_dir = plane.get('x_dir', (1, 0, 0)) + if op == 'box': + length = _named(p, 'length', 'x', default=p.get('width')) + width = _named(p, 'width', 'y', default=p.get('height')) + height = _named(p, 'height', 'z', default=p.get('depth')) + u, v = unit(x_dir), unit(cross(unit(axis), unit(x_dir))) + bottom = [float(origin[i]) + float(length) * u[i] / 2 + float(width) * v[i] / 2 for i in range(3)] + self.lines.extend([f' with scad.SimpleWorkplane(origin=(0, 0, 0), normal={fmt_vec(axis)}, x_dir={fmt_vec(x_dir)}):', + f' {var} = [scad.make_box_rsolid(width={fmt_num(length)}, height={fmt_num(width)}, depth={fmt_num(height)}, bottom_face_center={fmt_vec([dot(bottom, u), dot(bottom, v), dot(bottom, unit(axis))])})]']) + elif op in {'cylinder', 'cone'}: + angle = _named(p, 'angle', 'arc_size', default=360) + if abs(float(angle) - 360) > 1e-9: + axis, x_dir = unit(axis), unit(x_dir) + y_dir = unit(cross(axis, x_dir)) + def world(x, y, z): + return [origin[i] + x * x_dir[i] + y * y_dir[i] + z * axis[i] for i in range(3)] + if op == 'cylinder': + radius = float(p['radius']) + theta = math.radians(float(angle)) + start, mid, end = world(radius, 0, 0), world(radius * math.cos(theta / 2), radius * math.sin(theta / 2), 0), world(radius * math.cos(theta), radius * math.sin(theta), 0) + edges = [{'kind': 'line', 'start': origin, 'end': start}, {'kind': 'arc', 'start': start, 'mid': mid, 'end': end}, {'kind': 'line', 'start': end, 'end': origin}] + profile = {'origin': origin, 'u': x_dir, 'v': y_dir, 'normal': axis, 'wires': [{'edges': edges}]} + face = self.profile(profile, var + '_sector') + self.lines.append(f' {var} = [scad.extrude_rsolid(profile={face}, direction={fmt_vec(axis)}, distance={fmt_num(p["height"])})]') + else: + bottom_radius, top_radius = float(_named(p, 'base_radius', 'radius1', 'bottom_radius')), float(_named(p, 'radius2', 'top_radius', default=0)) + points = [world(0, 0, 0), world(bottom_radius, 0, 0), world(top_radius, 0, float(p['height'])), world(0, 0, float(p['height']))] + edges = [{'kind': 'line', 'start': a, 'end': b} for a, b in zip(points, points[1:] + points[:1]) if sum((x - y) ** 2 for x, y in zip(a, b)) > 1e-20] + profile = {'origin': origin, 'u': x_dir, 'v': axis, 'normal': cross(x_dir, axis), 'wires': [{'edges': edges}]} + face = self.profile(profile, var + '_meridian') + self.lines.append(f' {var} = [scad.revolve_rsolid(profile={face}, axis={fmt_vec(axis)}, angle={fmt_num(angle)}, origin={fmt_vec(origin)})]') + return + if op == 'cylinder': + args = f'radius={fmt_num(p["radius"])}' + else: + args = f'bottom_radius={fmt_num(_named(p, "radius1", "bottom_radius", "base_radius"))}, top_radius={fmt_num(_named(p, "radius2", "top_radius", default=0))}' + self.lines.append(f' {var} = [scad.make_{op}_rsolid({args}, height={fmt_num(p["height"])}, bottom_face_center={fmt_vec(origin)}, axis={fmt_vec(axis)})]') + elif op == 'sphere': + if any(abs(float(p.get(key, default)) - default) > 1e-9 for key, default in [('angle1', -90), ('angle2', 90), ('angle3', 360)]): + radius, axis, x_dir = float(p['radius']), unit(axis), unit(x_dir) + theta1, theta2 = math.radians(float(p.get('angle1', -90))), math.radians(float(p.get('angle2', 90))) + def meridian(theta): + return [origin[i] + radius * math.cos(theta) * x_dir[i] + radius * math.sin(theta) * axis[i] for i in range(3)] + start, mid, end = meridian(theta1), meridian((theta1 + theta2) / 2), meridian(theta2) + bottom_axis = [origin[i] + radius * math.sin(theta1) * axis[i] for i in range(3)] + top_axis = [origin[i] + radius * math.sin(theta2) * axis[i] for i in range(3)] + edges = [{'kind': 'arc', 'start': start, 'mid': mid, 'end': end}] + edges.extend({'kind': 'line', 'start': a, 'end': b} for a, b in [(end, top_axis), (top_axis, bottom_axis), (bottom_axis, start)] if sum((x - y) ** 2 for x, y in zip(a, b)) > 1e-20) + profile = {'origin': origin, 'u': x_dir, 'v': axis, 'normal': cross(x_dir, axis), 'wires': [{'edges': edges}]} + face = self.profile(profile, var + '_meridian') + self.lines.append(f' {var} = [scad.revolve_rsolid(profile={face}, axis={fmt_vec(axis)}, angle={fmt_num(p.get("angle3", 360))}, origin={fmt_vec(origin)})]') + return + self.lines.append(f' {var} = [scad.make_sphere_rsolid(radius={fmt_num(p["radius"])}, center={fmt_vec(origin)})]') + else: + start = _named(p, 'start_angle', 'angle1', default=0) + end = _named(p, 'end_angle', 'angle2', default=360) + if abs(float(start)) > 1e-9 or abs(float(end) - 360) > 1e-9: + raise ValueError('partial torus tube needs a traced meridian closure profile') + major, minor = _named(p, 'major_radius', 'radius1'), _named(p, 'minor_radius', 'radius2') + axis = unit(axis) + x_dir = unit(x_dir) + profile_origin = [o + float(major) * x for o, x in zip(origin, x_dir)] + profile = {'origin': profile_origin, 'u': x_dir, 'v': axis, 'normal': cross(x_dir, axis), + 'wires': [{'edges': [{'kind': 'circle', 'center': profile_origin, 'radius': minor}]}]} + face = self.profile(profile, var + '_meridian') + self.lines.append(f' {var} = [scad.revolve_rsolid(profile={face}, axis={fmt_vec(axis)}, angle={fmt_num(_named(p, "major_angle", "angle3", "angle", default=360))}, origin={fmt_vec(origin)})]') + + def transform(self, op, p, inputs, var): + if len(inputs) != 1: + raise ValueError('transform requires one parent feature') + expression = '_b' + matrix = _named(p, 'matrix', 'matrix4x4', 't_matrix', 'location', 'loc') + if matrix is None and isinstance(p.get('transform'), (dict, list, tuple)): + matrix = p['transform'] + if matrix is not None: + try: + scales, axis, angle, vector = _matrix_operations(matrix) + except ValueError as error: + if 'shear' not in str(error): + raise + raw_matrix = matrix.get('matrix') if isinstance(matrix, dict) else matrix + expression = f'scad.transform_rsolid(solid=_b, matrix={fmt_literal(raw_matrix)})' + self.lines.append(f' {var} = [{expression} for _b in {inputs[0]}]') + return + if any(abs(x - 1) > 1e-10 for x in scales): + expression = f'scad.scale_rsolid(solid={expression}, factors={fmt_vec(scales)}, center=(0, 0, 0))' + if abs(angle) > 1e-10: + expression = f'scad.rotate_shape(shape={expression}, angle={fmt_num(angle)}, axis={fmt_vec(axis)}, origin=(0, 0, 0))' + if any(abs(x) > 1e-10 for x in vector): + expression = f'scad.translate_shape(shape={expression}, vector={fmt_vec(vector)})' + elif op == 'mirror' or p.get('mirror_plane') is not None: + plane = p.get('mirror_plane') or p.get('plane') or {} + expression = f'scad.mirror_shape(shape=_b, plane_origin={fmt_vec(plane.get("origin", (0, 0, 0)))}, plane_normal={fmt_vec(plane.get("normal", (0, 0, 1)))})' + elif op == 'rotate' or p.get('angle') is not None: + axis, origin = _axis(p) + expression = f'scad.rotate_shape(shape=_b, angle={fmt_num(p["angle"])}, axis={fmt_vec(axis)}, origin={fmt_vec(origin)})' + elif op == 'scale' or p.get('method') == 'scale' or p.get('factor') is not None or p.get('factors') is not None: + factors = _named(p, 'factors', 'factor', 'scale', default=1) + if isinstance(factors, (int, float)): + factors = (factors,) * 3 + expression = f'scad.scale_rsolid(solid=_b, factors={fmt_vec(factors)}, center={fmt_vec(_named(p, "center", "about", default=(0, 0, 0)))})' + else: + vector = _named(p, 'vector', 'translation', 'offset') + if vector is None: + raise ValueError('transform has no supported parameters') + expression = f'scad.translate_shape(shape=_b, vector={fmt_vec(vector)})' + self.lines.append(f' {var} = [{expression} for _b in {inputs[0]}]') + + def split(self, step, p, inputs, var): + if len(inputs) != 1: + raise ValueError('split requires one parent feature') + plane = _named(p, 'plane', 'bisect_by', 'tool', default={}) + if not isinstance(plane, dict) or 'normal' not in plane: + raise ValueError('only planar solid splits are supported') + origin, u, v, normal = frame(plane) + source_step = self.steps[step['inputs'][0]] + bbox = source_step.get('result', {}).get('bbox') or self.trace.get('b3d_bbox') + if not bbox or len(bbox) != 6: + raise ValueError('split needs the source feature bounding box') + corners = [(x, y, z) for x in (bbox[0], bbox[3]) for y in (bbox[1], bbox[4]) for z in (bbox[2], bbox[5])] + projected = [[dot([c[i] - origin[i] for i in range(3)], axis) for c in corners] for axis in (u, v, normal)] + margin = max(math.sqrt(sum((bbox[i + 3] - bbox[i]) ** 2 for i in range(3))), 1) + center2 = [(min(a) + max(a)) / 2 for a in projected[:2]] + width, height = [max(a) - min(a) + margin * 2 for a in projected[:2]] + depth = max(abs(x) for x in projected[2]) + margin + tool_origin = [origin[i] + center2[0] * u[i] + center2[1] * v[i] for i in range(3)] + keep = str(p.get('keep', 'BOTH')).upper().split('.')[-1] + refs = [] + component_sides = step.get('result_component_sides') + ordered_sides = component_sides if keep == 'BOTH' and component_sides else ['TOP', 'BOTTOM'] + for side in ordered_sides: + label = str(side).lower() + if label not in {'top', 'bottom'}: + raise ValueError(f'unknown split component side: {side}') + sign = 1 if label == 'top' else -1 + if keep != 'BOTH' and keep != label.upper(): + continue + tool = var + '_' + label + self.lines.extend([f' with scad.SimpleWorkplane(origin={fmt_vec(tool_origin)}, normal={fmt_vec([sign * x for x in normal])}, x_dir={fmt_vec(u)}):', + f' {tool} = scad.make_box_rsolid(width={fmt_num(width)}, height={fmt_num(height)}, depth={fmt_num(depth)}, bottom_face_center=(0, 0, 0))']) + refs.append(f'scad.boolean_rsolidlist(left={inputs[0]}, right=[{tool}], operation="intersect")') + if not refs: + raise ValueError(f'unknown split keep mode: {keep}') + self.lines.append(f' {var} = [_piece for _group in [{", ".join(refs)}] for _piece in _group]') + + def replay(self): + for step in self.trace.get('steps', []): + before = len(self.lines) + before_ql = self.uses_ql + before_sketches = len(self.snapshots) + before_slugs = dict(self.slugs.counts) + try: + self.build_step(step) + for snapshot in self.snapshots[before_sketches:]: + snapshot.update({'feature_id': step.get('id'), 'op': step.get('op'), 'role': 'profile'}) + except (ValueError, KeyError, TypeError, IndexError) as error: + del self.lines[before:] + del self.snapshots[before_sketches:] + self.uses_ql = before_ql + self.slugs.counts = before_slugs + self.unsupported.append(f'{step.get("id", "?")} {step.get("op", "?")}: {error}') + requested = self.trace.get('result_refs') if 'result_refs' in self.trace else self.trace.get('results') + if requested is None: + requested = [self.trace['steps'][-1]['id']] if self.trace.get('steps') else [] + if isinstance(requested, str): + requested = [requested] + refs = [self.variables[x] for x in requested if isinstance(x, str) and x in self.variables] + missing = [x for x in requested if x not in self.variables] + if missing: + self.unsupported.append(f'final result features unavailable: {missing}') + if not refs: + self.unsupported.append('feature tree produced no solid') + if self.constraints_mode: + self.recover_constraints() + count = int(self.trace.get('solid_count') or sum(self.steps[x].get('result', {}).get('solid_count', 1) for x in requested if x in self.steps) or 1) + source = emit_module(self.stem, self.lines, refs, self.unsupported, self.uses_ql, count) + return source, {'status': 'partial' if self.unsupported else 'ok', 'feature_count': self.feature_count, + 'unsupported': self.unsupported, 'result_refs': requested, + 'notes': [], 'schema_version': '1.0', 'solid_count': count, + 'artifact_type': 'assembly' if count > 1 else 'part', + 'sketches': self.snapshots, 'sketch_count': len(self.snapshots), + 'constraint_recovery': self.constraint_reports} + + def recover_constraints(self): + """Reuse the source-agnostic CQ inference engine without changing it. + + It admits only geometry-entailed constraints whose production solver + preserves coordinates. Unsupported entity families retain geometry. + """ + import sys + from pathlib import Path + sibling = str(Path(__file__).resolve().parents[2] / 'cadqueryftc') + if sibling not in sys.path: + sys.path.insert(0, sibling) + from cqftc.constraints import emit_constraint_lines, recover_constraints_guarded + for snapshot in self.snapshots: + if snapshot.get('compact'): + self.constraint_reports.append({'name': snapshot['name'], 'status': 'unsupported', + 'reason': 'constraint emission is unavailable for compact multi-loop sketches', 'constraints': []}) + continue + if any(entity['kind'] not in {'point', 'line', 'circle', 'arc'} for entity in snapshot['entities']): + self.constraint_reports.append({'name': snapshot['name'], 'status': 'unsupported', + 'reason': 'constraint recovery does not handle ellipse/B-spline entities', 'constraints': []}) + continue + try: + report = recover_constraints_guarded(snapshot, timeout=30) + except Exception as error: + report = {'status': 'error', 'reason': str(error), 'constraints': []} + report['name'] = snapshot['name'] + self.constraint_reports.append(report) + if not report.get('coords_ok'): + continue + name = snapshot['name'] + constraints = [line.replace(' s = ', f' {name} = ').replace('sketch=s,', f'sketch={name},') + for line in emit_constraint_lines(report)] + for index, line in enumerate(self.lines): + if f'sketch={name}, profile=' in line and ('make_wire_from_sketch_rwire' in line or 'make_face_from_sketch_rface' in line): + self.lines[index:index] = constraints + break + + +def replay_trace_to_ftc(trace: Any, *, stem: str = 'part', constraints_mode: bool = False): + if hasattr(trace, 'to_dict'): + trace = trace.to_dict() + if not isinstance(trace, Mapping): + raise TypeError('trace must be a portable JSON mapping') + return TraceReplayer(trace, stem, constraints_mode=constraints_mode).replay() diff --git a/tools/research/build123dftc/b3ftc/schema.py b/tools/research/build123dftc/b3ftc/schema.py new file mode 100644 index 00000000..58a7f0c9 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/schema.py @@ -0,0 +1,8 @@ +"""Versioned, portable feature trace contract.""" +SCHEMA_VERSION = "1.0" + + +def empty_trace(): + return {"schema_version": SCHEMA_VERSION, "steps": [], "result_refs": [], + "b3d_status": "ok", "b3d_volume": None, "b3d_bbox": None, + "solid_count": 0, "unsupported": [], "error": ""} diff --git a/tools/research/build123dftc/b3ftc/tracer.py b/tools/research/build123dftc/b3ftc/tracer.py new file mode 100644 index 00000000..fc3a43c5 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/tracer.py @@ -0,0 +1,471 @@ +"""Execute build123d and capture the actual solid construction graph. + +Instrumentation sits at the topology API: Builder, algebra and direct topology +calls consequently share one history. Sketches are transcribed as analytic/NURBS +profiles at feature boundaries. Tracing executes Python; only trusted models +should be supplied. Patches are scoped and restored even on execution failure. +""" +from __future__ import annotations + +from contextlib import contextmanager +import functools +import hashlib +import inspect +from pathlib import Path +import sys +import threading +import traceback +from typing import Any + +from . import geometry +from .schema import SCHEMA_VERSION, empty_trace + +schema_version = SCHEMA_VERSION +_LOCK = threading.RLock() + + +class Recorder: + def __init__(self): + self.trace = empty_trace() + self.depth = 0 + self.refs: dict[int, str] = {} + self.components: dict[int, tuple[str, int, Any]] = {} + self.ref_solids: dict[str, list[Any]] = {} + self.patches: list[tuple[Any, str, Any, bool]] = [] + self.retained: list[Any] = [] + self.serial = 0 + + @staticmethod + def key(shape): + return hash(shape.wrapped) + + @staticmethod + def is_solid(shape): + return hasattr(shape, "solids") and bool(shape.solids()) + + def register(self, shape, ref): + if shape is None: + return + if isinstance(shape, (list, tuple)): + solids = [s for item in shape if item is not None for s in item.solids()] + if len(solids) == 1: + self.register(solids[0], ref) + else: + for index, solid in enumerate(solids): + self.components[self.key(solid)] = (ref, index, solid) + self.retained.append(solid.wrapped) + for item in shape: + if item is not None and len(item.solids()) == 1: + single = item.solids()[0] + if self.key(single) in self.components: + self.components[self.key(item)] = self.components[self.key(single)] + return + if not hasattr(shape, "wrapped") or shape.wrapped is None: + return + self.refs[self.key(shape)] = ref + self.retained.append(shape.wrapped) + solids = shape.solids() + if len(solids) == 1: + self.refs[self.key(solids[0])] = ref + self.retained.append(solids[0].wrapped) + elif len(solids) > 1: + for index, solid in enumerate(solids): + self.components[self.key(solid)] = (ref, index, solid) + self.retained.append(solid.wrapped) + + def emit(self, op, inputs, parameters, result, source, **extra): + import build123d as b + from OCP.TopLoc import TopLoc_Location + results = result if isinstance(result, (list, tuple)) else [result] + solids = [s for obj in results if obj is not None for s in obj.solids()] + if len(solids) > 1: + extra["result_components"] = [geometry.metrics(s) for s in solids] + if op == "union" and len(solids) > 1: + groups = [] + self.depth += 1 + try: + for output in solids: + group = [] + output_box = geometry.metrics(output)["bbox"] + for input_ref in inputs: + for operand in self.ref_solids.get(input_ref, []): + box = geometry.metrics(operand)["bbox"] + if any(box[i] < output_box[i] - 1e-5 or box[i+3] > output_box[i+3] + 1e-5 for i in range(3)): + continue + common = operand.intersect(output) + overlap = geometry.metrics(common)["volume"] if common is not None else 0. + operand_volume = geometry.metrics(operand)["volume"] + if abs(overlap - operand_volume) <= max(1e-6, abs(operand_volume) * 1e-7): + group.extend(self.reference(operand)) + groups.append(list(dict.fromkeys(group))) + finally: + self.depth -= 1 + extra["result_groups"] = groups + self.serial += 1 + ref = f"s{self.serial}" + step = {"id": ref, "op": op, "inputs": list(dict.fromkeys(inputs)), + "parameters": parameters, "source": source, + "result": geometry.metrics(result), **extra} + self.trace["steps"].append(step) + self.register(result, ref) + self.ref_solids[ref] = [b.Solid(s.wrapped.Moved(TopLoc_Location())) for s in solids] + return ref + + def unsupported(self, message, shape=None, source="unknown"): + entry = {"source": source, "reason": message} + if entry not in self.trace["unsupported"]: + self.trace["unsupported"].append(entry) + if shape is not None: + return self.emit("unsupported", [], {"reason": message}, shape, source) + return None + + def reference(self, shape): + if shape is None: + return [] + if isinstance(shape, (list, tuple)): + return [r for item in shape for r in self.reference(item)] + if hasattr(shape, "part") and not hasattr(shape, "wrapped"): + return self.reference(shape.part) + if not hasattr(shape, "wrapped") or shape.wrapped is None or not self.is_solid(shape): + return [] + key = self.key(shape) + if key in self.refs: + return [self.refs[key]] + if key in self.components: + parent, index, solid = self.components[key] + return [self.emit("component", [parent], {"index": index, "fingerprint": geometry.metrics(solid)}, solid, "Shape.solids")] + solids = shape.solids() + known = [self.refs.get(self.key(s)) for s in solids] + if all(known): + if len(set(known)) == 1: + self.register(shape, known[0]) + return [known[0]] + return [self.emit("compound", known, {}, shape, "Compound")] + if all(self.key(s) in self.refs or self.key(s) in self.components for s in solids): + children = [ref for solid in solids for ref in self.reference(solid)] + return [self.emit("compound", children, {}, shape, "Compound")] + ref = self.unsupported(f"Untraced solid construction: {type(shape).__name__}", shape) + return [ref] + + def patch(self, owner, name, replacement): + own_attribute = name in owner.__dict__ + original = inspect.getattr_static(owner, name) + self.patches.append((owner, name, original, own_attribute)) + setattr(owner, name, replacement) + + def bind(self, fn, args, kwargs): + bound = inspect.signature(fn).bind(*args, **kwargs) + bound.apply_defaults() + return dict(bound.arguments) + + def wrap(self, owner, name, op, *, factory=False): + fn = getattr(owner, name) + descriptor = inspect.getattr_static(owner, name) + + @functools.wraps(fn) + def wrapped(*args, **kwargs): + if self.depth: + return fn(*args, **kwargs) + receiver = None if factory else args[0] + if receiver is not None and not self.is_solid(receiver): + return fn(*args, **kwargs) + params = self.bind(fn, args, kwargs) + params.pop("self", None) + # Materialize iterables once: feature generators must still execute normally. + for field in ("objs", "profiles", "edge_list", "faces", "openings", "inner_wires"): + value = params.get(field) + if value is not None and not isinstance(value, (list, tuple)) and not hasattr(value, "wrapped"): + params[field] = list(value) + # Rebind the materialized values to preserve the original call contract. + bound = inspect.signature(fn).bind(*args, **kwargs) + for field, value in params.items(): + if field in bound.arguments: + bound.arguments[field] = value + inputs = self.reference(receiver) if receiver is not None else [] + extra = {} + encoded = {} + try: + for field, value in params.items(): + if field in {"obj", "profile", "section", "surface"} and op in {"extrude", "extrude_taper", "extrude_until", "revolve", "sweep", "extrude_twist", "thicken"}: + extra["profile"] = geometry.profile(value) + elif field in {"objs", "profiles"} and op in {"loft", "sweep_multi"}: + extra["profiles"] = [geometry.profile(v) for v in value] + elif field == "path" and op in {"sweep", "sweep_multi"}: + extra["path"] = geometry.path(value) + elif field in {"edge_list", "faces", "openings"}: + extra["selection"] = [geometry.fingerprint(v, receiver) for v in value or []] + if receiver is not None and len(receiver.solids()) > 1: + kind = "edge" if field == "edge_list" else "face" + extra["component_selections"] = [ + [geometry.fingerprint(v, solid) for v in value or [] + if any(child.wrapped.IsSame(v.wrapped) for child in + (solid.edges() if kind == "edge" else solid.faces()))] + for solid in receiver.solids()] + elif field in {"to_fuse", "to_cut", "to_intersect", "target"}: + inputs.extend(self.reference(value)) + elif field == "inner_wires": + # build123d uses explicit holes only for Wire sections. + # A Face already owns the loops actually consumed by the feature. + import build123d as b + section = params.get("section") + extra["inner_wires"] = ([] if isinstance(section, b.Face) else + [geometry.wire(v) for v in value or []]) + else: + encoded[field] = geometry.serialize(value) + except Exception as exc: + extra["geometry_error"] = str(exc) + self.unsupported(f"Cannot transcribe {name} inputs: {exc}", source=f"{owner.__name__}.{name}") + if op == "chamfer" and params.get("length2") is not None and params["length2"] != params["length"]: + # The native kernel assigns distance1 relative to an adjacent + # face, even when no reference face is supplied by the caller. + import build123d as b + from build123d.topology.three_d import IndexedDataMap_TopoDS_Shape_List_TopoDS_Shape_TopTools_ShapeMapHasher + from OCP.TopExp import TopExp + from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE + adjacency = IndexedDataMap_TopoDS_Shape_List_TopoDS_Shape_TopTools_ShapeMapHasher() + TopExp.MapShapesAndAncestors_s(receiver.wrapped, TopAbs_EDGE, TopAbs_FACE, adjacency) + directions = [] + for selected in params["edge_list"]: + explicit = params.get("face") + reference = explicit if explicit is not None else next(face for face in receiver.faces() if face.wrapped.IsSame(adjacency.FindFromKey(selected.wrapped).First())) + direction = geometry.vector(reference.normal_at(selected.position_at(.5))) + directions.append(direction) + encoded["reference_directions"] = directions + if directions and all(sum((a-b)**2 for a,b in zip(directions[0], item)) < 1e-12 for item in directions): + encoded["reference_direction"] = directions[0] + if op == "transform": + encoded["method"] = name + if name == "scale" and params.get("about") is None: + encoded["about"] = geometry.vector(receiver.location.position) + if name == "transformed": + from OCP.gp import gp_Trsf, gp_Ax1, gp_Pnt, gp_Dir, gp_Vec + import math + rotation = params["rotate"] + if hasattr(rotation, "X"): + rotation = geometry.vector(rotation) + rotations = [] + for i, angle in enumerate(rotation): + trsf = gp_Trsf() + axis = [0., 0., 0.] + axis[i] = 1. + trsf.SetRotation(gp_Ax1(gp_Pnt(), gp_Dir(*axis)), math.radians(angle)) + rotations.append(trsf) + translation = gp_Trsf() + offset = params["offset"] + translation.SetTranslation(gp_Vec(*(geometry.vector(offset) if hasattr(offset, "X") else offset))) + transformation = translation * rotations[0] * rotations[1] * rotations[2] + encoded["matrix"] = [[float(transformation.Value(i, j)) for j in range(1, 5)] for i in range(1, 4)] + [[0., 0., 0., 1.]] + if name in {"locate", "located"}: + # Absolute location requires replacing the current shape location. + import build123d as b + loc = params["loc"] + encoded["matrix"] = geometry.location_matrix(loc * receiver.location.inverse()) + elif name in {"move", "moved"}: + loc = params["loc"] + encoded["matrix"] = geometry.location_matrix(getattr(loc, "location", loc)) + self.depth += 1 + try: + result = fn(*bound.args, **bound.kwargs) + finally: + self.depth -= 1 + if result is not None and (self.is_solid(result) if not isinstance(result, (list, tuple)) else any(self.is_solid(v) for v in result if v is not None)): + if op == "split" and isinstance(result, tuple) and len(result) == 2: + for value, keep in zip(result, ["TOP", "BOTTOM"]): + if value is not None: + self.emit(op, inputs, {**encoded, "keep": keep}, value, f"{owner.__name__}.{name}", **extra) + else: + self.emit(op, inputs, encoded, result, f"{owner.__name__}.{name}", **extra) + return result + + if isinstance(descriptor, classmethod): + # `fn` is already bound to the class; discard the newly supplied cls. + @functools.wraps(fn) + def class_wrapped(cls, *args, **kwargs): + return wrapped(*args, **kwargs) + self.patch(owner, name, classmethod(class_wrapped)) + elif isinstance(descriptor, staticmethod): + self.patch(owner, name, staticmethod(wrapped)) + else: + self.patch(owner, name, wrapped) + + def install(self): + import build123d as b + # Compound algebra and Builder insertion can call the kernel boolean + # boundary directly, without Shape.fuse/cut/intersect. + original_boolean = b.Shape._bool_op + + @functools.wraps(original_boolean) + def boolean(shape, args, tools, operation): + if self.depth: + return original_boolean(shape, args, tools, operation) + args, tools = list(args), list(tools) + from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut, BRepAlgoAPI_Common + op = next((name for cls, name in ((BRepAlgoAPI_Fuse, "union"), + (BRepAlgoAPI_Cut, "cut"), (BRepAlgoAPI_Common, "intersect")) + if isinstance(operation, cls)), None) + if op is None or not args or any(not self.is_solid(s) for s in [*args, *tools]): + return original_boolean(shape, args, tools, operation) + # Each side is a set of bodies, not a chain of intersections. + inputs = self.reference(b.Compound(args)) + if tools: + inputs.extend(self.reference(b.Compound(tools))) + self.depth += 1 + try: + result = original_boolean(shape, args, tools, operation) + finally: + self.depth -= 1 + if result is not None and self.is_solid(result): + self.emit(op, inputs, {}, result, "Shape._bool_op") + return result + + self.patch(b.Shape, "_bool_op", boolean) + original_solid_init = b.Solid.__init__ + + @functools.wraps(original_solid_init) + def solid_init(solid, obj=None, *args, **kwargs): + if self.depth or not isinstance(obj, b.Shell): + return original_solid_init(solid, obj, *args, **kwargs) + extra = {} + try: + extra["profiles"] = [geometry.profile(face) for face in obj.faces()] + except Exception as exc: + extra["geometry_error"] = str(exc) + self.unsupported(f"Cannot transcribe Solid(Shell) inputs: {exc}", source="Solid.__init__") + self.depth += 1 + try: + original_solid_init(solid, obj, *args, **kwargs) + finally: + self.depth -= 1 + if self.is_solid(solid): + self.emit("sew_solid", [], {}, solid, "Solid.__init__", **extra) + + self.patch(b.Solid, "__init__", solid_init) + factories = {"make_box": "box", "make_cylinder": "cylinder", "make_cone": "cone", + "make_sphere": "sphere", "make_torus": "torus", "make_wedge": "wedge", + "extrude": "extrude", "extrude_taper": "extrude_taper", "extrude_until": "extrude_until", + "extrude_linear_with_rotation": "extrude_twist", "revolve": "revolve", + "make_loft": "loft", "sweep": "sweep", "sweep_multi": "sweep_multi", "thicken": "thicken"} + for name, op in factories.items(): + if hasattr(b.Solid, name): + self.wrap(b.Solid, name, op, factory=True) + for name, op in {"fuse": "union", "cut": "cut", "intersect": "intersect", "split": "split"}.items(): + self.wrap(b.Shape, name, op) + for name, op in {"fillet": "fillet", "chamfer": "chamfer", "offset_3d": "offset", "hollow": "shell", "draft": "draft"}.items(): + owner = next((c for c in b.Solid.__mro__ if name in c.__dict__), None) + if owner is not None: + self.wrap(owner, name, op) + for name in ("moved", "move", "located", "locate", "translate", "rotate", "mirror", "scale", "transformed", "transform_shape", "transform_geometry"): + self.wrap(b.Shape, name, "transform") + original_clean = b.Shape.clean + original_copy = b.Shape.__deepcopy__ + + @functools.wraps(original_copy) + def deepcopy(shape, memo): + refs = self.reference(shape) if not self.depth and self.is_solid(shape) else [] + self.depth += 1 + try: + result = original_copy(shape, memo) + finally: + self.depth -= 1 + if refs: + self.register(result, refs[0]) + return result + self.patch(b.Shape, "__deepcopy__", deepcopy) + original_location = inspect.getattr_static(b.Shape, "location") + + def set_location(shape, loc): + if self.depth or not self.is_solid(shape): + original_location.fset(shape, loc) + return + inputs = self.reference(shape) + matrix = geometry.location_matrix(loc * shape.location.inverse()) + self.depth += 1 + try: + original_location.fset(shape, loc) + finally: + self.depth -= 1 + self.emit("transform", inputs, {"method": "locate", "matrix": matrix}, shape, "Shape.location") + self.patch(b.Shape, "location", property(original_location.fget, set_location, original_location.fdel, original_location.__doc__)) + + @functools.wraps(original_clean) + def clean(shape): + refs = self.reference(shape) if not self.depth and self.is_solid(shape) else [] + self.depth += 1 + try: + result = original_clean(shape) + finally: + self.depth -= 1 + if refs: + self.register(result, refs[0]) + return result + self.patch(b.Shape, "clean", clean) + + def restore(self): + for owner, name, original, own_attribute in reversed(self.patches): + if own_attribute: + setattr(owner, name, original) + else: + delattr(owner, name) + self.patches.clear() + + +def _find_result(namespace): + from build123d import BuildPart, Shape + for name in ("result", "part", "model", "solid", "body"): + if name not in namespace: + continue + value = namespace[name] + if isinstance(value, BuildPart): + return value.part + # An explicit output is authoritative, including an empty or invalid one. + return value + for value in reversed(list(namespace.values())): + if isinstance(value, BuildPart) and value.part is not None: + return value.part + if isinstance(value, Shape) and value.solids(): + return value + return None + + +def trace_source(source: str, filename: str = "") -> dict[str, Any]: + """Execute a trusted program and return a portable feature graph.""" + with _LOCK: + recorder = Recorder() + namespace = {"__name__": "__build123d_trace__", "__file__": filename} + old_path = list(sys.path) + sys.path.insert(0, str(Path(filename).resolve().parent)) + try: + recorder.install() + try: + exec(compile(source, filename, "exec"), namespace) + except Exception: + recorder.trace["b3d_status"] = "error" + recorder.trace["error"] = traceback.format_exc() + result = _find_result(namespace) + values = result if isinstance(result, (list, tuple)) else [result] + from build123d import Shape + if (not values or any(v is not None and not isinstance(v, Shape) for v in values) + or not any(v is not None and recorder.is_solid(v) for v in values)): + recorder.trace["b3d_status"] = "error" + recorder.trace["error"] += "\nNo solid result found in model namespace" + else: + recorder.trace["result_refs"] = recorder.reference(result) + summary = geometry.metrics(result) + recorder.trace.update(b3d_volume=summary["volume"], b3d_bbox=summary["bbox"], solid_count=summary["solid_count"]) + recorder.trace["b3d_default_volume"] = summary.get("default_volume", summary["volume"]) + recorder.trace["b3d_volume_gk"] = summary["volume"] + recorder.trace["b3d_valid"] = all(v.is_valid for v in values if v is not None) + recorder.trace["b3d_area"] = summary["area"] + finally: + recorder.restore() + sys.path[:] = old_path + recorder.trace["source_file"] = filename + recorder.trace["source_sha256"] = hashlib.sha256(source.encode("utf-8")).hexdigest() + import build123d + recorder.trace["build123d_version"] = getattr(build123d, "__version__", "unknown") + return recorder.trace + + +def trace_file(path: str | Path) -> dict[str, Any]: + path = Path(path).resolve() + return trace_source(path.read_text(encoding="utf-8"), str(path)) diff --git a/tools/research/build123dftc/b3ftc/validation.py b/tools/research/build123dftc/b3ftc/validation.py new file mode 100644 index 00000000..32720242 --- /dev/null +++ b/tools/research/build123dftc/b3ftc/validation.py @@ -0,0 +1,314 @@ +"""Stage 3 acceptance, independent of the source CAD interpreter. + +Each generated program runs in a separate process. Acceptance requires all +source metrics, complete translation, real nonempty valid solids, and matching +volume, absolute bounds and solid count. Optional STEP comparison checks both +directions of the geometric difference. +""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +import time +from typing import Any, Mapping + +VOLUME_RTOL = 1e-3 +VOLUME_ATOL = 1e-6 +BBOX_RTOL = 1e-3 +BBOX_ATOL = 1e-4 +DIFFERENCE_RTOL = 1e-3 + + +def _finite_number(value: Any) -> bool: + return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) + + +def _bbox(value: Any) -> bool: + return (isinstance(value, (list, tuple)) and len(value) == 6 + and all(_finite_number(v) for v in value) + and all(value[i] <= value[i + 3] for i in range(3))) + + +def _positive_count(value: Any) -> bool: + return isinstance(value, int) and not isinstance(value, bool) and value > 0 + + +def _translation_notes(payload: Mapping[str, Any]) -> bool: + return any(bool(value) for name, value in payload.items() + if name in {"unsupported", "untranslated", "unhandled"} or name.startswith("unknown")) + + +def compare_metrics(trace: Mapping[str, Any], actual: Mapping[str, Any], + meta: Mapping[str, Any] | None, *, + volume_rtol: float = VOLUME_RTOL, volume_atol: float = VOLUME_ATOL, + bbox_rtol: float = BBOX_RTOL, bbox_atol: float = BBOX_ATOL, + difference_rtol: float = DIFFERENCE_RTOL, + require_reference: bool = False) -> dict[str, Any]: + """Compare untrusted JSON evidence; missing metrics always reject.""" + reasons: list[str] = [] + if trace.get("b3d_status") != "ok": + reasons.append("source_failed") + expected_volume, expected_bbox = trace.get("b3d_volume"), trace.get("b3d_bbox") + expected_count = trace.get("solid_count") + valid_source = (_finite_number(expected_volume) and expected_volume > 0 + and _bbox(expected_bbox) and _positive_count(expected_count) + and bool(trace.get("result_refs"))) + if not valid_source: + reasons.append("missing_source_metrics") + if trace.get("b3d_valid") is False or trace.get("b3d_is_valid") is False: + reasons.append("invalid_source") + if _translation_notes(trace): + reasons.append("unsupported_source") + if not isinstance(meta, Mapping) or meta.get("status") != "ok": + reasons.append("incomplete_translation") + if isinstance(meta, Mapping) and _translation_notes(meta): + reasons.append("untranslated_operations") + if actual.get("status") != "ok": + reasons.append("build_failed") + if not _positive_count(actual.get("body_count")): + reasons.append("empty_bodies") + if actual.get("is_valid") is not True: + reasons.append("invalid_topology") + volume, bounds, count = actual.get("volume"), actual.get("bbox"), actual.get("solid_count") + valid_actual = (_finite_number(volume) and volume > 0 and _bbox(bounds) + and _positive_count(count)) + if not valid_actual: + reasons.append("missing_actual_metrics") + volume_error = bbox_error = bbox_absolute_error = area_error = None + if valid_source and valid_actual: + volume_error = abs(volume - expected_volume) / abs(expected_volume) + if abs(volume - expected_volume) > volume_atol + volume_rtol * abs(expected_volume): + reasons.append("volume_mismatch") + # Six world coordinates, scaled by axis span (never by world position). + # A rigid translation therefore fails even when every extent matches. + spans = [max(expected_bbox[i + 3] - expected_bbox[i], 1.0) for i in range(3)] + deviations = [abs(a - b) for a, b in zip(bounds, expected_bbox)] + bbox_absolute_error = max(deviations) + bbox_error = max(delta / spans[i % 3] for i, delta in enumerate(deviations)) + if any(delta > bbox_atol + bbox_rtol * spans[i % 3] + for i, delta in enumerate(deviations)): + reasons.append("bbox_mismatch") + if count != expected_count: + reasons.append("solid_count_mismatch") + expected_area, area = trace.get("b3d_area"), actual.get("area") + if expected_area is not None: + if not (_finite_number(expected_area) and expected_area > 0 and _finite_number(area) and area > 0): + reasons.append("missing_area_metrics") + else: + area_error = abs(area - expected_area) / expected_area + if abs(area - expected_area) > volume_atol + volume_rtol * abs(expected_area): + reasons.append("area_mismatch") + difference = actual.get("symmetric_difference_volume") + difference_error = None + if require_reference: + reference_volume = actual.get("reference_volume") + if (actual.get("reference_status") != "ok" or not _finite_number(difference) + or difference < 0 or not _finite_number(reference_volume) or reference_volume <= 0): + reasons.append("reference_error") + else: + difference_error = difference / reference_volume + if difference > volume_atol + difference_rtol * reference_volume: + reasons.append("symmetric_difference_mismatch") + if "roundtrip_actual_volume" in actual: + exported = {"status": "ok", "volume": actual.get("roundtrip_actual_volume"), + "bbox": actual.get("roundtrip_actual_bbox"), + "solid_count": actual.get("roundtrip_actual_solid_count"), + "body_count": actual.get("body_count"), + "is_valid": actual.get("roundtrip_actual_valid")} + original = {"b3d_status": "ok", "b3d_volume": volume, "b3d_bbox": bounds, + "solid_count": count, "result_refs": ["actual"]} + check = compare_metrics(original, exported, {"status": "ok"}, + volume_rtol=volume_rtol, volume_atol=volume_atol, + bbox_rtol=bbox_rtol, bbox_atol=bbox_atol) + if check["quality"] != "accepted": + reasons.append("reference_roundtrip_changed_geometry") + return { + "quality": "rejected" if reasons else "accepted", "reasons": reasons, + "b3d_volume": expected_volume if _finite_number(expected_volume) else None, + "ftc_volume": volume if _finite_number(volume) else None, + "b3d_bbox": expected_bbox if _bbox(expected_bbox) else None, + "ftc_bbox": bounds if _bbox(bounds) else None, + "b3d_solid_count": expected_count, "ftc_solid_count": count, + "body_count": actual.get("body_count"), "is_valid": actual.get("is_valid"), + "volume_error": volume_error, "bbox_error": bbox_error, + "b3d_area": expected_area if _finite_number(expected_area) else None, + "ftc_area": area if _finite_number(area) else None, + "area_error": area_error, + "bbox_absolute_error": bbox_absolute_error, + "symmetric_difference_volume": difference if _finite_number(difference) else None, + "symmetric_difference_error": difference_error, + } + + +def _load_json(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"JSON root must be an object: {path}") + return payload + + +def _interpreter(executable: str | None) -> str: + executable = executable or sys.executable + # Keep a venv symlink instead of resolving it to the base Python binary. + return os.path.abspath(executable) if os.sep in executable else executable + + +def _process(command: list[str], staging: Path, env: Mapping[str, str], + output: Path, timeout: float) -> dict[str, Any]: + if timeout <= 0: + return {"status": "timeout", "error": "case timeout exhausted before reference comparison"} + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, cwd=staging, env=env, + start_new_session=os.name == "posix") + try: + stdout, stderr = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + if os.name == "posix": + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + else: + process.kill() + stdout, stderr = process.communicate() + return {"status": "timeout", "error": f"build exceeded {timeout:g} seconds", + "stdout": stdout[-4000:], "stderr": stderr[-4000:]} + if output.exists(): + try: + result = _load_json(output) + except (ValueError, OSError) as exc: + result = {"status": "worker_error", "error": str(exc)} + else: + result = {"status": "worker_error", "error": f"worker exited {process.returncode} without metrics"} + if process.returncode and result.get("status") == "ok": + result.update(status="worker_error", error=f"worker exited {process.returncode}") + if stdout.strip(): + result["stdout"] = stdout[-4000:] + if stderr.strip(): + result["stderr"] = stderr[-4000:] + return result + + +def _worker(ftc_path: Path, workdir: Path | None, timeout: float, + reference_step: Path | None, python_executable: str | None, + reference_python: str | None = None, + export_options: Mapping[str, Any] | None = None) -> dict[str, Any]: + if timeout <= 0 or not math.isfinite(timeout): + raise ValueError("timeout must be a positive finite number") + if workdir is not None: + workdir.mkdir(parents=True, exist_ok=True) + tool_dir = Path(__file__).resolve().parent.parent + repo_root = tool_dir.parents[2] + with tempfile.TemporaryDirectory(prefix="b3ftc-validate-", dir=workdir) as tmp: + staging = Path(tmp).resolve() + output = staging / "result.json" + command = [_interpreter(python_executable), str(tool_dir / "b3ftc/_validate_worker.py"), + "--ftc", str(ftc_path.resolve()), "--output", str(output), + "--staging-dir", str(staging)] + actual_step = staging / "actual.step" + if reference_step is not None and reference_python is not None: + command.extend(["--export-step", str(actual_step)]) + if export_options: + command.extend(["--export-options", json.dumps(export_options)]) + elif reference_step is not None: + command.extend(["--reference-step", str(reference_step.resolve())]) + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join([str(repo_root / "src"), str(tool_dir), + env.get("PYTHONPATH", "")]) + started = time.monotonic() + result = _process(command, staging, env, output, timeout) + if reference_python is not None and reference_step is not None and result.get("status") == "ok": + ref_output = staging / "reference.json" + command = [_interpreter(reference_python), str(tool_dir / "b3ftc/_reference_worker.py"), + "--actual-step", str(actual_step), "--reference-step", str(reference_step.resolve()), + "--output", str(ref_output)] + ref = _process(command, staging, env, ref_output, timeout - (time.monotonic() - started)) + result.update({key: value for key, value in ref.items() + if key.startswith(("reference_", "roundtrip_actual_", "symmetric_difference_"))}) + if ref.get("status") != "ok": + result.update(reference_status="error", reference_error=ref.get("reference_error", ref.get("error", ""))) + return result + + +def validate_row(ftc_path: Path, trace_path: Path, workdir: Path | None = None, *, + timeout: float = 120, reference_step: Path | None = None, + python_executable: str | None = None, + reference_python: str | None = None) -> dict[str, Any]: + """Validate one FTC. Errors are returned as rejected rows for batch use.""" + ftc_path, trace_path = Path(ftc_path), Path(trace_path) + stem = ftc_path.name.removesuffix(".ftc.py") + row: dict[str, Any] = {"stem": stem, "quality": "rejected", "metrics_quality": "rejected", "metrics_reasons": ["not_evaluated"]} + try: + if not ftc_path.is_file(): + return {**row, "build": "missing_ftc", "reasons": ["missing_ftc"]} + if not trace_path.is_file(): + return {**row, "build": "missing_trace", "reasons": ["missing_trace"]} + trace = _load_json(trace_path) + meta_path = ftc_path.with_name(stem + ".meta.json") + meta = _load_json(meta_path) if meta_path.is_file() else None + # Reject absent/failed source or incomplete conversion without executing it. + preliminary = compare_metrics(trace, {}, meta) + source_reasons = [reason for reason in preliminary["reasons"] if reason in { + "source_failed", "missing_source_metrics", "invalid_source", "unsupported_source", + "incomplete_translation", "untranslated_operations"}] + if source_reasons: + return {**row, **preliminary, "build": "not_run", "reasons": source_reasons, + "error": trace.get("error", "")} + metadata = trace.get("source_metadata") or {} + export_options = metadata.get("export_options", trace.get("export_options")) + metrics = _worker(ftc_path, Path(workdir) if workdir is not None else None, + timeout, Path(reference_step) if reference_step is not None else None, + python_executable, reference_python, export_options) + result = compare_metrics(trace, metrics, meta, require_reference=reference_step is not None) + basic = compare_metrics(trace, metrics, meta, require_reference=False) + return {**row, **result, "build": metrics.get("status", "worker_error"), + "metrics_quality": basic["quality"], "metrics_reasons": basic["reasons"], + "error": metrics.get("error", ""), + "reference_status": metrics.get("reference_status"), + "reference_kernel": metrics.get("reference_kernel"), + "reference_export_options": metrics.get("reference_export_options"), + "reference_boolean_fuzzy_tolerance": metrics.get("reference_boolean_fuzzy_tolerance"), + "reference_python": reference_python, + "build_kernel": metrics.get("build_kernel"), + "volume_method": metrics.get("volume_method"), + "area_method": metrics.get("area_method"), + "reference_error": metrics.get("reference_error", ""), + "stdout": metrics.get("stdout", ""), "stderr": metrics.get("stderr", "")} + except Exception as exc: + return {**row, "build": "validator_error", "reasons": ["validator_error"], + "error": f"{type(exc).__name__}: {exc}"} + + +def validate_directory(ftc_dir: Path, traces_dir: Path, *, timeout: float = 120, + reference_step_dir: Path | None = None, limit: int | None = None, + python_executable: str | None = None, + reference_python: str | None = None) -> dict[str, Any]: + """Check the union of FTC and trace stems, never omit a failed conversion.""" + ftc_dir, traces_dir = Path(ftc_dir), Path(traces_dir) + stems = sorted({path.name.removesuffix(".ftc.py") for path in ftc_dir.glob("*.ftc.py")} + | {path.name.removesuffix(".trace.json") for path in traces_dir.glob("*.trace.json")}) + if limit is not None: + if limit < 0: + raise ValueError("limit must be nonnegative") + stems = stems[:limit] + rows = [validate_row(ftc_dir / (stem + ".ftc.py"), traces_dir / (stem + ".trace.json"), + timeout=timeout, python_executable=python_executable, + reference_python=reference_python, + reference_step=(Path(reference_step_dir) / (stem + ".step")) + if reference_step_dir is not None else None) for stem in stems] + return {"schema_version": "1.0", "rows": rows, + "counts": {quality: sum(row["quality"] == quality for row in rows) + for quality in ("accepted", "partial", "rejected")}, + "tolerances": {"volume_relative": VOLUME_RTOL, "volume_absolute": VOLUME_ATOL, + "bbox_relative_to_axis_span": BBOX_RTOL, "bbox_absolute": BBOX_ATOL, + "symmetric_difference_relative": DIFFERENCE_RTOL}, + "volume_method": "Gauss-Kronrod, epsilon=1e-9, closed shells, BSpline spans", + "area_method": "Independent geometry copy, C1 continuity spans, adaptive epsilon=1e-10", + "reference_step_dir": str(reference_step_dir) if reference_step_dir else None, + "reference_python": reference_python} diff --git a/tools/research/build123dftc/build123d_profile.py b/tools/research/build123dftc/build123d_profile.py new file mode 100644 index 00000000..52467f1e --- /dev/null +++ b/tools/research/build123dftc/build123d_profile.py @@ -0,0 +1,44 @@ +"""Stage 4: count actual traced operations and report conversion gaps.""" +import argparse +from collections import Counter +import json +from pathlib import Path + + +def profile_traces(paths): + operations, sources, unsupported = Counter(), Counter(), Counter() + rows = [] + for path in paths: + trace = json.loads(Path(path).read_text(encoding="utf-8")) + for step in trace.get("steps", []): + operations[step["op"]] += 1 + sources[step.get("source", step["op"])] += 1 + for note in trace.get("unsupported", []): + unsupported[note.get("reason", str(note)) if isinstance(note, dict) else str(note)] += 1 + rows.append({"stem": trace.get("stem", Path(path).name.removesuffix(".trace.json")), + "status": trace.get("b3d_status"), "steps": len(trace.get("steps", [])), + "unsupported": len(trace.get("unsupported", []))}) + return {"model_count": len(rows), "operations": dict(operations.most_common()), + "source_methods": dict(sources.most_common()), "unsupported": dict(unsupported.most_common()), "rows": rows} + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--traces-dir", required=True) + parser.add_argument("--report") + args = parser.parse_args(argv) + paths = sorted(Path(args.traces_dir).glob("*.trace.json")) + if not paths: + parser.error("no traces found") + report = profile_traces(paths) + text = json.dumps(report, ensure_ascii=False, indent=2) + "\n" + if args.report: + path = Path(args.report) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + print(text) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/research/build123dftc/build123d_to_ftc.py b/tools/research/build123dftc/build123d_to_ftc.py new file mode 100644 index 00000000..0ae979a5 --- /dev/null +++ b/tools/research/build123dftc/build123d_to_ftc.py @@ -0,0 +1,66 @@ +"""Stage 2: translate portable build123d traces into native FTC Python.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + + +def safe_stem(stem): + if not isinstance(stem, str) or stem in ("", ".", "..") or any(c in stem for c in ("/", "\\", "\x00", "\n", "\r")): + raise ValueError("stem must be a single nonempty filename") + return stem + + +def translate_trace_file(trace_path: Path, out_dir: Path, *, constraints_mode=False): + from b3ftc.replay import replay_trace_to_ftc + from b3ftc.schema import SCHEMA_VERSION + payload = json.loads(trace_path.read_text(encoding="utf-8")) + stem = safe_stem(payload.get("stem", trace_path.name.removesuffix(".trace.json"))) + if payload.get("schema_version") != SCHEMA_VERSION: + raise ValueError(f"unsupported trace schema: {payload.get('schema_version')!r}") + options = {"constraints_mode": True} if constraints_mode else {} + source, meta = replay_trace_to_ftc(payload, stem=stem, **options) + meta = {"stem": stem, "source_file": payload.get("source_file"), **meta} + snapshots = meta.pop("sketches", []) + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / f"{stem}.ftc.py").write_text(source, encoding="utf-8") + (out_dir / f"{stem}.meta.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8") + (out_dir / f"{stem}.sketches.json").write_text(json.dumps({"stem": stem, "constraints_mode": constraints_mode, "sketches": snapshots}, + ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8") + return stem, source, meta + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + inputs = parser.add_mutually_exclusive_group(required=True) + inputs.add_argument("--trace") + inputs.add_argument("--traces-dir") + parser.add_argument("--out", default="tools/research/build123dftc/out/ftc") + parser.add_argument("--constraints", choices=("on", "off"), default="off", help="recover inferred constraints with the existing CQ engine") + args = parser.parse_args(argv) + paths = [Path(args.trace)] if args.trace else sorted(Path(args.traces_dir).glob("*.trace.json")) + if not paths: + parser.error("no traces found") + failed = 0 + for path in paths: + try: + stem, source, meta = translate_trace_file(path, Path(args.out), constraints_mode=args.constraints == "on") + failed += meta["status"] != "ok" + print(f"{stem}: {meta['status']} ({meta.get('feature_count', 0)} features, {len(meta.get('unsupported', []))} unsupported)", flush=True) + except Exception as error: + failed += 1 + stem = safe_stem(path.name.removesuffix(".trace.json")) + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + (out / f"{stem}.meta.json").write_text(json.dumps({"stem": stem, "status": "error", "unsupported": [f"{type(error).__name__}: {error}"]}, indent=2) + "\n") + print(f"{stem}: translation_error: {type(error).__name__}: {error}", flush=True) + print(f"translated {len(paths)} traces, {failed} incomplete -> {args.out}") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/research/build123dftc/build123d_trace.py b/tools/research/build123dftc/build123d_trace.py new file mode 100644 index 00000000..23c1f078 --- /dev/null +++ b/tools/research/build123dftc/build123d_trace.py @@ -0,0 +1,121 @@ +"""Stage 1: execute trusted build123d models in isolated worker processes.""" +from __future__ import annotations + +import argparse +import json +import math +import os +from pathlib import Path +import signal +import subprocess +import sys + +TOOLS = Path(__file__).resolve().parent +sys.path.insert(0, str(TOOLS)) +from b3ftc.schema import empty_trace + + +def discover_inputs(*, input_file=None, inputs_dir=None, manifest=None): + if input_file: + paths = [Path(input_file).resolve()] + elif manifest: + path = Path(manifest).resolve() + payload = json.loads(path.read_text(encoding="utf-8")) + paths = [(path.parent / item["file"]).resolve() for item in payload["examples"]] + elif inputs_dir: + paths = sorted(Path(inputs_dir).resolve().glob("*.py")) + else: + raise ValueError("one input source is required") + if not paths: + raise ValueError("no Python models found") + if any(not p.is_file() for p in paths): + raise FileNotFoundError("some declared model files do not exist") + stems = [p.stem for p in paths] + if len(set(stems)) != len(stems): + raise ValueError("model stems must be unique within one batch") + return paths + + +def trace_model(path: Path, out_dir: Path, *, stem=None, timeout=120, python_executable=None, source_metadata=None): + from build123d_to_ftc import safe_stem + if not math.isfinite(timeout) or timeout <= 0: + raise ValueError("timeout must be finite and positive") + path = path.resolve() + stem = safe_stem(stem or path.stem) + out_dir.mkdir(parents=True, exist_ok=True) + output = out_dir / f"{stem}.trace.json" + temporary = out_dir / f"{stem}.trace.tmp.json" + log_path = out_dir / f"{stem}.trace.log" + temporary.unlink(missing_ok=True) + command = [python_executable or sys.executable, str(TOOLS / "b3ftc/_trace_worker.py"), + str(path), str(temporary.resolve())] + with log_path.open("w", encoding="utf-8") as log: + process = subprocess.Popen(command, stdout=log, stderr=log, cwd=path.parent, + start_new_session=True) + try: + code = process.wait(timeout=timeout) + failure = f"trace worker exited with code {code}" if code else "" + except subprocess.TimeoutExpired: + if os.name == "posix": + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + else: + process.kill() + process.wait() + failure = f"source execution timed out after {timeout}s" + if failure or not temporary.exists(): + payload = empty_trace() + payload.update(b3d_status="error", source_file=str(path), error=failure or "worker produced no trace") + with log_path.open("rb") as log: + log.seek(max(0, log_path.stat().st_size - 4096)) + payload["error"] += "\n" + log.read().decode("utf-8", errors="replace") + else: + payload = json.loads(temporary.read_text(encoding="utf-8")) + payload["stem"] = stem + if source_metadata: + payload["source_metadata"] = source_metadata + temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8") + temporary.replace(output) + return payload + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + inputs = parser.add_mutually_exclusive_group(required=True) + inputs.add_argument("--input", dest="input_file", help="one trusted Python source") + inputs.add_argument("--inputs-dir", "--dataset", dest="inputs_dir", help="directory of Python models") + inputs.add_argument("--manifest", help="JSON examples manifest, paths relative to manifest") + parser.add_argument("--out", default="tools/research/build123dftc/out/traces") + parser.add_argument("--stem", help="output stem for one source") + parser.add_argument("--timeout", type=float, default=120) + parser.add_argument("--limit", type=int) + parser.add_argument("--python", dest="python_executable", help="interpreter with build123d installed") + args = parser.parse_args(argv) + if not math.isfinite(args.timeout) or args.timeout <= 0 or (args.limit is not None and args.limit <= 0): + parser.error("timeout and limit must be positive") + if args.stem and not args.input_file: + parser.error("--stem requires --input") + paths = discover_inputs(input_file=args.input_file, inputs_dir=args.inputs_dir, manifest=args.manifest) + metadata = {} + if args.manifest: + manifest_path = Path(args.manifest).resolve() + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + metadata = {str((manifest_path.parent / row["file"]).resolve()): + {key: row[key] for key in ("source_url", "source_sha256", "export_options") if key in row} + for row in manifest["examples"]} + paths = paths[:args.limit] if args.limit else paths + failed = 0 + for path in paths: + row = trace_model(path, Path(args.out), stem=args.stem, timeout=args.timeout, + python_executable=args.python_executable, source_metadata=metadata.get(str(path))) + failed += row["b3d_status"] != "ok" + print(f"{row['stem']}: {row['b3d_status']} ({len(row['steps'])} operations, " + f"{len(row['unsupported'])} unsupported)", flush=True) + print(f"captured {len(paths)} models, {failed} failed -> {args.out}") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/research/build123dftc/build123d_validate.py b/tools/research/build123dftc/build123d_validate.py new file mode 100644 index 00000000..c0632463 --- /dev/null +++ b/tools/research/build123dftc/build123d_validate.py @@ -0,0 +1,45 @@ +"""Stage 3 — validate generated FTC solids against source traces and optional STEP.""" +from __future__ import annotations + +import argparse +import json +import math +from pathlib import Path + +from b3ftc.validation import validate_directory + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--ftc-dir", type=Path, required=True) + parser.add_argument("--traces-dir", type=Path, required=True) + parser.add_argument("--report", type=Path) + parser.add_argument("--timeout", type=float, default=120) + parser.add_argument("--reference-step-dir", type=Path) + parser.add_argument("--reference-python", help="Optional independent OCCT interpreter for STEP differences") + parser.add_argument("--limit", type=int) + parser.add_argument("--python", dest="python_executable", help="SimpleCADAPI worker interpreter") + args = parser.parse_args(argv) + if not args.ftc_dir.is_dir() or not args.traces_dir.is_dir(): + parser.error("--ftc-dir and --traces-dir must be existing directories") + if not math.isfinite(args.timeout) or args.timeout <= 0: + parser.error("--timeout must be positive and finite") + if args.limit is not None and args.limit < 0: + parser.error("--limit must be nonnegative") + if args.reference_python and not args.reference_step_dir: + parser.error("--reference-python requires --reference-step-dir") + report = validate_directory(args.ftc_dir, args.traces_dir, timeout=args.timeout, + reference_step_dir=args.reference_step_dir, limit=args.limit, + python_executable=args.python_executable, + reference_python=args.reference_python) + for row in report["rows"]: + print(f"{row['stem']}: {row['quality']} (build={row['build']}, reasons={','.join(row['reasons']) or 'none'})") + print(json.dumps(report["counts"])) + if args.report is not None: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") + return 0 if report["rows"] and not (report["counts"]["rejected"] or report["counts"]["partial"]) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/research/build123dftc/research/inspect_api.py b/tools/research/build123dftc/research/inspect_api.py new file mode 100644 index 00000000..7fc65ac4 --- /dev/null +++ b/tools/research/build123dftc/research/inspect_api.py @@ -0,0 +1,328 @@ +"""Generate a version-specific public build123d API and corpus inventory. + +This is research evidence, not a declaration that conversion is implemented. +Run with the same interpreter as the source model corpus. +""" + +from __future__ import annotations + +import argparse +import ast +from collections import Counter +from enum import Enum +import inspect +import hashlib +import json +from pathlib import Path +from typing import Any + + +DOCUMENTATION_SOURCES = [ + "https://build123d.readthedocs.io/en/v0.13.0/objects.html", + "https://build123d.readthedocs.io/en/v0.13.0/operations.html", + "https://build123d.readthedocs.io/en/v0.13.0/key_concepts_builder.html", + "https://build123d.readthedocs.io/en/v0.13.0/key_concepts_algebra.html", + "https://build123d.readthedocs.io/en/v0.13.0/direct_api_reference.html", +] + +OPERATORS = { + "__add__", "__radd__", "__sub__", "__and__", "__mul__", "__rmul__", + "__matmul__", "__xor__", "__mod__", "__neg__", "__invert__", +} +TRANSFORMS = { + "move", "moved", "locate", "located", "translate", "rotate", "scale", + "mirror", "transform_shape", "transform_geometry", "transformed", +} +FEATURES = { + "extrude", "extrude_taper", "extrude_until", "extrude_linear_with_rotation", + "revolve", "sweep", "sweep_multi", "loft", "fillet", "chamfer", "draft", + "hollow", "offset", "offset_2d", "offset_3d", "split", "split_by_perimeter", + "thicken", "dprism", "trim", "trim_infinite", "trim_to_length", + "trim_to_other", "make_holes", "without_holes", "close", "stitch", "combine", + "clean", "fix", "fix_degenerate_edges", "to_splines", "reversed", "wrap", + "wrap_faces", "cut", "fuse", "intersect", "fillet_2d", "chamfer_2d", + "project", "project_to_shape", "project_faces", "sew_faces", +} + + +def category(name: str, obj: Any) -> str: + module = getattr(obj, "__module__", "") + if inspect.isclass(obj) and issubclass(obj, Enum): + return "enum" + if module.startswith("build123d.operations_"): + return "operation" + if module.startswith("build123d.objects_"): + return "object_" + module.removeprefix("build123d.objects_") + if name.startswith("Build"): + return "builder" + if name.endswith("Locations") or name == "Locations": + return "locations" + if module.startswith("build123d.topology"): + return "direct_topology" + if module == "build123d.geometry": + return "geometry" + if "export" in module or name.startswith("Export"): + return "export" + if "import" in module or name.startswith("import_"): + return "import" + if module == "build123d.joints": + return "assembly_joint" + if module == "build123d.drafting": + return "drawing_object" + if not callable(obj): + return "constant_or_type_alias" + return "utility" + + +def source_info(obj: Any, package_dir: Path) -> dict[str, Any]: + result: dict[str, Any] = {} + try: + source = Path(inspect.getsourcefile(obj) or "") + result["source_file"] = str(source.relative_to(package_dir.parent)) + result["source_line"] = inspect.getsourcelines(obj)[1] + except (TypeError, ValueError, OSError): + pass + try: + result["signature"] = str(inspect.signature(obj)) + except (TypeError, ValueError): + pass + return result + + +def member_category(name: str, raw: Any, owner: type) -> str: + if isinstance(raw, property): + return "property" + if name in OPERATORS: + return "algebra_operator" + if name in TRANSFORMS: + return "transform" + if name.startswith("make_") or name == "from_bounding_box": + return "factory" + if name in FEATURES: + return "feature" + if name in {"__enter__", "__exit__"}: + return "context_lifecycle" + if issubclass(owner, Enum): + return "enum_member" + return "query_or_utility" + + +class ModelVisitor(ast.NodeVisitor): + """Count modeling calls, excluding entrypoint and constant-false docs branches.""" + + def __init__(self, exports: set[str]): + self.exports = exports + self.constants: dict[str, Any] = {} + self.calls: Counter[str] = Counter() + self.methods: Counter[str] = Counter() + self.operators: Counter[str] = Counter() + self.builders = False + self.aliases = {"build123d"} + + def visit_Import(self, node: ast.Import) -> None: + for alias in node.names: + if alias.name == "build123d": + self.aliases.add(alias.asname or alias.name) + + def visit_Assign(self, node: ast.Assign) -> None: + if isinstance(node.value, ast.Constant): + for target in node.targets: + if isinstance(target, ast.Name): + self.constants[target.id] = node.value.value + self.generic_visit(node) + + def visit_If(self, node: ast.If) -> None: + test = node.test + is_entrypoint = ( + isinstance(test, ast.Compare) + and isinstance(test.left, ast.Name) + and test.left.id == "__name__" + ) + if is_entrypoint: + return + if isinstance(test, ast.Name) and self.constants.get(test.id) is False: + for stmt in node.orelse: + self.visit(stmt) + return + self.generic_visit(node) + + def visit_Call(self, node: ast.Call) -> None: + func = node.func + if isinstance(func, ast.Name) and func.id in self.exports: + self.calls[func.id] += 1 + self.builders |= func.id in {"BuildLine", "BuildSketch", "BuildPart"} + elif isinstance(func, ast.Attribute): + if isinstance(func.value, ast.Name) and func.value.id in self.aliases: + self.calls[func.attr] += 1 + self.builders |= func.attr in {"BuildLine", "BuildSketch", "BuildPart"} + else: + self.methods[func.attr] += 1 + self.generic_visit(node) + + def visit_BinOp(self, node: ast.BinOp) -> None: + self.operators[type(node.op).__name__] += 1 + self.generic_visit(node) + + +def inventory(manifest_path: Path) -> dict[str, Any]: + import build123d as bd + from build123d.build_common import BaseObjectMeta, Builder + from build123d.topology.shape_core import Shape + from build123d.topology.one_d import Mixin1D + from build123d.topology.two_d import Mixin2D + from build123d.topology.three_d import Mixin3D + + package_dir = Path(bd.__file__).parent + manifest = json.loads(manifest_path.read_text()) + top_level: list[dict[str, Any]] = [] + members: dict[str, dict[str, Any]] = {} + classes: dict[str, list[str]] = {} + extra_classes = [Shape, Mixin1D, Mixin2D, Mixin3D, Builder, BaseObjectMeta] + for name in bd.__all__: + obj = getattr(bd, name) + record = { + "name": name, + "kind": "class" if inspect.isclass(obj) else "function" if callable(obj) else "value", + "category": category(name, obj), + "module": getattr(obj, "__module__", type(obj).__module__), + "conversion_status": "unverified", + **source_info(obj, package_dir), + } + if inspect.isclass(obj) and issubclass(obj, Enum): + record["values"] = list(obj.__members__) + top_level.append(record) + if inspect.isclass(obj): + extra_classes.append(obj) + for cls in dict.fromkeys(extra_classes): + refs = [] + for name in sorted(dir(cls)): + if name.startswith("_") and name not in OPERATORS | {"__enter__", "__exit__", "__call__"}: + continue + owner = next((parent for parent in cls.__mro__ if name in vars(parent)), None) + if owner is None or not owner.__module__.startswith("build123d"): + continue + raw = vars(owner)[name] + value = raw.fget if isinstance(raw, property) else raw.__func__ if isinstance(raw, (staticmethod, classmethod)) else raw + if not callable(value) and not isinstance(raw, property): + continue + key = owner.__module__ + "." + owner.__name__ + "." + name + refs.append(key) + if key not in members: + members[key] = { + "name": name, + "owner": owner.__module__ + "." + owner.__name__, + "category": member_category(name, raw, owner), + "descriptor": "property" if isinstance(raw, property) else "classmethod" if isinstance(raw, classmethod) else "staticmethod" if isinstance(raw, staticmethod) else "method", + "conversion_status": "unverified", + **source_info(value, package_dir), + } + classes[cls.__module__ + "." + cls.__name__] = refs + models = [] + all_calls: Counter[str] = Counter() + all_methods: Counter[str] = Counter() + for item in manifest["examples"]: + model_path = manifest_path.parent / item["file"] + visitor = ModelVisitor(set(bd.__all__)) + visitor.visit(ast.parse(model_path.read_text())) + all_calls.update(visitor.calls) + all_methods.update(visitor.methods) + models.append({ + "id": item["id"], "name": item["name"], "file": item["file"], + "source_url": item["source_url"], + "builder_context_present": visitor.builders, + "top_level_call_sites": dict(visitor.calls.most_common()), + "method_call_sites": dict(visitor.methods.most_common()), + "operator_syntax_counts": dict(visitor.operators.most_common()), + }) + return { + "schema_version": 1, + "purpose": "Version-specific API inventory and static source corpus evidence; unverified is not implemented support.", + "build123d_version": bd.__version__, + "corpus_tag": manifest["tag"], + "corpus_commit": manifest["commit"], + "sources": DOCUMENTATION_SOURCES, + "scope": "203 package __all__ exports plus public build123d-declared members reachable through these classes, Shape, topology mixins and Builder; Python/OCP internals are excluded.", + "counts": { + "top_level_exports": len(top_level), "unique_public_member_declarations": len(members), + "class_interfaces": len(classes), "corpus_models": len(models), + "corpus_distinct_top_level_calls": len(all_calls), + "corpus_top_level_call_sites": sum(all_calls.values()), + "top_level_by_category": dict(Counter(item["category"] for item in top_level)), + "members_by_category": dict(Counter(item["category"] for item in members.values())), + }, + "top_level_api": top_level, + "members": members, + "class_members": classes, + "corpus": { + "counting_note": "AST call sites, not runtime execution counts. Constant-false GEN_DOCS branches and __main__ export blocks are omitted; loops/functions are counted syntactically. Operator counts include numeric arithmetic.", + "top_level_calls": dict(all_calls.most_common()), + "method_calls": dict(all_methods.most_common()), + "models": models, + }, + } + + +def validation_evidence(data: dict[str, Any], report_path: Path, traces_dir: Path | None) -> None: + """Link only verified samples, never promote an entire API parameter space.""" + report = json.loads(report_path.read_text()) + rows = {row["stem"]: row for row in report["rows"]} + accepted = {stem for stem, row in rows.items() if row.get("quality") == "accepted"} + usage: dict[str, set[str]] = {} + for model in data["corpus"]["models"]: + stem = Path(model["file"]).stem + for api in model["top_level_call_sites"]: + usage.setdefault(api, set()).add(stem) + observed_members: dict[str, set[str]] = {} + if traces_dir is not None: + for path in traces_dir.glob("*.trace.json"): + stem = path.name.removesuffix(".trace.json") + trace = json.loads(path.read_text()) + for step in trace.get("steps", []): + source = step.get("source") + if source and step.get("op") != "unsupported": + observed_members.setdefault(source, set()).add(stem) + source_report = {"path": str(report_path), + "sha256": hashlib.sha256(report_path.read_bytes()).hexdigest()} + for record in data["top_level_api"]: + exercised = usage.get(record["name"], set()) + verified = sorted(exercised & accepted) + if verified: + record["conversion_status"] = "verified_sampled_usage" + record["validation_evidence"] = { + "accepted_models": verified, + "rejected_or_unverified_models": sorted(exercised - accepted), + "scope": "Model-level evidence for the listed source calls only; other argument variants remain unverified.", + } + for record in data["members"].values(): + source = record["owner"].rsplit(".", 1)[-1] + "." + record["name"] + exercised = observed_members.get(source, set()) + verified = sorted(exercised & accepted) + if verified: + record["conversion_status"] = "verified_sampled_usage" + record["validation_evidence"] = {"accepted_models": verified, + "scope": "Captured member call in accepted models; other argument variants remain unverified."} + data["verification"] = {"source_report": source_report, + "accepted_models": sorted(accepted), + "rejected_or_unverified_models": sorted(set(rows) - accepted), + "scope": "Sample-level acceptance is not a claim of all-API or all-parameter coverage.", + "verified_top_level_entries": sum(r["conversion_status"] == "verified_sampled_usage" for r in data["top_level_api"]), + "verified_public_member_entries": sum(r["conversion_status"] == "verified_sampled_usage" for r in data["members"].values())} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--validation-report", type=Path, help="Optional strict stage-3 report for sample evidence") + parser.add_argument("--traces-dir", type=Path, help="Trace source member names supporting the validation report") + args = parser.parse_args() + data = inventory(args.manifest.resolve()) + if args.validation_report is not None: + validation_evidence(data, args.validation_report, args.traces_dir) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n") + print(json.dumps(data["counts"], indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index 233b89f8..7d795ef1 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10, <3.14" resolution-markers = [ "python_full_version >= '3.12'", @@ -3227,7 +3227,7 @@ wheels = [ [[package]] name = "simplecadapi" -version = "2.2.0b1" +version = "2.2.1" source = { editable = "." } dependencies = [ { name = "cadquery-ocp" }, @@ -3239,7 +3239,6 @@ dependencies = [ { name = "ocp-gordon" }, { name = "plotly" }, { name = "py-slvs" }, - { name = "pymupdf" }, { name = "rfc8785" }, { name = "rich" }, { name = "tomli", marker = "python_full_version < '3.11'" }, @@ -3251,9 +3250,19 @@ dev = [ { name = "black" }, { name = "isort" }, { name = "mypy" }, + { name = "pymupdf" }, { name = "pytest" }, { name = "pytest-cov" }, ] +drawing = [ + { name = "pymupdf" }, +] +editor = [ + { name = "starlette" }, + { name = "uvicorn" }, + { name = "websockets", version = "16.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "websockets", version = "17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] fem = [ { name = "ccx2paraview", extra = ["vtk"] }, { name = "gmsh" }, @@ -3275,10 +3284,15 @@ dev = [ { name = "jupyterlab" }, { name = "matplotlib" }, { name = "numpy-stl" }, + { name = "pymupdf" }, { name = "pytest" }, + { name = "starlette" }, { name = "trimesh" }, { name = "twine" }, { name = "ty" }, + { name = "uvicorn" }, + { name = "websockets", version = "16.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "websockets", version = "17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.metadata] @@ -3299,15 +3313,19 @@ requires-dist = [ { name = "ocp-gordon", specifier = ">=0.2,<0.3" }, { name = "plotly", specifier = ">=7.1.0" }, { name = "py-slvs", specifier = "==1.0.6" }, - { name = "pymupdf", specifier = ">=1.28.2" }, + { name = "pymupdf", marker = "extra == 'dev'", specifier = ">=1.28.2" }, + { name = "pymupdf", marker = "extra == 'drawing'", specifier = ">=1.28.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "rfc8785", specifier = ">=0.1.4,<0.2.0" }, { name = "rich", specifier = ">=14.0.0" }, + { name = "starlette", marker = "extra == 'editor'", specifier = ">=1.7.0" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0,<3" }, { name = "typing-extensions", specifier = ">=4.12,<5" }, + { name = "uvicorn", marker = "extra == 'editor'", specifier = ">=0.54.0" }, + { name = "websockets", marker = "extra == 'editor'", specifier = ">=16.1.1" }, ] -provides-extras = ["dev", "editor", "inspect", "whucad", "gmsh", "fem"] +provides-extras = ["dev", "drawing", "editor", "inspect", "whucad", "gmsh", "fem"] [package.metadata.requires-dev] dev = [ @@ -3316,10 +3334,14 @@ dev = [ { name = "jupyterlab", specifier = ">=4.5.5" }, { name = "matplotlib", specifier = ">=3.10.8" }, { name = "numpy-stl", specifier = ">=3.2.0" }, + { name = "pymupdf", specifier = ">=1.28.2" }, { name = "pytest" }, + { name = "starlette", specifier = ">=1.7.0" }, { name = "trimesh", specifier = ">=4.11.3" }, { name = "twine", specifier = ">=6.1.0" }, { name = "ty", specifier = ">=0.0.18" }, + { name = "uvicorn", specifier = ">=0.54.0" }, + { name = "websockets", specifier = ">=16.1.1" }, ] [[package]]