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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and

## Unreleased

## 2.2.6.post1 — 2026-10-21

### Fixed

- Editor: the parameter panel reads `scad.var(name="j1", default=0.0, ...)` — the keyword
form — as well as `scad.var("j1", 0.0, ...)`. Before, only the second positional argument
was taken as the default, so keyword declarations never showed up as parameters.
- Add-ons: `[compat] sca` ranges and the add-on commands accept a post-release version such as
`2.2.6.post1` (it sorts after `2.2.6` and before `2.2.7`); the parser rejected it.

### Changed

Cache and runtime speed, measured on the desktop six-axis arm (cold 80.5 s to 34.6 s, no
change 23.6 s to 4.0 s, one joint edited 18.2 s to 3.0 s, cache 1.7 GB to 96 MB):

- Restoring a cached shape is cheaper: adding a child to a shape no longer refreshes its
whole subtree's tag cache unless a tag can pass down, and a restore finds entities by
position and refreshes each tag cache once. A restored shape is then built when something
first uses it, so a cached value that no running cell reads is never built; pickling it
again hands back what it was restored from.
- The cell cache stores less and compresses it: no values for a cell's private (`_name`)
variables, geometry bytes and cache entries zlib-compressed (level 1). Entries written
before are still read.
- `scad.var(...)` values and the expressions made from them are keyed by content, so a cell
that reads one parameter is no longer redone because another one, defined in the same cell,
changed (marimo keyed any object it cannot hash by the code of the cell that made it).
- A cell that does `from lib.dims import A` depends on the definition of `A` (and what it
reads, across local modules), not on the whole file. A module the analysis cannot prove
(calls or loops at top level, star imports, a name defined twice, a plain
`import lib.dims`) still depends on the whole file.
- A cell that runs a notebook with `use("child.py")` (a literal path) is keyed by what that
child is (its text, the modules it imports, the notebooks it uses in turn) instead of by
everything a run recorded: editing one child redoes the cell that uses it and not those
that use its siblings, and the editor kernel caches such cells too (it ran them live
before). A path computed at run time still depends on what a run recorded.
- The `use` calls a cell is about to make (literal path; arguments made of values it reads,
`float(...)`, `notebook_id()`) are started together in worker processes, which fill the
notebooks' caches; the cell then finds them built. OpenCASCADE keeps the interpreter lock,
so these are processes, not threads. `SCA_JOBS` sets how many (default: the smaller of 4
and the cores; 1 turns it off). Workers are only started when the builds they take off the
cell's hands are worth the seconds a worker takes to start (a notebook remembers how long
it last took to build), and a notebook that has not changed since it last ran is not
started at all. A notebook's state directory is locked between processes while it runs.
- Editor: a slider drag is sent when it pauses (250 ms) or ends, not step by step; clicks and
number-box commits still go at once.

## 2.2.6 — 2026-10-19

### Added
Expand Down
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,20 @@ the full crank cycle sweeps clean in the same sim ·

---

## Update Notes (2.2.6)
## Update Notes (2.2.6.post1)

> Validate generated definitions, assembly constraints, and manufacturing
> geometry before production use.

- **Switch notebooks without leaving the CLI.** `sca cell open NOTEBOOK` moves the editor
and waits for its kernel; the write commands take `--open` to switch and write in one
step, and a refused write names the way out.
- **`sca viewport` answers what the person sees.** The image's path, age and size, copied
out with `--out`, instead of reading `.sca-editor/viewport.png` by hand.
- **Cache entries are signed and verified again.** `cryptography` ships as a dependency,
so marimo's cache verification no longer silently degrades to off.
- **Keyword `scad.var` is a parameter again.** `scad.var(name="j1", default=0.0)` is read
by the editor's parameter panel like the positional form; before, it was skipped.
- **A cached run costs much less.** On the desktop arm: cold 80 s to 35 s, nothing changed
24 s to 4 s, one joint edited 18 s to 3 s, cache 1.7 GB to 96 MB. Restored shapes are built
on first use, parameters and imported names are keyed by value, and the `use` calls of a
cell run in worker processes (`SCA_JOBS`, 1 turns it off).
- **Slider drags are sent when they settle**, not step by step.

Full notes for this version: [`docs/updates/2.2.6.md`](docs/updates/2.2.6.md).
Full notes for this version: [`docs/updates/2.2.6.post1.md`](docs/updates/2.2.6.post1.md).
Earlier versions: the [changelog](CHANGELOG.md) and [`docs/updates/`](docs/updates/).

---
Expand All @@ -183,7 +183,7 @@ in a compact public API for creating solids, applying features, tagging semantic
intent, querying topology, exporting manufacturing files, and translating recorded
models into FreeCAD workflows.

Current release: `simplecadapi==2.2.6`.
Current release: `simplecadapi==2.2.6.post1`.

## What It Provides

Expand Down Expand Up @@ -698,6 +698,7 @@ and the [STEP reconstruction workflow](docs/skill/references/workflows/step-reco

## Documentation

- 2.2.6.post1 update notes: [`docs/updates/2.2.6.post1.md`](docs/updates/2.2.6.post1.md)
- 2.2.6 update notes: [`docs/updates/2.2.6.md`](docs/updates/2.2.6.md)
- 2.2.5 update notes: [`docs/updates/2.2.5.md`](docs/updates/2.2.5.md)
- 2.2.4 update notes: [`docs/updates/2.2.4.md`](docs/updates/2.2.4.md)
Expand Down
18 changes: 10 additions & 8 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,18 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真

---

## 更新日志(2.2.6)
## 更新日志(2.2.6.post1)

> 投入生产前请验证生成的定义、装配约束和制造几何。

- **不离开 CLI 就能切换 notebook。** `sca cell open NOTEBOOK` 切换编辑器并等 kernel 就绪;
写命令加 `--open` 切换 + 写入一步完成,被拒绝的写入会直接指路。
- **`sca viewport` 直接回答"用户看到什么"。** 图片的路径、age、大小,`--out` 复制出去,
不用手读 `.sca-editor/viewport.png`。
- **缓存条目重新有了签名验证。** `cryptography` 随包安装,marimo 缓存验证不再静默降级。
- **关键字写法的 `scad.var` 重新被识别为参数。** `scad.var(name="j1", default=0.0)` 现在和
位置参数写法一样出现在参数面板;之前会被跳过。
- **缓存运行便宜得多。** 桌面机械臂:冷启动 80 秒到 35 秒,什么都没改 24 秒到 4 秒,改一个关节 18 秒到 3 秒,
缓存 1.7 GB 到 96 MB。形状在第一次用到时才构建,参数和导入的名字按值定键,cell 里的 `use` 调用在 worker
进程里并行(`SCA_JOBS`,设为 1 关闭)。
- **滑块拖动在停下来时才发送**,不再逐步发送。

本版本的完整说明:[`docs/updates/2.2.6.zh-CN.md`](docs/updates/2.2.6.zh-CN.md)。
本版本的完整说明:[`docs/updates/2.2.6.post1.zh-CN.md`](docs/updates/2.2.6.post1.zh-CN.md)。
更早的版本:见[更新日志](CHANGELOG.md)和 [`docs/updates/`](docs/updates/)。

---
Expand All @@ -161,7 +162,7 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真

SimpleCADAPI 是一个基于 OCP 的 Python CAD SDK,提供清晰的函数式建模操作和可重放的模型图。它在 OpenCascade 几何内核之上提供精简的公共 API,可用于创建实体、应用特征、添加语义标签、查询拓扑、导出制造文件,以及将记录的模型转换为 FreeCAD 工作流。

当前版本:`simplecadapi==2.2.6`。
当前版本:`simplecadapi==2.2.6.post1`。

## 核心能力

Expand Down Expand Up @@ -587,6 +588,7 @@ uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py

## 文档

- 2.2.6.post1 更新说明:[`docs/updates/2.2.6.post1.zh-CN.md`](docs/updates/2.2.6.post1.zh-CN.md)
- 2.2.6 更新说明:[`docs/updates/2.2.6.zh-CN.md`](docs/updates/2.2.6.zh-CN.md)
- 2.2.5 更新说明:[`docs/updates/2.2.5.zh-CN.md`](docs/updates/2.2.5.zh-CN.md)
- 2.2.4 更新说明:[`docs/updates/2.2.4.zh-CN.md`](docs/updates/2.2.4.zh-CN.md)
Expand Down
4 changes: 2 additions & 2 deletions docs/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ description: Build, assemble, inspect, reconstruct, and export parametric CAD mo
license: Apache-2.0
metadata:
project: simplecadapi
version: 2.2.6
version: 2.2.6.post1
package-name: simplecadapi
package-version: 2.2.6
package-version: 2.2.6.post1
---

# SimpleCAD SDK Skill
Expand Down
44 changes: 44 additions & 0 deletions docs/updates/2.2.6.post1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 2.2.6.post1 Update Notes

A hotfix for the editor's parameter panel, and a round of speed work on the notebook cache
and on `use`, measured on the desktop six-axis arm.

## `scad.var(name=..., default=...)` is a parameter again

The editor found a parameter's default only as the second *positional* argument of
`scad.var`. A declaration written with keywords —

```python
J1 = scad.var(name="j1", default=0.0, comment="Joint 1")
```

— was skipped: no slider, no commit back to the file. Both forms are read now, and
committing a value rewrites the right argument in either.

## What a cached run costs

| desktop arm | before | now |
|---|---|---|
| cold run | 80.5 s | 34.6 s |
| nothing changed | 23.6 s | 4.0 s |
| one joint angle edited | 18.2 s | 3.0 s |
| a link length edited | 35.1 s | 19 s |
| cache on disk | 1.7 GB | 96 MB |

- **Restoring is cheaper.** A restored shape is built when something first uses it, so a
cached value no running cell reads costs nothing; the tag caches are rebuilt once, not
once per ancestor. The cache no longer stores a cell's private (`_name`) values and
compresses what it writes; entries from before are still read.
- **Parameters are keyed by value.** A cell that reads one `scad.var` is not redone because a
neighbour defined in the same cell changed.
- **A name, not a file.** `from lib.dims import A` depends on the definition of `A`.
- **`use` is followed one call at a time.** A cell that uses a notebook by a literal path
depends on that notebook (its text, the modules it imports, the notebooks it uses): editing
one child redoes the cell that uses it, not its siblings. The editor kernel caches such
cells too.
- **`use` calls run side by side.** The `use` calls a cell is about to make are started in
worker processes (threads would not help: OpenCASCADE keeps the interpreter lock) and the
cell finds them built. `SCA_JOBS` sets the number (default: the smaller of 4 and the
cores; `1` turns it off). Workers are started only when that saves more than they take to
start, and a notebook is locked between processes while it runs.
- **Editor sliders.** A drag is sent when it pauses (250 ms) or ends, not step by step.
34 changes: 34 additions & 0 deletions docs/updates/2.2.6.post1.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 2.2.6.post1 更新说明

编辑器参数面板的热修复,以及一轮针对 notebook 缓存和 `use` 的速度优化,数据来自桌面六轴机械臂。

## `scad.var(name=..., default=...)` 重新被识别为参数

编辑器只把 `scad.var` 的第二个**位置参数**当作默认值。用关键字写的声明——

```python
J1 = scad.var(name="j1", default=0.0, comment="Joint 1")
```

——会被跳过:没有滑块,也无法写回文件。现在两种写法都能读取,提交数值时也会改写对应的那个参数。

## 缓存运行的代价

| 桌面机械臂 | 之前 | 现在 |
|---|---|---|
| 冷启动 | 80.5 s | 34.6 s |
| 什么都没改 | 23.6 s | 4.0 s |
| 改一个关节角 | 18.2 s | 3.0 s |
| 改一个连杆长度 | 35.1 s | 19 s |
| 磁盘缓存 | 1.7 GB | 96 MB |

- **恢复更便宜。** 恢复出来的形状在第一次被用到时才构建,没有 cell 读取的缓存值不花时间;标签缓存只重建一次,
不再每个祖先一次。缓存不再存 cell 私有(`_name`)变量的值,并压缩写入的内容;旧条目照样能读。
- **参数按值定键。** 读取某个 `scad.var` 的 cell,不会因为同一个 cell 里的邻居参数变化而重跑。
- **按名字而不是按文件。** `from lib.dims import A` 只依赖 `A` 的定义。
- **`use` 按调用逐个跟踪。** 用字面量路径 `use` 一个 notebook 的 cell 依赖那个 notebook(文本、它导入的模块、
它再 `use` 的 notebook):改一个子 notebook 只重跑用它的 cell,不动它的兄弟。编辑器内核也缓存这类 cell。
- **`use` 并行。** cell 即将发出的 `use` 调用交给 worker 进程同时构建(线程没用:OpenCASCADE 不释放解释器锁),
cell 随后发现已经建好。`SCA_JOBS` 设置数量(默认 4 与核数的较小值;`1` 关闭)。只有省下的时间大于 worker
启动成本才会启动,并且 notebook 运行期间其状态目录在进程间加锁。
- **编辑器滑块。** 拖动在停顿 250 ms 或松开时才发送,不再逐步发送。
24 changes: 12 additions & 12 deletions editor-web/src/components/CellEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ParamControl, type ParamMeta, type ParamValue } from "@/components/ParamControl";
import { createSettler, SETTLE_MS } from "@/lib/settle";
import { cadTheme } from "@/lib/cm-theme";
import { sdkCompletion } from "@/lib/sdk-complete";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -205,17 +206,16 @@ export function CellEditor({
);
};

// Forward slider bursts; the server coalesces (latest-wins rebuild).
const pendingParams = useRef(new Map<string, ParamValue>());
const paramTimer = useRef<number | null>(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);
// A drag is sent when it pauses or ends, not step by step: every value the
// server gets is a rebuild.
const socketRef = useRef(socket);
socketRef.current = socket;
const settler = useRef(
createSettler<ParamValue>(SETTLE_MS, (values) => socketRef.current?.send({ type: "param.set", values })),
);
const forwardParam = (id: string, value: ParamValue, now = false) => {
settler.current.push(id, value);
if (now) settler.current.flush();
};

// Disarm delete when clicking anywhere outside the armed button.
Expand Down Expand Up @@ -364,7 +364,7 @@ function CellBlock({
live: boolean;
armed: boolean;
socket: SocketLike;
forwardParam: (id: string, value: ParamValue) => void;
forwardParam: (id: string, value: ParamValue, now?: boolean) => void;
onToggle: () => void;
onRun: () => void;
onInterrupt: () => void;
Expand Down
27 changes: 17 additions & 10 deletions editor-web/src/components/ModuleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 { createSettler, SETTLE_MS } from "@/lib/settle";
import { sdkCompletion } from "@/lib/sdk-complete";

/** A local module of the workspace (shared dimensions, helpers): what the
Expand Down Expand Up @@ -41,8 +42,17 @@ export function ModuleView({
const [draft, setDraft] = useState(module.code);
const dirty = useRef(false);
const saveTimer = useRef<number | null>(null);
const paramTimer = useRef<number | null>(null);
const pending = useRef(new Map<string, ParamValue>());
const socketRef = useRef(socket);
socketRef.current = socket;
const pathRef = useRef(module.path);
pathRef.current = module.path;
const settler = useRef(
createSettler<ParamValue>(SETTLE_MS, (values) =>
socketRef.current?.send({ type: "module.param", path: pathRef.current, values }),
),
);
// values of the module that was left are not the next one's
useEffect(() => () => settler.current.cancel(), [module.path]);

// A different module, an external edit or a parameter edit replaces the
// text; our own autosave echo must not clobber what is being typed.
Expand All @@ -68,14 +78,11 @@ export function ModuleView({
editRef.current = edit;
const handleChange = useCallback((value: string) => editRef.current(value), []);

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);
// A drag is sent when it pauses or ends, not step by step: every value the
// server gets is a rebuild.
const forward = (id: string, value: ParamValue, now = false) => {
settler.current.push(id, value);
if (now) settler.current.flush();
};

const groups = new Map<string, ParamMeta[]>();
Expand Down
26 changes: 21 additions & 5 deletions editor-web/src/components/ParamControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ export function ParamControl({
forwardParam,
}: {
param: ParamMeta;
forwardParam: (id: string, value: ParamValue) => void;
/** `now`: send at once (a click, a release) instead of when the changes settle. */
forwardParam: (id: string, value: ParamValue, now?: boolean) => void;
}) {
// A drag shows its own value: the server only hears of it when it settles,
// so the value it echoes back cannot move the thumb meanwhile.
const [drag, setDrag] = useState<number | null>(null);
const dragging = useRef(false);
useEffect(() => {
if (!dragging.current) setDrag(null);
}, [param.value]);
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";
Expand All @@ -42,7 +50,7 @@ export function ParamControl({
type="button"
className="flex items-center justify-between gap-2 text-left text-[11px]"
title={title}
onClick={() => forwardParam(param.id, !on)}
onClick={() => forwardParam(param.id, !on, true)}
>
<span className={ink}>{label}</span>
<span className={cn("border-2 px-1.5 text-[10px] font-bold uppercase", on ? "border-primary bg-primary text-primary-foreground" : "border-input text-muted-foreground")}>
Expand All @@ -51,7 +59,7 @@ export function ParamControl({
</button>
);
}
const value = Number(param.value);
const value = drag ?? Number(param.value);
return (
<div title={title}>
<div className="mb-1 flex items-baseline justify-between gap-2 text-[11px]">
Expand All @@ -63,7 +71,7 @@ export function ParamControl({
min={param.min}
max={param.max}
step={param.step ?? (param.kind === "int" ? 1 : null)}
onCommit={(next) => forwardParam(param.id, next)}
onCommit={(next) => forwardParam(param.id, next, true)}
/>
) : (
<span>{value}</span>
Expand All @@ -78,7 +86,15 @@ export function ParamControl({
max={param.max}
modified={param.modified}
step={param.step ?? (param.kind === "int" ? 1 : (param.max - param.min) / 200)}
onValueChange={([next]) => forwardParam(param.id, next)}
onValueChange={([next]) => {
dragging.current = true;
setDrag(next);
forwardParam(param.id, next);
}}
onValueCommit={([next]) => {
dragging.current = false;
forwardParam(param.id, next, true);
}}
/>
)}
</div>
Expand Down
Loading
Loading