Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3733280
ci(release): the release page carries the version's update notes
NiJingzhe Oct 26, 2026
476511b
fix(diagnostics): fillet, chamfer and ql failures carry their picture…
NiJingzhe Oct 27, 2026
b626269
feat(verify): simplecadapi.verify and the verify extra for verifier s…
NiJingzhe Oct 27, 2026
609e2bc
perf(editor): fewer kernel round trips per settle, background tessell…
NiJingzhe Oct 27, 2026
62f6171
feat(editor): push the model over the WebSocket with the preview event
NiJingzhe Oct 27, 2026
4e7305f
fix(runtime): a lineage record of another graph is a cache miss, not …
NiJingzhe Oct 27, 2026
cb1a2b3
fix(editor): a repeated parameter value is not a second rebuild
NiJingzhe Oct 27, 2026
f31b306
fix(runtime): a cached cell with no lineage record runs once and repa…
NiJingzhe Oct 27, 2026
49650fb
perf(runtime): use() hands out the last run's product while the child…
NiJingzhe Oct 27, 2026
89bdc97
fix(runtime): delete cache entries whose classes are gone instead of …
NiJingzhe Oct 27, 2026
3971b9a
perf(editor): incremental preview, signature beside the build, reply …
NiJingzhe Oct 27, 2026
e80468a
perf(editor): the kernel keeps what its cells made; saved values foll…
NiJingzhe Oct 28, 2026
da24f18
feat: shape.wrapped is an OCP shape when OCP is installed
NiJingzhe Oct 28, 2026
a20d3c5
fix(editor): a partial or damaged model file does not keep the editor…
NiJingzhe Oct 28, 2026
ac1b50b
fix: review findings in the editor, the cell memory and use() memo
NiJingzhe Oct 28, 2026
ebe892a
test: restore test_rearchitecture_2_0_kernel_builders; record the old…
NiJingzhe Oct 28, 2026
d54d0d0
fix: a failing fillet/chamfer and a failing ql exactly() attach their…
NiJingzhe Oct 28, 2026
d941cda
fix: second-round review findings (failure pictures, use() owners, re…
NiJingzhe Oct 28, 2026
7edd550
style: add .clang-format and format the C++ tree with it
NiJingzhe Oct 29, 2026
a4c39a3
ci: format check, ASan/UBSan job, golden strictness, stub freshness, …
NiJingzhe Oct 29, 2026
7345e01
refactor(bindings): one exception translator ordered by priority; the…
NiJingzhe Oct 29, 2026
764e4a3
docs: a human README, CONTRIBUTING, reading order and landing plan; t…
NiJingzhe Oct 29, 2026
b4f8067
refactor: name the tolerances (scad::tol) instead of repeating their …
NiJingzhe Oct 29, 2026
e792c0e
refactor(render,reactive,io,std,sketch,semantic): split draw_panel, O…
NiJingzhe Oct 29, 2026
954ec58
refactor(render,std,io,semantic,reactive,product,ops): split render_s…
NiJingzhe Oct 29, 2026
eeb312c
refactor(ql,std,semantic,sketch,surface,reactive,topo): split the rem…
NiJingzhe Oct 29, 2026
01ab3ca
refactor(io,reactive): glb_json and Session::recompute below the 80-l…
NiJingzhe Oct 29, 2026
e9e3ee8
build: type stubs (.pyi) of the compiled module, generated by tools/g…
NiJingzhe Oct 29, 2026
ee96102
build: make the generated stubs reproducible
NiJingzhe Oct 29, 2026
53cbdc8
fix: CI stub step quoting; stubs without test-only modules; the sanit…
NiJingzhe Oct 29, 2026
497db13
Merge PR #66 (fillet, chamfer and ql failure pictures) into the serve…
NiJingzhe Oct 29, 2026
cc9c347
Merge PR #67 (simplecadapi.verify, the verify extra) into the serve b…
NiJingzhe Oct 29, 2026
02c3baa
Merge PR #65 (the release page carries the update notes) into the ser…
NiJingzhe Oct 29, 2026
0ae7e58
fix(tests): the reference interpreter is never this repository
NiJingzhe Oct 29, 2026
4f5bcd7
test: the verifier-bridge test follows shape.wrapped becoming an OCP …
NiJingzhe Oct 29, 2026
9ae54df
ci: a Release run of the C++ doctests; clang-tidy only on the engine …
NiJingzhe Oct 29, 2026
b302ca8
test/ci: the default bolt is held to 2.2.6's value on each platform; …
NiJingzhe Oct 29, 2026
674137d
ci: UBSan skips the alignment check for everything that comes from OC…
NiJingzhe Oct 29, 2026
5d6b191
ci: LSan skips OCCT's process-lifetime singletons
NiJingzhe Oct 29, 2026
424a812
ci: the C++ Release and ASan/UBSan jobs and the stub check are required
NiJingzhe Oct 29, 2026
af2b601
refactor(catch): every swallowed failure is an explicit, narrowed fal…
NiJingzhe Oct 29, 2026
971f42f
refactor(optional): every optional access is dominated by a check or …
NiJingzhe Oct 29, 2026
0537e45
build: regenerate the stubs (the provider signature lost its highligh…
NiJingzhe Oct 29, 2026
fee292c
refactor(optional): the last unchecked optional access (selection.cpp…
NiJingzhe Oct 29, 2026
f448af3
fix: an OCCT failure while reading a geometry type or closedness make…
NiJingzhe Oct 30, 2026
e06a4db
feat: Var and Expr behave like numbers where Python asks for one
NiJingzhe Oct 30, 2026
e0533cc
fix: the cell memory sees every write to a shape's semantic state, no…
NiJingzhe Oct 30, 2026
8ea87e8
docs(skill): notebook cell design - where cells go, when a number is …
NiJingzhe Oct 30, 2026
c1fec88
build: regenerate the stubs (Var / Expr numeric protocol, AnyShape._s…
NiJingzhe Oct 30, 2026
219afa7
Merge pull request #68 from PhySpace/feat/sca-serve
NiJingzhe Oct 30, 2026
2ce6b3f
release: 2.2.7
NiJingzhe Oct 30, 2026
f5db0ee
Merge pull request #69 from PhySpace/release/2.2.7
NiJingzhe Oct 30, 2026
7b31b33
fix: a checkout is a built source tree only when the compiled module …
NiJingzhe Oct 30, 2026
e64224c
test: drop the assertion that the checkout is built (true only in a d…
NiJingzhe Oct 30, 2026
9ba1c1b
Merge pull request #71 from PhySpace/fix/built-tree-detection
NiJingzhe Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
152 changes: 152 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,140 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: python3 src/cpp/tools/build_occt.py --prefix "$HOME/occt"

format:
# No OpenCASCADE needed: runs in seconds, ahead of the heavy jobs.
name: clang-format
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Install clang-format
run: python3 -m pip install clang-format==18.1.8

- name: Check formatting
run: CLANG_FORMAT=clang-format src/cpp/tools/check_format.sh

occt-guard:
# Every boolean / fillet / offset builder is constructed in a function that runs under occt_guard (or says, with
# `// occt_guard: the caller X ...`, that its one caller does). See src/cpp/tools/check_occt_guard.py.
name: OCCT guard audit
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Audit
run: python3 src/cpp/tools/check_occt_guard.py --strict

cpp-tests:
# The engine's own doctest executables (one per area), Release, no sanitizer: what the Python jobs below never run.
name: C++ tests (Release)
needs: occt
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Restore the static OpenCASCADE build
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/occt
key: occt-ci-linux-${{ hashFiles('src/cpp/tools/build_occt.py') }}
fail-on-cache-miss: true

- name: Configure
run: >
cmake -S src/cpp -B build/release -G Ninja
-DCMAKE_BUILD_TYPE=Release -DSCAD_BUILD_PYTHON=OFF -DSCAD_BUILD_TESTS=ON
-DSCAD_OCCT_PREFIX="$HOME/occt"

- name: Build
run: cmake --build build/release -j"$(nproc)"

- name: Run the doctest executables
run: ctest --test-dir build/release --output-on-failure -j"$(nproc)"

sanitize:
# The OCCT it links is not instrumented, so OCCT's own reports are suppressed (tools/ubsan.supp, tools/lsan.supp);
# only the doctest executables are built (no Python layer).
name: C++ tests (ASan + UBSan)
needs: occt
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Restore the static OpenCASCADE build
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/occt
key: occt-ci-linux-${{ hashFiles('src/cpp/tools/build_occt.py') }}
fail-on-cache-miss: true

- name: Configure
# SCAD_SANITIZE adds -fsanitize=address,undefined (no thread sanitizer option exists). The Python module is not
# built: loading an ASan module into an uninstrumented interpreter needs LD_PRELOAD games, and the doctest
# executables cover the engine.
run: >
cmake -S src/cpp -B build/asan -G Ninja
-DCMAKE_BUILD_TYPE=RelWithDebInfo
-DSCAD_SANITIZE=ON -DSCAD_BUILD_PYTHON=OFF -DSCAD_BUILD_TESTS=ON
-DSCAD_OCCT_PREFIX="$HOME/occt"

- name: Build
run: cmake --build build/asan -j"$(nproc)"

- name: Run the doctest executables
env:
ASAN_OPTIONS: halt_on_error=1:abort_on_error=1:detect_leaks=1:strict_string_checks=1:check_initialization_order=1
LSAN_OPTIONS: suppressions=${{ github.workspace }}/src/cpp/tools/lsan.supp:print_suppressions=0
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:suppressions=${{ github.workspace }}/src/cpp/tools/ubsan.supp
run: ctest --test-dir build/asan --output-on-failure -j"$(nproc)"

clang-tidy:
# Advisory: .clang-tidy is small but the code base has never been run through it. Changed files only.
name: clang-tidy (advisory)
needs: occt
runs-on: ubuntu-latest
timeout-minutes: 60
continue-on-error: true
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Restore the static OpenCASCADE build
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/occt
key: occt-ci-linux-${{ hashFiles('src/cpp/tools/build_occt.py') }}
fail-on-cache-miss: true

- name: Install clang-tidy
run: python3 -m pip install clang-tidy==18.1.8

- name: Configure (compile_commands.json)
run: >
cmake -S src/cpp -B build/tidy -G Ninja
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DSCAD_BUILD_PYTHON=OFF
-DSCAD_OCCT_PREFIX="$HOME/occt"

- name: Run clang-tidy on the changed files
run: |
base="${{ github.event.pull_request.base.sha || github.event.before }}"
if [ -z "$base" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then base="HEAD~1"; fi
# The engine only: the bindings need Python.h and pybind11, which a configure without the Python layer
# (SCAD_BUILD_PYTHON=OFF) does not put in the compile database.
files=$(git diff --name-only --diff-filter=d "$base" HEAD -- 'src/cpp/src/*.cpp' || true)
if [ -z "$files" ]; then echo "no C++ sources changed"; exit 0; fi
# shellcheck disable=SC2086
clang-tidy -p build/tidy --quiet $files

test:
name: Python ${{ matrix.python-version }}
needs: occt
Expand Down Expand Up @@ -82,6 +216,24 @@ jobs:
- name: Run tests
run: uv run pytest test tests -q

- name: Run the C++ parity tests (goldens must exist)
# SCAD_GOLDEN_STRICT (read by tests/python/ref.py) makes a missing golden file a failure, not a silent recording;
# SCAD_REGOLDEN is refused under it.
env:
SCAD_GOLDEN_STRICT: "1"
run: uv run pytest src/cpp/tests/python -q

- name: Check the type stubs are current
# The checked-in stubs (generated on macOS arm64) are reproduced on Linux.
if: matrix.python-version == '3.12'
run: |
uv pip install pybind11-stubgen==2.5.5
PYTHON="uv run --no-sync python" src/cpp/tools/gen_stubs.sh --check

# The verifier bridge needs OCP, which the SDK and the suite above never do: it is overlaid for this one run.
- name: Verifier bridge (OCP)
run: uv run --with "cadquery-ocp-novtk>=7.9.3.1,<8" pytest src/cpp/tests/python/test_verify_bridge.py -q

editor-e2e:
needs: occt
# The editor tests that boot a real marimo kernel (about a minute); the main
Expand Down
19 changes: 17 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ jobs:
run: |
.venv/bin/python -m pytest test tests -q
.venv/bin/python -m pytest src/cpp/tests/python -q
# Last: the verifier bridge needs OCP, which nothing above may find installed.
uv pip install --python .venv/bin/python "cadquery-ocp-novtk>=7.9.3.1,<8"
.venv/bin/python -m pytest src/cpp/tests/python/test_verify_bridge.py -q

release:
name: Build the skill and publish the release
Expand Down Expand Up @@ -432,10 +435,22 @@ jobs:
if [[ "$RELEASE_TAG" =~ (a|b|rc)[0-9]+$ ]]; then
release_flags+=(--prerelease --latest=false)
fi
# The page shows the version's update notes (docs/updates/<version>.md), as every release page has; only a
# version without notes falls back to the generated list of merged pull requests.
version="${RELEASE_TAG#v}"
notes=(--generate-notes)
if [[ -f "docs/updates/${version}.md" ]]; then
cp "docs/updates/${version}.md" "$RUNNER_TEMP/release-notes.md"
if [[ -f "docs/updates/${version}.zh-CN.md" ]]; then
printf '\n---\n中文说明:[docs/updates/%s.zh-CN.md](%s/%s/blob/%s/docs/updates/%s.zh-CN.md)\n' \
"$version" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$RELEASE_TAG" "$version" >> "$RUNNER_TEMP/release-notes.md"
fi
notes=(--notes-file "$RUNNER_TEMP/release-notes.md")
fi
gh release create "$RELEASE_TAG" dist/* skills/simplecadapi.tar.gz \
--verify-tag \
--generate-notes \
--title "$RELEASE_TAG" \
"${notes[@]}" \
--title "simplecadapi ${version}" \
"${release_flags[@]}"

publish-pypi:
Expand Down
111 changes: 111 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,117 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and

## Unreleased

## 2.2.7 — 2026-10-30

### Fixed
- The editor starts with whatever model file is on disk: a model kept by an older build that leaves parts out, or a damaged
one, is not restored (and no longer stops the kernel from starting, which showed as "starting the kernel" for ever), and
a model that leaves parts out is not filed.
- A cache entry that names classes which no longer exist (written by an older build: `No module named
'simplecadapi._native'`) is deleted and written again. marimo restores such a value as a stub; the runner dropped the
cache for the run and ran again without it, which neither read nor wrote entries, so the same stubs came back on every
later run and every cell ran every time. A notebook with such entries (a child run through `use`, the six-axis arm's
gripper) now takes 0.8 s instead of 3.3-6 s once the entries are replaced. A stub for any other reason (a value that
cannot be pickled) keeps the old behaviour.
- A cell whose value is cached but whose lineage record is missing now runs once and writes both again. It used to
run on every call and never repair the record (the cell stayed active nowhere, so nothing was saved), which showed as
a notebook that never hit its cache, such as a child notebook run through `use`.
- Editor: a slider drag that pauses and is then released no longer rebuilds twice. The page sends the value when the
drag pauses and again on release; the server now skips a value the model already has (unless the last run failed,
when the same value is a retry).
- A cached cell whose lineage record belongs to another graph is a cache miss and runs again. A part-family member
(`use("child.py", id="member")`) with such a record used to fail with `value contains graph node '...' not owned by
active graph 'member'`. A restored cell no longer overwrites its record, and a record is only written under the id
its cache belongs to.
- A failed fillet or chamfer, and a ql cardinality error, carry their diagnostic picture again (as in 2.2.6): the
failing edge(s) as orange lines, or the matched faces orange and the near misses purple. 2.2.7b1 drew pictures for
failed booleans only. Highlighted faces are shaded with the standard material, so the palette colours read as before.
- A refused preview post names what the editor server said (`HTTP 400: damaged preview: ...`) instead of only the status line.

### Changed
- `Var` and `Expr` behave like numbers where Python asks for one: `int`, `round`, `floor`/`ceil`, comparisons
(`r > 3`), `max`/`min`/`sorted`, `range(n)`, `[0] * n`, `lst[n - 1]`, `//`, `%`, `f"{r:.2f}"`, `str`, and numpy
scalars as operands now work, and `r == 5.0` is `True` (it was `False`). Arithmetic still builds the formula; these
decisions read the value the expression has now. A ql predicate with a Var-derived value
(`ql.prop("geom.length", ">=", 2 * math.pi * radius)`) matches, where 2.2.6 matched nothing. `bool(var)` and
`json.dumps(var)` still raise. See `src/cpp/docs/DEVIATIONS.md`; the matrix test is
`src/cpp/tests/python/test_expr_numeric_protocol.py`.
- A handled failure that used to leave no trace now prints a `[sca] ...` line on stderr: the preview warm-up, an
unreadable lineage record, a cache entry that could not be deleted, a preview frame that could not be built, a
notebook that could not be signed, and a kept model that is damaged and is built again.
- `shape.wrapped` is an OCP shape when OCP is installed (the `verify` extra), of the class of its kind (`TopoDS_Solid`,
`TopoDS_Face`, ...), as it is in CadQuery: the OCCT code an agent writes for verifier scripts runs as written
(`BRepGProp.VolumeProperties_s(solid.wrapped, props)`). It is a copy made through BinTools, with its location and
orientation: every access makes a new one (`s.wrapped is s.wrapped` is false), so a change made to it (a
`BRepBuilderAPI_Transform`, `BRepTools.Clean`) does not reach the SDK's shape, which is unlike CadQuery's live handle
and unlike 2.2.6, where `.wrapped` was the shape itself. To use a result, convert it back with
`simplecadapi._glue.bridge.ocp_to_native`. Without OCP it stays the SDK's own OCCT shape. `SCA_WRAPPED=native|ocp`
chooses, and is read on each access. The SDK's own Python layers take `shape._native_wrapped`, which
is always the native one, and the functions that take a raw shape (the `inspect.brep` queries, the STEP export, the
geometry helpers of `simplecadapi.kernel`) accept either.
- Editor kernel: the cells keep what they made (`simplecadapi.runtime.cache._CellMemory`, 12 results; `SCA_NO_CELL_MEMORY=1`
turns it off). marimo unpickles an entry while it answers the cache lookup, which is about a second for each of the
six-axis arm's assemblies; the kernel now asks its memory right after the cell's key is hashed and before the disk,
and hands out the same objects when the entry is one this process holds. Only plain data and the SDK's value types are
kept. They are functional except for the semantic state of a shape: `apply_tag`, `set_metadata` and `auto_tag_faces`
change the shape they are called on and return the same object, so a later cell that tags a shape it received would
change what the producing cell has kept (the disk copy has none of it). Every root of shapes now counts its writes
(`AnyShape._semantic_revision`); an entry whose counts moved since it was kept is dropped and the next restore reads
the disk copy as before. Reading a shape or consuming it in an operator does not count. Parts and assemblies are
followed to their bodies and components.
The limit is twelve entries, not a number of bytes, and costs about 300 MB on the six-axis arm; only the editor's
kernel has a memory (`sca run` and the tests do not). With a marimo whose `cache_attempt_from_hash` has another
signature than the one this was written for, a line on stderr says so and the memory is off. `SCA_EDITOR_TRACE=1` with
`SCA_EDITOR_TRACE_FILE=<path>` writes what the cache did with each cell and how long each part took.
- The saved parameter values in a cell's digest follow what the cell reads. A value the notebook's parameter cell
defines used to be part of the digest of every cell below that cell, so changing one joint angle changed the digest
of the cell that builds the parts and every cell after it. It is now part of the digest of the cell that defines it,
of the cells that read it, and of the cells that read from those. On the six-axis arm a joint angle that has not been
seen before takes 2.3-2.7 s to show instead of 5.7-6.5 s, and one that has been seen takes 0.6 s instead of 5.5 s.
The digests of cells of a notebook that has saved parameter values are not those of the previous version, so the
first run after the upgrade builds those notebooks again once.
- Editor: the preview is incremental. A model's parts are identified by the content of their geometry, so the kernel
posts only the parts the server does not hold, the server sends each page only the parts that page lacks (a page that
joins late, or asks with `preview.resync`, is given the whole model), and the page builds geometry only for new
parts and reuses the rest. On the six-axis arm a joint angle now moves 115 KB instead of 6.26 MB: the kernel's post
takes 17 ms instead of 285-610 ms, the page handles the frame in 2-5 ms with no long task, and from the last cell
to the model on screen it takes 0.36-0.41 s instead of 1.3-3.2 s. `simplecadapi.scene.preview.encode_preview` takes
`omit=` for the parts to leave out; a post that is not a readable preview is answered `400` (`409` is only for parts
the server does not hold), and the server replaces the model it holds as one value; the editor keeps parts in `simplecadapi.editor.preview_parts.PartStore`.
- Editor: the signature that files a model on disk is taken beside the build, not before it, and the server replies to
the kernel's post before filing the model and writing the snapshot. `SCA_EDITOR_TRACE=1` prints how long each step
of a settle takes.
- `use` keeps the product of the last run of each child notebook in the process and hands it out again while the
child's static digest (source, header, local modules, input files, the children it uses) and the overrides are
unchanged (`simplecadapi.runtime.usememo`; `SCA_NO_USE_MEMO=1` turns it off). One entry per (notebook, id): the run's
nodes are registered by graph id, so the entry is the last run's, and two notebooks that run under one graph id
replace each other's entry. The editor's kernel is a long-lived process, so a
new `OPEN_STROKE` on the six-axis arm rebuilds in 4.2 s instead of 6.4 s. Children with a computed `use` path and
overrides that are not plain data are not kept. Prefetch no longer starts workers for what is kept.
- Editor: the model is pushed over the WebSocket with the `preview` event, in one binary frame
(`<u32 big-endian N><N bytes of JSON envelope><SCAPREV1 preview>`), and a page that joins late is given it on
connect: no second HTTP request for it. A model already on screen still travels as the bare notice
(`unchanged: true`). `GET /api/preview/latest` stays for agents and tools.
- Editor: the model reaches the page sooner after the last cell. A settle makes one call into the kernel instead of
three (the cells are listed in the call that publishes the preview), the duplicate settle a boot used to run is gone,
and the settle debounce is 0.12 s. On the six-axis arm the time from the last cell to the preview event went from
about 0.83 s (plus a second, redundant round of 0.27 s) to about 0.36 s.
- Editor: parts are tessellated in the background while the next cell computes (`simplecadapi.scene.prewarm`;
`SCA_NO_PREVIEW_WARM=1` turns it off). `cached_mesh` runs without the interpreter lock for that; a failure is
recorded on the shape back under the lock.
- Preview edge polylines are keyed with one numpy pass instead of one `struct.pack` per point; the bytes are identical.

### Added
- `simplecadapi.verify` (`to_ocp`, `from_ocp`) and the `verify` extra (`pip install "simplecadapi[verify]"`, OCP 7.9
without VTK) for verifier scripts. With OCP installed `shape.wrapped` is an OCP shape itself (see Changed), so both
routes work; `to_ocp` / `from_ocp` are the explicit ones. The SDK itself still never imports OCP. The skill gains
`references/discipline/verifier-scripts.md` with the recipes (volume, bounds, validity, point in solid, distance,
overlap) and a verifier skeleton.

### Removed
- The `fem` extra (`gmsh` and `ccx2paraview[VTK]`) and the README steps that used it; the `gmsh` extra stays. VTK is no
longer in the lock file at all.

## 2.2.7b1 — 2026-10-24

### Added
Expand Down
Loading
Loading