diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9e4e67..d80c5f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,10 @@ jobs: - name: Run tests run: uv run pytest test tests -q + # 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0592f98..85efdbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index d746aab..645baa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ## Unreleased +### Added +- `simplecadapi.verify` (`to_ocp`, `from_ocp`) and the `verify` extra (`pip install "simplecadapi[verify]"`, OCP 7.9 + without VTK) for verifier scripts: `shape.wrapped` is the SDK's own binding and OCP functions reject it. 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 diff --git a/README.md b/README.md index 82c5639..5fa8bac 100644 --- a/README.md +++ b/README.md @@ -484,37 +484,6 @@ Both outputs contain the same oriented triangles and require no optional remeshing dependency. Control curved-surface accuracy with `linear_deflection` and `angular_deflection_degrees`. -### Optional CalculiX FEM workflow - -The AP242/Gmsh example also includes an optional CalculiX FEM workflow. Install -the Python-side FEM dependencies with `uv sync --extra fem`, and install the -external CalculiX solver separately (on macOS: `brew install -costerwi/homebrew-calculix/calculix-ccx`). The example uses consistent `mm`, -`N`, and `MPa` units: - -```bash -uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py \ - --ccx "$(brew --prefix calculix-ccx)/bin/ccx_2.23" -uv run --extra fem python examples/ap242_gmsh_volume_mesh/visualize_calculix.py -uv run --extra fem python examples/ap242_gmsh_volume_mesh/study_mesh_convergence.py \ - --ccx "$(brew --prefix calculix-ccx)/bin/ccx_2.23" \ - --linear-solver "ITERATIVE CHOLESKY" --solver-timeout 2400 -``` - -The analysis writes CalculiX `.inp`, `.dat`, `.frd`, solver-log, summary JSON, -ParaView `.vtu`, and displaced von-Mises PNG artifacts. The preview shows the -load physical group's yellow boundary and red `-Z` force arrows without -covering the stress heatmap. The convergence study supports `--resume`; failed -solver levels are reported separately and never enter the numerical sequence. - -The checked-in `-1000 N` study evaluates eleven mesh sizes from `h=3.0 mm` to -`h=0.25 mm`. A platform requires three consecutive refinement pairs below `5%` -maximum-displacement change and `10%` peak integration-point von-Mises change. -The verification level N is `h=0.25 mm` (`0.0331843 mm`, `98.6392 MPa`), so the -recommended production level N-1 is `h=0.27 mm`. Fine levels use iterative -Cholesky after a same-mesh comparison at `h=0.375 mm` matched SPOOLES within -`0.005%`; this avoids the direct solver's in-memory capacity limit. - ## Replayable Operation Graphs Use an explicit `GraphSession` when a geometry flow outside a notebook should @@ -681,10 +650,9 @@ uv run python examples/flange_plate/verify.py sca run examples/compact_two_stage_planetary_reducer/compact_two_stage_planetary_reducer.py uv run python examples/compact_two_stage_planetary_reducer/export_mjcf.py -# FEM (AP242 STEP -> Gmsh volume mesh -> Calculix statics) +# Gmsh volume mesh (AP242 STEP -> volume mesh) uv run python examples/ap242_gmsh_volume_mesh/export_step.py uv run --extra gmsh python examples/ap242_gmsh_volume_mesh/export_fem_mesh.py -uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py ``` To try the editor on a real model, `sca example u_link` copies a motor mount diff --git a/README.zh-CN.md b/README.zh-CN.md index 811631a..cc581e8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -438,33 +438,6 @@ STL 与 OBJ 共用 OpenCASCADE 对求值后 BREP 的直接三角化。两种格 定向三角面,不再需要可选 remeshing 依赖。曲面精度由 `linear_deflection` 和 `angular_deflection_degrees` 控制。 -### 可选 CalculiX FEM 流程 - -AP242/Gmsh 示例还包含可选 CalculiX FEM 流程。Python 侧依赖通过 -`uv sync --extra fem` 安装;CalculiX 求解器需要单独安装(macOS: -`brew install costerwi/homebrew-calculix/calculix-ccx`): - -```bash -uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py \ - --ccx "$(brew --prefix calculix-ccx)/bin/ccx_2.23" -uv run --extra fem python examples/ap242_gmsh_volume_mesh/visualize_calculix.py -uv run --extra fem python examples/ap242_gmsh_volume_mesh/study_mesh_convergence.py \ - --ccx "$(brew --prefix calculix-ccx)/bin/ccx_2.23" \ - --linear-solver "ITERATIVE CHOLESKY" --solver-timeout 2400 -``` - -分析统一使用 `mm`、`N`、`MPa`,输出 CalculiX `.inp`、`.dat`、`.frd`、求解日志、 -摘要 JSON、ParaView `.vtu` 和位移放大后的 von Mises 云图 PNG。图中用黄色轮廓 -标出载荷 physical group,并用红色 `-Z` 箭头表示载荷方向,不覆盖应力热力图。 -收敛研究支持 `--resume`;失败的求解级别单独记录,不会混入数值序列。 - -仓库中的 `-1000 N` 研究从 `h=3.0 mm` 加密到 `h=0.25 mm`,共 11 级。平台要求 -连续三组细化同时满足最大位移变化低于 `5%`、积分点峰值 von Mises 应力变化低于 -`10%`。第 N 级验证网格为 `h=0.25 mm`(`0.0331843 mm`、`98.6392 MPa`), -因此生产计算推荐第 N-1 级 `h=0.27 mm`。细网格使用迭代 Cholesky;在 -`h=0.375 mm` 同网格上与 SPOOLES 的位移和峰值应力差异均低于 `0.005%`,从而 -绕过直接求解器的内存容量限制。 - ## 可重放操作图 notebook 之外的几何流程需要检查、序列化或重放时,使用显式 `GraphSession`: @@ -572,10 +545,9 @@ uv run python examples/flange_plate/verify.py sca run examples/compact_two_stage_planetary_reducer/compact_two_stage_planetary_reducer.py uv run python examples/compact_two_stage_planetary_reducer/export_mjcf.py -# FEM(AP242 STEP -> Gmsh 体网格 -> CalculiX 静力) +# Gmsh 体网格(AP242 STEP -> 体网格) uv run python examples/ap242_gmsh_volume_mesh/export_step.py uv run --extra gmsh python examples/ap242_gmsh_volume_mesh/export_fem_mesh.py -uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py ``` 想在真实模型上试用编辑器:`sca example u_link` 把一个电机座(两个零件 notebook 加 diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md index 236741f..abc351e 100644 --- a/docs/skill/SKILL.md +++ b/docs/skill/SKILL.md @@ -208,6 +208,7 @@ print(len(rebuilt)) - `references/workflows/` — goal-oriented workflows, including drawing reconstruction - `references/domains/` — capability domains, including drawing inspection - `references/discipline/` — modeling knowledge and invariants +- `references/discipline/verifier-scripts.md` — writing a verifier script (OCP through `simplecadapi.verify`) - `references/SDK_OVERVIEW.md` — package-level map - `references/inspect/brep-reverse-engineering.md` - `references/domains/addon-development.md` — `sca` addon CLI and authoring guide diff --git a/docs/skill/references/discipline/geometric-validation.md b/docs/skill/references/discipline/geometric-validation.md index 40a12ff..2b64fff 100644 --- a/docs/skill/references/discipline/geometric-validation.md +++ b/docs/skill/references/discipline/geometric-validation.md @@ -29,6 +29,10 @@ invalid — correct bounds and volume, one bad face — which survives until the next operation fails far from the cause. Gate every boolean result, not just the final one. +A verifier that measures with the full OCCT toolbox (point classification, +minimum distance, Boolean overlap) is covered by +`references/discipline/verifier-scripts.md`. + ## Incremental grounding Every major step prints a small QL-derived fact: selected face count, a diff --git a/docs/skill/references/discipline/verifier-scripts.md b/docs/skill/references/discipline/verifier-scripts.md new file mode 100644 index 0000000..904d65f --- /dev/null +++ b/docs/skill/references/discipline/verifier-scripts.md @@ -0,0 +1,147 @@ +# Discipline: Verifier Scripts + +A verifier script measures a finished model against the specification and exits +non-zero when a requirement fails. It is separate from the modeling script: it +imports the model, measures it independently, and prints one line per check. + +## Evidence labels + +- **SDK contract:** `simplecadapi.verify.to_ocp(shape)` returns an OCP + `TopoDS_Shape` copy of any SDK shape; `from_ocp(shape)` returns the SDK shape + of an OCP shape. `Solid.get_volume()`, `simplecadapi.inspect.brep` and QL + need no extra install. +- **Validation guidance:** the checks and the skeleton below are + recommendations, not SDK methods. + +## Use the SDK first, OCP for the rest + +Measure with the SDK where it can answer: `get_volume()`, QL for face and edge +facts, `inspect.brep` for validity and shell closure +(`references/discipline/geometric-validation.md`). Reach for OCP only for what +the SDK does not expose: point-in-solid classification, minimum distance +between shapes, Boolean overlap volume, mass-property centroids and inertia. + +The SDK itself does not use OCP. Install it for verifier scripts only: + +```bash +pip install "simplecadapi[verify]" # OCP 7.9, the OCCT generation the SDK ships +``` + +## `shape.wrapped` is not an OCP shape + +`shape.wrapped` is the SDK's own binding. Passing it to an OCP function fails +with `incompatible function arguments`. Convert with `to_ocp`: + +```python +from simplecadapi.verify import to_ocp + +BRepGProp.VolumeProperties_s(solid.wrapped, props) # TypeError +BRepGProp.VolumeProperties_s(to_ocp(solid), props) # works +``` + +`to_ocp` copies through OpenCASCADE's binary format (milliseconds for a part). +Convert once per shape and reuse the result; tags and tracking history stay on +the SDK shape. An OCP shape you built yourself comes back with `from_ocp`. + +The other direction has one more step: the SDK's diagnostic functions +(`inspect_shape_rbrepinspection`, ...) take the raw shape of an SDK shape, its +`.wrapped`. For a shape that came out of OCP, pass `from_ocp(shape).wrapped` +(or keep the SDK shape and pass `solid.wrapped`). + +## Recipes (OCP 7.9) + +```python +from OCP.BRepAlgoAPI import BRepAlgoAPI_Common +from OCP.BRepBndLib import BRepBndLib +from OCP.BRepCheck import BRepCheck_Analyzer +from OCP.BRepClass3d import BRepClass3d_SolidClassifier +from OCP.BRepExtrema import BRepExtrema_DistShapeShape +from OCP.BRepGProp import BRepGProp +from OCP.Bnd import Bnd_Box +from OCP.GProp import GProp_GProps +from OCP.TopAbs import TopAbs_IN +from OCP.TopoDS import TopoDS +from OCP.gp import gp_Pnt + + +def volume(shape): + props = GProp_GProps() + BRepGProp.VolumeProperties_s(shape, props) + return props.Mass() + + +def bounds(shape): # (xmin, ymin, zmin, xmax, ymax, zmax) + box = Bnd_Box() + BRepBndLib.Add_s(shape, box) + return box.Get() + + +def is_valid(shape): + return BRepCheck_Analyzer(shape).IsValid() + + +def contains(solid, x, y, z, tol=1e-6): # solid: an OCP solid, see the cast below + return BRepClass3d_SolidClassifier(solid, gp_Pnt(x, y, z), tol).State() == TopAbs_IN + + +def gap(a, b): # minimum distance, 0 when they touch or overlap + measure = BRepExtrema_DistShapeShape(a, b) + assert measure.IsDone() + return measure.Value() + + +def overlap_volume(a, b): # interference between two parts + common = BRepAlgoAPI_Common(a, b) + assert common.IsDone() + return volume(common.Shape()) +``` + +- `to_ocp` returns a generic `TopoDS_Shape`. Functions that need the concrete + type want a cast: `TopoDS.Solid_s(to_ocp(solid))`. +- OCP 7.9 casts and static methods end in `_s` (`TopoDS.Solid_s`, + `BRepGProp.VolumeProperties_s`). OCP 8 renames them; the `verify` extra pins + 7.9 so documentation and code agree. +- Do not guess OCP names. Open the class in the OCCT reference, or + `help(OCP..)`, before the first call (a method that is not in + the documentation does not exist, however plausible it looks). + +## Skeleton + +```python +"""verify_bracket.py: exits 1 when a requirement is not met.""" +import math +import sys + +from simplecadapi.verify import to_ocp + +from bracket import build # the modeling script's result: a Solid + +checks = [] + + +def check(name, ok, detail=""): + checks.append(ok) + print(("PASS" if ok else "FAIL"), name, detail) + + +part = build() +shape = to_ocp(part) + +check("valid", is_valid(shape)) +check("volume 5151.77 mm3 +-0.1%", math.isclose(volume(shape), 5151.77, rel_tol=1e-3), f"{volume(shape):.2f}") +x0, y0, z0, x1, y1, z1 = bounds(shape) +check("height 30 mm", math.isclose(z1 - z0, 30.0, abs_tol=1e-6), f"{z1 - z0:.4f}") +check("bore is empty", not contains(TopoDS.Solid_s(shape), 0, 0, 15)) + +sys.exit(0 if all(checks) else 1) +``` + +Rules for a verifier: + +- Every user-specified dimension gets its own targeted check with the measured + value printed; "valid" alone proves nothing about size. +- Tolerances come from the specification, not from what happens to pass. +- A check that cannot run (missing OCP, unreadable file) is a failure, not a + skip: report it and exit non-zero. +- Do not edit the model to make a check pass; fix the model or report the + discrepancy. diff --git a/pyproject.toml b/pyproject.toml index b3242e4..f1f9ec5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,16 +57,18 @@ editor = [ # simplecadapi.inspect.brep is part of the default install; this extra is kept empty so that # `pip install "simplecadapi[inspect]"` from older instructions still works. inspect = [] +# Verifier scripts measure models with OCP; the SDK itself never imports it (simplecadapi.verify bridges the two). +# The build without VTK (same `OCP` import, no 100 MB VTK) and the OCCT generation the SDK ships (7.9): OCP 8 renames +# the casts and changes signatures. +verify = [ + "cadquery-ocp-novtk>=7.9.3.1,<8", +] whucad = [ "h5py>=3.8", ] gmsh = [ "gmsh>=4.15,<5", ] -fem = [ - "gmsh>=4.15,<5", - "ccx2paraview[VTK]>=3.2,<4", -] [project.urls] Homepage = "https://github.com/PhySpace/SimpleCADAPI" diff --git a/src/cpp/tests/python/test_verify_bridge.py b/src/cpp/tests/python/test_verify_bridge.py new file mode 100644 index 0000000..969c306 --- /dev/null +++ b/src/cpp/tests/python/test_verify_bridge.py @@ -0,0 +1,129 @@ +"""simplecadapi.verify: SDK shapes to OCP and back, for verifier scripts. + +Runs only where OCP is installed (``uv run --with cadquery-ocp pytest ...``); the SDK and the rest of the suite never +need it. The verifier recipes of the skill (docs/skill/references/workflows/verifier-scripts.md) are run here too. +""" + +from __future__ import annotations + +import math + +import pytest + +import simplecadapi as scad +from simplecadapi import verify + +ocp = pytest.importorskip("OCP") + +from OCP.BRepAlgoAPI import BRepAlgoAPI_Common # noqa: E402 +from OCP.BRepBndLib import BRepBndLib # noqa: E402 +from OCP.BRepCheck import BRepCheck_Analyzer # noqa: E402 +from OCP.BRepClass3d import BRepClass3d_SolidClassifier # noqa: E402 +from OCP.BRepExtrema import BRepExtrema_DistShapeShape # noqa: E402 +from OCP.BRepGProp import BRepGProp # noqa: E402 +from OCP.Bnd import Bnd_Box # noqa: E402 +from OCP.GProp import GProp_GProps # noqa: E402 +from OCP.TopAbs import TopAbs_IN, TopAbs_ON # noqa: E402 +from OCP.TopoDS import TopoDS # noqa: E402 +from OCP.gp import gp_Pnt # noqa: E402 + + +def part(): + box = scad.make_box_rsolid(10, 20, 30, bottom_face_center=(0, 0, 0)) + hole = scad.make_cylinder_rsolid(3, 40, bottom_face_center=(0, 0, -5)) + return scad.cut_rsolid(box, hole) + + +def volume(shape): + props = GProp_GProps() + BRepGProp.VolumeProperties_s(shape, props) + return props.Mass() + + +def test_the_sdk_wrapped_shape_is_not_an_ocp_shape(): + with pytest.raises(TypeError, match="incompatible"): + BRepGProp.VolumeProperties_s(part().wrapped, GProp_GProps()) + + +def test_to_ocp_measures_like_the_sdk(): + solid = part() + shape = verify.to_ocp(solid) + assert math.isclose(volume(shape), solid.get_volume(), rel_tol=1e-12) + assert BRepCheck_Analyzer(shape).IsValid() + box = Bnd_Box() + BRepBndLib.Add_s(shape, box) + assert [round(v, 6) for v in box.Get()] == [-5.0, -10.0, 0.0, 5.0, 10.0, 30.0] + + +def test_recipes_point_in_solid_distance_and_overlap(): + solid = verify.to_ocp(part()) + classify = BRepClass3d_SolidClassifier(TopoDS.Solid_s(solid), gp_Pnt(4, 5, 6), 1e-6) + assert classify.State() == TopAbs_IN + in_hole = BRepClass3d_SolidClassifier(TopoDS.Solid_s(solid), gp_Pnt(0, 0, 15), 1e-6) + assert in_hole.State() != TopAbs_IN and in_hole.State() != TopAbs_ON + + other = verify.to_ocp(scad.make_box_rsolid(2, 2, 2, bottom_face_center=(20, 0, 0))) + distance = BRepExtrema_DistShapeShape(solid, other) + assert distance.IsDone() and math.isclose(distance.Value(), 14.0, abs_tol=1e-9) # 20 - 1 - 5 + + overlapping = verify.to_ocp(scad.make_box_rsolid(10, 10, 10, bottom_face_center=(0, 0, 25))) + common = BRepAlgoAPI_Common(solid, overlapping) + # the box overlaps the top 5 mm of the part; the 3 mm hole runs through that slab + assert common.IsDone() and math.isclose(volume(common.Shape()), 10 * 10 * 5 - math.pi * 3**2 * 5, rel_tol=1e-9) + + +def test_from_ocp_brings_a_shape_back(): + original = part() + back = verify.from_ocp(verify.to_ocp(original)) + assert isinstance(back, scad.Solid) + assert math.isclose(back.get_volume(), original.get_volume(), rel_tol=1e-12) + + +def test_wrong_arguments_say_what_to_pass(): + with pytest.raises(TypeError, match="expects an SDK shape"): + verify.to_ocp(verify.to_ocp(part())) + with pytest.raises(TypeError, match="expects an OCP TopoDS_Shape"): + verify.from_ocp(part()) + + +def test_a_missing_ocp_names_the_extra(monkeypatch): + import builtins + + real = builtins.__import__ + + def refuse(name, *args, **kwargs): + if name == "OCP" or name.startswith("OCP."): + raise ImportError("no OCP") + return real(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", refuse) + assert verify.ocp_available() is False + with pytest.raises(ImportError, match=r"simplecadapi\[verify\]"): + verify.to_ocp(part()) + + +def test_the_skill_page_code_runs(monkeypatch): + """The recipes and the skeleton of references/discipline/verifier-scripts.md run as written.""" + + import re + import sys + import types + from pathlib import Path + + page = Path(__file__).resolve().parents[4] / "docs/skill/references/discipline/verifier-scripts.md" + blocks = re.findall(r"```python\n(.*?)```", page.read_text(encoding="utf-8"), flags=re.S) + recipes = next(b for b in blocks if "def overlap_volume" in b) + skeleton = next(b for b in blocks if "verify_bracket.py" in b) + + bracket = types.ModuleType("bracket") + bracket.build = part + monkeypatch.setitem(sys.modules, "bracket", bracket) + + namespace: dict = {"__name__": "verifier"} + exec(compile(recipes, "recipes", "exec"), namespace) + exec(compile(skeleton.replace("sys.exit(0 if all(checks) else 1)", ""), "skeleton", "exec"), namespace) + assert namespace["checks"] and all(namespace["checks"]), namespace["checks"] + # every recipe answers on the part + shape = namespace["shape"] + assert namespace["contains"](TopoDS.Solid_s(shape), 4, 5, 6) + assert namespace["gap"](shape, verify.to_ocp(scad.make_box_rsolid(2, 2, 2, bottom_face_center=(20, 0, 0)))) == pytest.approx(14.0) diff --git a/src/simplecadapi/verify.py b/src/simplecadapi/verify.py new file mode 100644 index 0000000..81e76a1 --- /dev/null +++ b/src/simplecadapi/verify.py @@ -0,0 +1,87 @@ +"""Verifier scripts: hand SDK shapes to OCP and take OCP shapes back. + +The SDK computes on its own compiled OpenCASCADE (``simplecadapi._cpp``) and does not need OCP. A verifier script that +measures a model with the full OCCT toolbox (mass properties, point classification, distances, Boolean overlap, ...) +is easier to write against OCP, whose API every OCCT reference documents. Install it with the ``verify`` extra:: + + pip install "simplecadapi[verify]" + +``shape.wrapped`` is **not** an OCP object: it is the SDK's own binding, and OCP functions reject it with +``incompatible function arguments``. Convert instead:: + + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + from simplecadapi.verify import to_ocp + + props = GProp_GProps() + BRepGProp.VolumeProperties_s(to_ocp(solid), props) # not solid.wrapped + print(props.Mass()) + +The conversion goes through OpenCASCADE's binary format (geometry only, bit-exact doubles, a few milliseconds for a +part), so it copies: changes made to the OCP shape do not reach the SDK shape; use ``from_ocp`` to bring a result back. +Tags and tracking history stay with the SDK shape. +""" + +from __future__ import annotations + +import io +from typing import Any + +import simplecadapi._cpp as _cpp + +__all__ = ["to_ocp", "from_ocp", "ocp_available"] + +_INSTALL_HINT = ( + 'OCP is not installed. Verifier scripts that use OCP need the optional extra: pip install "simplecadapi[verify]" ' + "(the SDK itself does not use OCP)." +) + + +def ocp_available() -> bool: + """True when OCP can be imported.""" + + try: + import OCP # noqa: F401 + except ImportError: + return False + return True + + +def _require_ocp() -> None: + try: + import OCP # noqa: F401 + except ImportError as error: + raise ImportError(_INSTALL_HINT) from error + + +def to_ocp(shape: Any) -> Any: + """The OCP ``TopoDS_Shape`` of an SDK shape (a copy). + + Down-cast it when a function wants the concrete type: ``TopoDS.Solid_s(to_ocp(solid))``. + """ + + _require_ocp() + from OCP.BinTools import BinTools + from OCP.TopoDS import TopoDS_Shape + + if not hasattr(shape, "to_brep"): + raise TypeError( + f"to_ocp expects an SDK shape (Solid, Face, Wire, Edge, Vertex, Compound, ...), got {type(shape).__name__}; " + "an OCP shape is already one" + ) + result = TopoDS_Shape() + BinTools.Read_s(result, io.BytesIO(shape.to_brep())) + return result + + +def from_ocp(shape: Any) -> Any: + """The SDK shape (``Solid``, ``Face``, ... as the content says) of an OCP ``TopoDS_Shape`` (a copy).""" + + _require_ocp() + from OCP.BinTools import BinTools + + if hasattr(shape, "to_brep"): + raise TypeError("from_ocp expects an OCP TopoDS_Shape, got an SDK shape") + buffer = io.BytesIO() + BinTools.Write_s(shape, buffer) + return _cpp.from_brep(buffer.getvalue()) diff --git a/uv.lock b/uv.lock index ab78e6a..8c61338 100644 --- a/uv.lock +++ b/uv.lock @@ -212,21 +212,41 @@ css = [ ] [[package]] -name = "ccx2paraview" -version = "3.2.0" +name = "cadquery-ocp-novtk" +version = "7.9.3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cadquery-ocp-proxy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/80/2d3f15215a6ef3029bc4a5a58365cd6979a8d9dd2ca7b97e7ef929381522/ccx2paraview-3.2.0.tar.gz", hash = "sha256:c9f0c8d0fadd7de9a30be1b38ff53ecede49a87659e32b67ba32230d9dafd600", size = 36243, upload-time = "2024-12-19T21:17:20.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/da/46b9c79188084d4c650144ce8341b78ecfe3a790bcdd585de9a3a5ae9798/ccx2paraview-3.2.0-py3-none-any.whl", hash = "sha256:814520a138c4ac345809b16f6242b43e2f16bb926215bec069398a0672e887a9", size = 29352, upload-time = "2024-12-19T21:17:18.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/ce/287c003e8a96771854cac6563ca209a13a1b22a9bc012e32aafed2d5ebe5/cadquery_ocp_novtk-7.9.3.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cee061a62066e58b366d1b0647d7148ffa2b3a7cec08e0e612a59a25b019791a", size = 61702081, upload-time = "2026-05-28T13:07:19.388Z" }, + { url = "https://files.pythonhosted.org/packages/dd/18/801428d5be43a76f90d3a3327a4990c4955d6717fa35ad7485395f4e3954/cadquery_ocp_novtk-7.9.3.1.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:13e33a0c86171e517a3f5d6e191322ec8e69916051fd224ca78b6cc0e49878a7", size = 66324787, upload-time = "2026-05-28T13:07:22.856Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b1/8b22847370096e400436cb241ad66e1c8062f35b4068666ee132219d0fa7/cadquery_ocp_novtk-7.9.3.1.1-cp310-cp310-manylinux_2_31_aarch64.whl", hash = "sha256:50b269de006ad2cdb1e3ffd5740fdf1d3920913352ece0b68f6f6d4fcb6dfdc7", size = 62101524, upload-time = "2026-05-28T13:07:26.383Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/ee5ad24ecdf60b575aedc96ca49c0819fa64fdb09596b4fbc1182fd871f7/cadquery_ocp_novtk-7.9.3.1.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:235c66c8e0a793ae329a4c22e2f63b80a3a706494b431ec4f9608f69ee7c9664", size = 67561530, upload-time = "2026-05-28T13:07:30.03Z" }, + { url = "https://files.pythonhosted.org/packages/11/47/978175357b3c377e1ad91feaf8d68d254760e77fa3ffc565c2fc2df84681/cadquery_ocp_novtk-7.9.3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:db50a67cb2a0976200526aa12d2ca5ca331d2e8b27f9c7f2ba688aa8d624af23", size = 46102878, upload-time = "2026-05-28T13:07:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/85/ea/54168cbd1f49de64634e5bca2bdf759095d7093f0e985eb28655f530fa90/cadquery_ocp_novtk-7.9.3.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dc157f6ff3ecf28f1b1b43daa908b79a2f934b2d26390e40a04d9ed38387527", size = 61745628, upload-time = "2026-05-28T13:07:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0c/fd9ca136b6199a6cf07a944898d03f9517fc75351920265788bb632942ec/cadquery_ocp_novtk-7.9.3.1.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:7eb101093a84bbbbc40d93a4f8726ae3b14f85b49d0087212efe3afdea69c852", size = 66398032, upload-time = "2026-05-28T13:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/554474defd6f051f71d5d93a2cebecfce863f833ce997a1484b080b1f77c/cadquery_ocp_novtk-7.9.3.1.1-cp311-cp311-manylinux_2_31_aarch64.whl", hash = "sha256:0b566f9e00f0e4255996b2bcb18b580dc758f451fd2b7546542fae08261db535", size = 62178457, upload-time = "2026-05-28T13:07:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f5/1d1b07c6b4436fd0f889143086899294710e8ce7f97864ced455477773de/cadquery_ocp_novtk-7.9.3.1.1-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:860649d3d2c2d43eb2068ff604bce198dbebf402eedae496829d12de4107a2f5", size = 67631036, upload-time = "2026-05-28T13:07:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/13561649828a657499b1121a2dccb67e9c1861769ab5515a84c0db4598cc/cadquery_ocp_novtk-7.9.3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:46ca650c78372a05d91db79c66a4f28d5cd7ec674f5347ddbf94e927ef6e4090", size = 46150439, upload-time = "2026-05-28T13:07:51.817Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/1f86553953ba4a511e2b324b6101fe42fd323d7feef6089fb3137fc36dc4/cadquery_ocp_novtk-7.9.3.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50931ba2d20aa57d5e3fe2bac790093fc65c8257f7899ec5071b9e8d13dbf4bd", size = 62300338, upload-time = "2026-05-28T13:07:55.429Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8a/48a995811dbbf08e5b32337ff4e5f1bf3213d692b7ab43c47fa107346ce3/cadquery_ocp_novtk-7.9.3.1.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:17f707744671367a20271fc10826d1ff13ebb835345288c9377d10cd7768eeca", size = 67131956, upload-time = "2026-05-28T13:07:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/36/26/32dc553e7f177976c16c085a20b6e67c782bc9ea7dcd130abdba28ee4c55/cadquery_ocp_novtk-7.9.3.1.1-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:847ce78339ce2860f25a3d3109852c9c7746f4450293f359f69ecaa57967752f", size = 61980056, upload-time = "2026-05-28T13:08:02.522Z" }, + { url = "https://files.pythonhosted.org/packages/4d/32/8907aae7ee5c5c968a50a1d382a01854ca3abdc830a486e79dda64a66746/cadquery_ocp_novtk-7.9.3.1.1-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:60497cf42419dd2000d323ab772937f61539f2f0a5d3ef1b288611b13254c587", size = 67452403, upload-time = "2026-05-28T13:08:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/b5/03/3cc50b5a68dbe16456eccbd2c5128bcef62783c9a809d36cf11f4d1120ed/cadquery_ocp_novtk-7.9.3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5d22339cdaac64c396f0658de8728915acfac9b868406f0078e52f50a3c25c65", size = 46364919, upload-time = "2026-05-28T13:08:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/f7/80/dc46dac2ebb6ae6bcce27bc7f717438b99897199be25323cda559a76e0a1/cadquery_ocp_novtk-7.9.3.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2287812f213af7020f8bbc69b07d6039e529f5e0e154c0192221aff3fdac6676", size = 62295929, upload-time = "2026-05-28T13:08:15.333Z" }, + { url = "https://files.pythonhosted.org/packages/97/55/93f76eb2a1874b2d51c98e6cad1b55ae7562fc9b6eb36365e3b0597d25c4/cadquery_ocp_novtk-7.9.3.1.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:e2969d2129bf21067f44953117cc18f1426f5aacaed8bcc074228eb50f3179b2", size = 67126917, upload-time = "2026-05-28T13:08:18.75Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bc/3eb58a3b7513309d665e0a65c00da93fc825dc930744cd8ab23626cd2e1f/cadquery_ocp_novtk-7.9.3.1.1-cp313-cp313-manylinux_2_31_aarch64.whl", hash = "sha256:d8f68b05c2917fb0f5ea569ed88c697d47cc57a7b293929d863b52c99ace9312", size = 61978120, upload-time = "2026-05-28T13:08:22.182Z" }, + { url = "https://files.pythonhosted.org/packages/f3/31/82baf17406c0a13f2eb98c1d46d09a640795fc7d6b373a69bc5f44344db3/cadquery_ocp_novtk-7.9.3.1.1-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:ffcd04d4efa087d3aa942360020265a1ac0e12407bd7bcfeb37e70b4d1ee72df", size = 67466562, upload-time = "2026-05-28T13:08:25.751Z" }, + { url = "https://files.pythonhosted.org/packages/57/48/4595c22b0ae1b4759f209a13d21dadd229db6c374d9b0a95afa7ed0c4714/cadquery_ocp_novtk-7.9.3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:3390be6d8199a50b01ae0137a60f56d0cfecd8753dbee08532bbbeb8b7169682", size = 46364506, upload-time = "2026-05-28T13:08:29.174Z" }, ] -[package.optional-dependencies] -vtk = [ - { name = "vtk" }, +[[package]] +name = "cadquery-ocp-proxy" +version = "7.9.3.1.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c0/04e9363a99fee892de2776820e3dcf04f8825b6edc9580efe3416c9465a7/cadquery_ocp_proxy-7.9.3.1.1-py3-none-any.whl", hash = "sha256:ca4164ec4b54956d9fc3e68c67d555b5486cb963c2f71e18df005ba16b921c91", size = 3322, upload-time = "2026-05-28T13:08:49.092Z" }, ] [[package]] @@ -3012,13 +3032,12 @@ editor = [ { 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" }, -] gmsh = [ { name = "gmsh" }, ] +verify = [ + { name = "cadquery-ocp-novtk" }, +] whucad = [ { name = "h5py" }, ] @@ -3044,10 +3063,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0" }, - { name = "ccx2paraview", extras = ["vtk"], marker = "extra == 'fem'", specifier = ">=3.2,<4" }, + { name = "cadquery-ocp-novtk", marker = "extra == 'verify'", specifier = ">=7.9.3.1,<8" }, { name = "cryptography", specifier = ">=42.0.0" }, { name = "ezdxf", specifier = ">=1.4.4" }, - { name = "gmsh", marker = "extra == 'fem'", specifier = ">=4.15,<5" }, { name = "gmsh", marker = "extra == 'gmsh'", specifier = ">=4.15,<5" }, { name = "h5py", marker = "extra == 'whucad'", specifier = ">=3.8" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.0" }, @@ -3070,7 +3088,7 @@ requires-dist = [ { name = "uvicorn", marker = "extra == 'editor'", specifier = ">=0.54.0" }, { name = "websockets", marker = "extra == 'editor'", specifier = ">=16.1.1" }, ] -provides-extras = ["dev", "drawing", "editor", "inspect", "whucad", "gmsh", "fem"] +provides-extras = ["dev", "drawing", "editor", "inspect", "verify", "whucad", "gmsh"] [package.metadata.requires-dev] dev = [ @@ -3368,36 +3386,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, ] -[[package]] -name = "vtk" -version = "9.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/71/0cc26142f4f5276944c17a8c839b3e06894617627750686e12dab708ade0/vtk-9.5.2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9ca87122352cf3c8748fee73c48930efa46fe1a868149a1f760bc17e8fae27ba", size = 86865294, upload-time = "2025-09-18T00:56:08.013Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/2b891f70cb2144a9fb9fe30803d5dfdbae4b839bc27318e1e5e93eba181c/vtk-9.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6da02d69dcf2d42472ec8c227e6a8406cedea53d3928af97f8d4e776ff89c95f", size = 80550341, upload-time = "2025-09-18T00:56:14.773Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6a/79ef2fc5fb460d104b48b46215138e54097ea1eb08f2f7ae874372c0890e/vtk-9.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0ba9cc4b5cd463a1984dfac6d0a9eeef888b273208739f8ebc46d392ddabb93", size = 112239919, upload-time = "2025-09-18T00:56:19.058Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b4/8c89d628ec4674fe53d85d8249c1d74c5678554b29f4333ad4f06d3388b5/vtk-9.5.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9254f864ebef3d69666a1feedf09cad129e4c91f85ca804c38cf8addedb2748", size = 103718843, upload-time = "2025-09-18T00:56:23.684Z" }, - { url = "https://files.pythonhosted.org/packages/10/64/6a6b8100c8a9e7aac92e38f6625458737f3fc8060b280fa67c30b70253e7/vtk-9.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:7c56dbd02e5b4ec0422886bf9e26059ad2d4622857dbfb90d9ed254104fd9d6c", size = 63915487, upload-time = "2025-09-18T00:56:29.245Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8e/c8a4dee522ad0436c846f0f62444a2699cc0d72b2554aa09bcfcf2c01f29/vtk-9.5.2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:afcbc6dc122ebba877793940fda8fd2cbe14e1dae590e6872ea74894abdab9be", size = 86865360, upload-time = "2025-09-18T00:56:33.18Z" }, - { url = "https://files.pythonhosted.org/packages/39/d5/ec52c2cea957221d5e41ebe1768c78d79c714f1e0e635cc4a545cab69d31/vtk-9.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:005877a568b96cf00ceb5bec268cf102db756bed509cb240fa40ada414a24bf0", size = 80550040, upload-time = "2025-09-18T00:56:37.337Z" }, - { url = "https://files.pythonhosted.org/packages/b9/00/73a2e3548eae8c122569ae40e947166620cf192ec9a46c6be94e04597dc3/vtk-9.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2e2fe2535483adb1ba8cc83a0dc296faaffa2505808a3b04f697084f656e5f84", size = 112239648, upload-time = "2025-09-18T00:56:42.591Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fb/9a88a43be9deccdfb04228a059aa6e7e0416b96223436cf040bc627b5a1d/vtk-9.5.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0248aab2ee51a69fadcdcf74697a045e2d525009a35296100eed2211f0cca2bb", size = 103718432, upload-time = "2025-09-18T00:56:49.733Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f0/2f499af38d5b30f0ee80f644ef16be3be739f475d19fbdf518ab622dfb88/vtk-9.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:f78674fd265022499ea6b7f03d7f11a861e89e1df043592a82e4f5235c537ef5", size = 63914667, upload-time = "2025-09-18T00:56:53.795Z" }, - { url = "https://files.pythonhosted.org/packages/23/43/58f4d2d127fc8e8aa26e4793cf80e901b395de44fb6ac69acd7c7b8856ce/vtk-9.5.2-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:cf5dbc93b6806b08799204430a4fc4bea74290c1c101fa64f1a4703144087fa3", size = 87057834, upload-time = "2025-09-18T00:56:57.737Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1a/a5d5ef2a5a55512158cad83524bab7a4a451f917aee342f117144d53c6eb/vtk-9.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cce212b911d13fb0ca36d339f658c9db1ff27a5a730cdddd5d0c6b2ec24c15b1", size = 80607226, upload-time = "2025-09-18T00:57:01.782Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6c/e5544c77b568bfbcc3592fb5e23a5dee8e84df9ee316230509362fb6d432/vtk-9.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:454711c51038824ddc75f955e1064c4e214b452c2e67083f01a8b43fc0ed62cb", size = 112292112, upload-time = "2025-09-18T00:57:07.554Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/55f7832950cde2d3e5e70d3640e26f3f6c97640769ed5079b4db91189e38/vtk-9.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9fce9688f0dede00dc6f3b046037c5fa8378479fa8303a353fd69afae4078d9a", size = 103797602, upload-time = "2025-09-18T00:57:17.783Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/a292e122cb038c1c2bbc44f3e0588f2ece2bcd44b889631a5c8d82a5f8bd/vtk-9.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5357bccdf8629373195cab871e45c50383d052d316192aa48f45bd9f87bafccb", size = 64002205, upload-time = "2025-09-18T00:57:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e0/c1d4baa2d9c32fbf7d36bfa49347168b24136232b6536de433e094482e55/vtk-9.5.2-cp313-cp313-macosx_10_10_x86_64.whl", hash = "sha256:1eae5016620a5fd78f4918256ea65dbe100a7c3ce68f763b64523f06aaaeafbc", size = 87078130, upload-time = "2025-09-18T00:57:31.183Z" }, - { url = "https://files.pythonhosted.org/packages/95/e9/02af96275fd3c4cdf0d55964427c5f70c4a2106c1b6c357726d62e3e6da0/vtk-9.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29ad766e308dcaa23b36261180cd9960215f48815b31c7ac2aa52edc88e21ef7", size = 80618932, upload-time = "2025-09-18T00:57:38.197Z" }, - { url = "https://files.pythonhosted.org/packages/20/d1/c8c4c9b4ff2044fadf4f0d0230a933cc1962e7ba8bca6013737e5773c5c2/vtk-9.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11cf870c05539e9f82f4a5adf450384e0be4ee6cc80274f9502715a4139e2777", size = 112292475, upload-time = "2025-09-18T00:57:46.196Z" }, - { url = "https://files.pythonhosted.org/packages/87/6d/b0d404822329d3b7eeccd66cf61480cbb66bf1bf6bd2ef8eabedcec8428f/vtk-9.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3c4b658d61815cb87177f4e94281396c9be5a28798464a2c6fa0897b1bba282f", size = 103798352, upload-time = "2025-09-18T00:57:52.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/8f/400d4e4270bd5da3a19ce465aa288c03435e1f38b0344ac2540615ec5b0b/vtk-9.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:974783b8865e2ddc2818d3090705b6bc6bf8ae40346d67f9a43485fabcfb3a99", size = 64000682, upload-time = "2025-09-18T00:57:57.926Z" }, -] - [[package]] name = "wcwidth" version = "0.2.13"