From 5aa9f53468e6e32830c71d89be691dbbe48487d3 Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Tue, 13 Oct 2026 23:29:13 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(editor):=20code=20completion=20from=20?= =?UTF-8?q?the=20SDK=20=E2=80=94=20scad.=20lists=20functions,=20(=20writes?= =?UTF-8?q?=20the=20required=20name=3D=20parameters,=20Tab=20moves=20betwe?= =?UTF-8?q?en=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/sdk: the running package's public API (names, parameters, which are required, summaries) from inspect, for scad, scad.ql and scad.std. - editor-web/src/lib/sdk-complete.ts: member completion (scad., scad.ql., ql., Q.), snippet on accepting a function or typing ( after a known name, name= completion inside calls (required first, above Python's builtins), Tab accepts a suggestion and then moves between parameters - the editor wrapper rebuilt its configuration on every render (new onChange / basicSetup each time), which drops a snippet in progress; both are stable now - popup styled to the editor's themes Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 + editor-web/package-lock.json | 2 + editor-web/package.json | 2 + editor-web/src/components/CellEditor.tsx | 15 +- editor-web/src/components/ModuleView.tsx | 16 +- editor-web/src/lib/cm-theme.ts | 19 +++ editor-web/src/lib/sdk-complete.ts | 208 +++++++++++++++++++++++ src/simplecadapi/editor/apiindex.py | 120 +++++++++++++ src/simplecadapi/editor/server.py | 7 + test/test_editor.py | 58 +++++++ 10 files changed, 444 insertions(+), 8 deletions(-) create mode 100644 editor-web/src/lib/sdk-complete.ts create mode 100644 src/simplecadapi/editor/apiindex.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1604527..63bc0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ below was added since 2.1.4, the notebook runtime of 2.2.0b1 included. ### Added +- Editor: code completion from the SDK itself. `scad.` (and `scad.ql.`, `scad.std.gear.`, `ql.`) + lists the real functions with their signatures and docstrings; accepting one, or typing `(` + after a known name, writes the call with every required parameter as `name=` and Tab moves + between them; inside a call the parameters not given yet complete as `name=` (required first). + The names come from the running package (`GET /api/sdk`), so they cannot drift from the code. - Modeling: `scale`, affine `transform`, signed `offset`, `draft`, tapered and until-limited extrusions and multisection sweeps are recorded features, and boolean results can keep every output of a set operation; graph nodes keep diff --git a/editor-web/package-lock.json b/editor-web/package-lock.json index 378d381..64fa773 100644 --- a/editor-web/package-lock.json +++ b/editor-web/package-lock.json @@ -8,8 +8,10 @@ "name": "simplecad-editor-web", "version": "0.0.0", "dependencies": { + "@codemirror/autocomplete": "^6.20.3", "@codemirror/lang-python": "^6.2.1", "@codemirror/language": "^6.13.1", + "@codemirror/state": "^6.7.6", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.43.14", "@lezer/highlight": "^1.2.5", diff --git a/editor-web/package.json b/editor-web/package.json index ab30b07..2b2afbd 100644 --- a/editor-web/package.json +++ b/editor-web/package.json @@ -11,8 +11,10 @@ "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { + "@codemirror/autocomplete": "^6.20.3", "@codemirror/lang-python": "^6.2.1", "@codemirror/language": "^6.13.1", + "@codemirror/state": "^6.7.6", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.43.14", "@lezer/highlight": "^1.2.5", diff --git a/editor-web/src/components/CellEditor.tsx b/editor-web/src/components/CellEditor.tsx index f1581cb..7b87b23 100644 --- a/editor-web/src/components/CellEditor.tsx +++ b/editor-web/src/components/CellEditor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import CodeMirror from "@uiw/react-codemirror"; import { python } from "@codemirror/lang-python"; import { EditorView } from "@codemirror/view"; @@ -20,6 +20,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 { sdkCompletion } from "@/lib/sdk-complete"; import { cn } from "@/lib/utils"; export type NotebookCell = { @@ -49,7 +50,9 @@ export type { ParamMeta, ParamValue }; type SocketLike = { send: (message: Record) => void } | null; const AUTOSAVE_MS = 700; -const EXTENSIONS = [python(), EditorView.lineWrapping]; +const EXTENSIONS = [python(), EditorView.lineWrapping, sdkCompletion]; +/** a constant: a new object each render makes the editor wrapper rebuild its configuration, which drops a snippet in progress */ +const BASIC_SETUP = { lineNumbers: false, foldGutter: false, highlightActiveLine: false }; function loadCollapsed(notebook: string | null): Set { try { @@ -317,6 +320,10 @@ function CellBlock({ onDelete: () => void; onEdit: (value: string) => void; }) { + // the editor takes this once: a new function each render would rebuild its configuration + const onEditRef = useRef(onEdit); + onEditRef.current = onEdit; + const handleChange = useCallback((value: string) => onEditRef.current(value), []); 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; @@ -411,8 +418,8 @@ function CellBlock({ extensions={EXTENSIONS} theme={cadTheme} editable={!cell.readonly} - basicSetup={{ lineNumbers: false, foldGutter: false, highlightActiveLine: false }} - onChange={onEdit} + basicSetup={BASIC_SETUP} + onChange={handleChange} onKeyDown={(event) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); diff --git a/editor-web/src/components/ModuleView.tsx b/editor-web/src/components/ModuleView.tsx index 4dc7c04..2f12221 100644 --- a/editor-web/src/components/ModuleView.tsx +++ b/editor-web/src/components/ModuleView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import CodeMirror from "@uiw/react-codemirror"; import { python } from "@codemirror/lang-python"; import { EditorView } from "@codemirror/view"; @@ -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 { sdkCompletion } from "@/lib/sdk-complete"; /** A local module of the workspace (shared dimensions, helpers): what the * server sends as the `module` envelope. */ @@ -21,7 +22,9 @@ export type ModuleState = { }; const AUTOSAVE_MS = 700; -const EXTENSIONS = [python(), EditorView.lineWrapping]; +const EXTENSIONS = [python(), EditorView.lineWrapping, sdkCompletion]; +/** a constant: a new object each render makes the editor wrapper rebuild its configuration, which drops a snippet in progress */ +const BASIC_SETUP = { lineNumbers: true, foldGutter: false, highlightActiveLine: false }; /** 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 @@ -60,6 +63,11 @@ export function ModuleView({ }, AUTOSAVE_MS); }; + // the editor takes this once: a new function each render would rebuild its configuration + const editRef = useRef(edit); + 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; @@ -124,8 +132,8 @@ export function ModuleView({ height="auto" extensions={EXTENSIONS} theme={cadTheme} - basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: false }} - onChange={edit} + basicSetup={BASIC_SETUP} + onChange={handleChange} /> diff --git a/editor-web/src/lib/cm-theme.ts b/editor-web/src/lib/cm-theme.ts index 8ca5599..9df73ae 100644 --- a/editor-web/src/lib/cm-theme.ts +++ b/editor-web/src/lib/cm-theme.ts @@ -22,10 +22,29 @@ const theme = EditorView.theme( color: "var(--popover-foreground)", border: "2px solid var(--border)", }, + ".cm-tooltip-autocomplete": { fontSize: "12px" }, + ".cm-tooltip-autocomplete > ul": { fontFamily: "var(--mono)", maxHeight: "16em" }, + ".cm-tooltip-autocomplete ul li": { padding: "2px 8px" }, ".cm-tooltip-autocomplete ul li[aria-selected]": { backgroundColor: "var(--primary)", color: "var(--primary-foreground)", }, + ".cm-completionDetail": { color: "var(--muted-foreground)", fontStyle: "normal", marginLeft: "1em" }, + ".cm-tooltip-autocomplete ul li[aria-selected] .cm-completionDetail": { color: "inherit", opacity: "0.75" }, + ".cm-completionMatchedText": { textDecoration: "none", color: "var(--focus)", fontWeight: "700" }, + ".cm-tooltip-autocomplete ul li[aria-selected] .cm-completionMatchedText": { color: "inherit" }, + ".cm-completionIcon": { opacity: "0.6" }, + ".cm-completionInfo": { + backgroundColor: "var(--popover)", + color: "var(--popover-foreground)", + border: "2px solid var(--border)", + padding: "6px 8px", + maxWidth: "420px", + }, + ".sca-api-signature": { color: "var(--name)", wordBreak: "break-word" }, + ".sca-api-doc": { marginTop: "4px", color: "var(--muted-foreground)" }, + // the parameter to fill in next + ".cm-snippetField": { backgroundColor: "var(--code-selection)", outline: "1px solid var(--focus)" }, }, { dark: false }, ); diff --git a/editor-web/src/lib/sdk-complete.ts b/editor-web/src/lib/sdk-complete.ts new file mode 100644 index 0000000..756b8f8 --- /dev/null +++ b/editor-web/src/lib/sdk-complete.ts @@ -0,0 +1,208 @@ +import { acceptCompletion, snippet, type Completion, type CompletionContext, type CompletionResult } from "@codemirror/autocomplete"; +import { pythonLanguage } from "@codemirror/lang-python"; +import { Prec, type Extension } from "@codemirror/state"; +import { EditorView, keymap } from "@codemirror/view"; + +/** + * Code completion from the SDK itself. The server describes the running package (GET /api/sdk: + * every public function and class with its parameters); from that: + * + * - `scad.` lists the functions (also `scad.ql.`, `scad.std.gear.`, and `ql.` / `Q.`); + * - accepting one, or typing `(` after a known name, writes the call with every required + * parameter as `name=` and moves between them with Tab; + * - inside a call, the parameters not given yet complete as `name=` (required ones first). + */ + +interface Param { + n: string; + r: boolean; + /** "p" positional only, "a" the *args parameter, "w" the **kwargs one, "" an ordinary one */ + k: "p" | "a" | "w" | ""; + d?: string; +} +interface Entry { + n: string; + k: "f" | "c"; + p: Param[]; + d: string; +} +interface SdkIndex { + namespaces: Record; + submodules: Record; +} + +let index: SdkIndex | null = null; +let loading: Promise | null = null; +const load = (): Promise => + (loading ??= fetch("/api/sdk") + .then((response) => (response.ok ? (response.json() as Promise) : null)) + .then((data) => (index = data)) + .catch(() => null)); + +/** how a name written in a cell reaches a namespace of the index */ +const HEADS: Record = { scad: "scad", ql: "scad.ql", Q: "scad.ql" }; +const namespaceOf = (path: string): string | null => { + const [head, ...rest] = path.split("."); + const base = HEADS[head]; + return base ? [base, ...rest].join(".") : null; +}; + +/** `scad.ql.faces` before the cursor: the namespace path and the name being typed */ +const MEMBER = /(? { + const space = namespaceOf(path); + return (space && index?.namespaces[space]?.find((entry) => entry.n === name)) || null; +}; + +const escapeSnippet = (text: string) => text.replace(/[\\${}]/g, "\\$&"); + +/** `(width=${1}, height=${2})`: every parameter that has to be written, in order */ +export const callTemplate = (entry: Entry): string => { + let slot = 0; + const args = entry.p + .filter((param) => param.k !== "w" && (param.r || param.k === "a")) + .map((param) => { + slot += 1; + // positional ones cannot be named: a hint the person overwrites + return param.k === "p" || param.k === "a" ? `\${${slot}:${escapeSnippet(param.n)}}` : `${param.n}=\${${slot}}`; + }); + return `(${args.length ? args.join(", ") : "${}"})`; +}; + +const signature = (entry: Entry): string => { + const required = entry.p.filter((param) => param.r || param.k === "a").map((param) => (param.k === "a" ? `*${param.n}` : param.n)); + const more = entry.p.some((param) => !param.r && param.k !== "a" && param.k !== "w"); + return `(${required.join(", ")}${more ? (required.length ? ", …" : "…") : ""})`; +}; + +const info = (entry: Entry) => () => { + const root = document.createElement("div"); + const call = document.createElement("div"); + call.className = "sca-api-signature"; + call.textContent = + entry.n + + "(" + + entry.p.map((param) => (param.k === "a" ? "*" : param.k === "w" ? "**" : "") + param.n + (param.d !== undefined ? `=${param.d}` : "")).join(", ") + + ")"; + root.append(call); + if (entry.d) { + const doc = document.createElement("div"); + doc.className = "sca-api-doc"; + doc.textContent = entry.d; + root.append(doc); + } + return root; +}; + +const entryCompletion = (entry: Entry): Completion => ({ + label: entry.n, + type: entry.k === "c" ? "class" : "function", + detail: signature(entry), + info: info(entry), + apply: (view, completion, from, to) => { + // editing an existing call: only the name + if (view.state.sliceDoc(to, to + 1) === "(") { + view.dispatch({ changes: { from, to, insert: entry.n }, selection: { anchor: from + entry.n.length } }); + return; + } + snippet(`${escapeSnippet(entry.n)}${callTemplate(entry)}`)(view, completion, from, to); + }, +}); + +/** `scad.` + the typed start of a name */ +const memberSource = async (context: CompletionContext): Promise => { + const line = context.state.doc.lineAt(context.pos); + const match = MEMBER.exec(line.text.slice(0, context.pos - line.from)); + if (!match) return null; + const space = namespaceOf(match[1]); + if (!space) return null; + const data = index ?? (await load()); + if (!data || (!data.namespaces[space] && !data.submodules[space])) return null; + const options: Completion[] = [ + ...(data.namespaces[space] ?? []).map(entryCompletion), + ...(data.submodules[space] ?? []).map((name): Completion => ({ label: name, type: "namespace", detail: "module" })), + ]; + return { from: context.pos - match[2].length, options, validFor: /^\w*$/ }; +}; + +/** the innermost call the cursor is in: its function, and what is written between the parentheses */ +const enclosingCall = (text: string, pos: number): { path: string; name: string; args: string } | null => { + let depth = 0; + for (let i = pos - 1; i >= Math.max(0, pos - 4000); i--) { + const ch = text[i]; + if (ch === ")" || ch === "]" || ch === "}") depth++; + else if (ch === "(" || ch === "[" || ch === "{") { + if (depth > 0) depth--; + else if (ch !== "(") return null; + else { + const named = /(? { + let depth = 0; + let out = ""; + for (const ch of args) { + if (ch === "(" || ch === "[" || ch === "{") depth++; + else if (ch === ")" || ch === "]" || ch === "}") depth = Math.max(0, depth - 1); + else if (depth === 0) out += ch; + } + return out; +}; + +/** inside `scad.f(`: the parameters not given yet, as `name=` */ +const kwargSource = async (context: CompletionContext): Promise => { + const word = context.matchBefore(/\w*/); + if (!word || (word.from === word.to && !context.explicit)) return null; + const text = context.state.sliceDoc(0, context.pos); + const call = enclosingCall(text, context.pos); + if (!call) return null; + const data = index ?? (await load()); + if (!data) return null; + const entry = lookup(call.path, call.name); + if (!entry) return null; + const top = topLevel(call.args); + const segments = top.split(","); + // only where an argument starts: not after `name=` and not inside an expression + if (!/^\s*\w*$/.test(segments[segments.length - 1])) return null; + const given = new Set([...top.matchAll(/(?])\b([A-Za-z_]\w*)\s*=(?!=)/g)].map((m) => m[1])); + const positional = segments.slice(0, -1).filter((segment) => segment.trim() && !/(?])=(?!=)/.test(segment)).length; + const free = entry.p.filter((param) => param.k !== "a" && param.k !== "w"); + const options: Completion[] = free + .filter((param, k) => !given.has(param.n) && k >= positional && param.k !== "p") + .map((param) => ({ + label: `${param.n}=`, + type: "property", + detail: param.r ? "required" : param.d !== undefined ? `default ${param.d}` : "", + // above Python's own names (bool, ...): inside this call these are what is wanted + boost: param.r ? 99 : 50, + })); + return options.length ? { from: word.from, options, validFor: /^\w*$/ } : null; +}; + +/** `(` typed after a known function: write the call with its required parameters */ +const parenthesis = EditorView.inputHandler.of((view, from, to, text) => { + if (text !== "(" || from !== to || !index) return false; + const line = view.state.doc.lineAt(from); + const match = MEMBER.exec(line.text.slice(0, from - line.from)); + const entry = match && match[2] ? lookup(match[1], match[2]) : null; + if (!entry || view.state.sliceDoc(to, to + 1) === "(") return false; + snippet(callTemplate(entry))(view, null, from, to); + return true; +}); + +export const sdkCompletion: Extension = [ + pythonLanguage.data.of({ autocomplete: memberSource }), + pythonLanguage.data.of({ autocomplete: kwargSource }), + Prec.high(parenthesis), + // Tab takes the highlighted suggestion (else it moves to the next parameter of a call) + Prec.highest(keymap.of([{ key: "Tab", run: acceptCompletion }])), +]; + +void load(); diff --git a/src/simplecadapi/editor/apiindex.py b/src/simplecadapi/editor/apiindex.py new file mode 100644 index 0000000..d811e5d --- /dev/null +++ b/src/simplecadapi/editor/apiindex.py @@ -0,0 +1,120 @@ +"""The SDK's public API as plain data, for the editor's code completion. + +The cell editor completes ``scad.``, and a call's parameters as ``name=`` placeholders, from +this index. It is read from the live package (``inspect``), so it is always the API of the +version that is running and cannot drift from the code. Namespaces are the places a notebook +reaches names through: ``scad`` (the package), ``scad.ql`` and ``scad.std.``. +""" + +from __future__ import annotations + +import functools +import inspect +import types +from typing import Any + +import simplecadapi + +_KINDS = { + inspect.Parameter.POSITIONAL_ONLY: "p", + inspect.Parameter.POSITIONAL_OR_KEYWORD: "", + inspect.Parameter.KEYWORD_ONLY: "", + inspect.Parameter.VAR_POSITIONAL: "a", + inspect.Parameter.VAR_KEYWORD: "w", +} +_DOC_LIMIT = 240 +_DEFAULT_LIMIT = 40 + + +def _summary(obj: Any) -> str: + """The first paragraph of the docstring, on one line.""" + + doc = inspect.getdoc(obj) or "" + first = doc.strip().split("\n\n", 1)[0] + text = " ".join(first.split()) + return text if len(text) <= _DOC_LIMIT else text[: _DOC_LIMIT - 1].rstrip() + "…" + + +def _parameters(obj: Any) -> list[dict[str, Any]] | None: + try: + signature = inspect.signature(obj) + except (TypeError, ValueError): + return None + rows: list[dict[str, Any]] = [] + for parameter in signature.parameters.values(): + if parameter.name == "self" or parameter.name.startswith("_"): + continue + required = parameter.default is inspect.Parameter.empty and parameter.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ) + row: dict[str, Any] = {"n": parameter.name, "r": required, "k": _KINDS[parameter.kind]} + if parameter.default is not inspect.Parameter.empty and not str(parameter.default).startswith("<"): + text = repr(parameter.default) + row["d"] = text if len(text) <= _DEFAULT_LIMIT else text[: _DEFAULT_LIMIT - 1] + "…" + rows.append(row) + return rows + + +def _entry(name: str, obj: Any) -> dict[str, Any] | None: + if inspect.isclass(obj): + kind = "c" + elif inspect.isfunction(obj) or inspect.isbuiltin(obj): + kind = "f" + else: + return None + parameters = _parameters(obj) + if parameters is None: + return None + return {"n": name, "k": kind, "p": parameters, "d": _summary(obj)} + + +def _ours(obj: Any) -> bool: + """Defined by the SDK itself, not a typing helper or another library re-exported.""" + + module = getattr(obj, "__module__", "") or "" + return module == "simplecadapi" or module.startswith("simplecadapi.") + + +def _entries(module: types.ModuleType, names: list[str] | None = None, *, own: bool = False) -> list[dict[str, Any]]: + """Entries for the callables of *module*; with *own*, only those defined in that module (not re-exports).""" + + rows = [] + for name in sorted(names if names is not None else dir(module)): + if name.startswith("_"): + continue + obj = getattr(module, name, None) + if obj is None or isinstance(obj, types.ModuleType) or not _ours(obj): + continue + if own and not (getattr(obj, "__module__", "") or "").startswith(module.__name__): + continue + row = _entry(name, obj) + if row is not None: + rows.append(row) + return rows + + +@functools.lru_cache(maxsize=1) +def build_index() -> dict[str, Any]: + """``{"namespaces": {path: [entry]}, "submodules": {path: [name]}}``. + + An entry is ``{"n": name, "k": "f"|"c", "p": [parameter], "d": summary}``; a parameter is + ``{"n": name, "r": required, "k": "p"|"a"|"w"|"", "d"?: default}`` (``p`` positional only, + ``a`` the ``*args`` one, ``w`` the ``**kwargs`` one). + """ + + from simplecadapi import ql, std + + namespaces: dict[str, list[dict[str, Any]]] = { + "scad": _entries(simplecadapi, list(getattr(simplecadapi, "__all__", dir(simplecadapi)))), + "scad.ql": _entries(ql, own=True), + } + std_modules = sorted( + name for name in dir(std) if not name.startswith("_") and isinstance(getattr(std, name), types.ModuleType) + ) + for name in std_modules: + module = getattr(std, name) + namespaces[f"scad.std.{name}"] = _entries(module, list(getattr(module, "__all__", dir(module))), own=True) + submodules = {"scad": ["ql", "std"], "scad.std": std_modules} + return {"version": getattr(simplecadapi, "__version__", ""), "namespaces": namespaces, "submodules": submodules} diff --git a/src/simplecadapi/editor/server.py b/src/simplecadapi/editor/server.py index 35d48ce..7c5054f 100644 --- a/src/simplecadapi/editor/server.py +++ b/src/simplecadapi/editor/server.py @@ -1365,6 +1365,12 @@ async def latest_package(request: Request) -> Response: ) return JSONResponse({"error": "no package made yet"}, status_code=404) + async def sdk_index(request: Request) -> JSONResponse: + """The SDK's public API (names and parameters), for the cell editor's completion.""" + from .apiindex import build_index + + return JSONResponse(build_index(), headers={"Cache-Control": "max-age=3600"}) + 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 @@ -1484,6 +1490,7 @@ async def ws_endpoint(websocket) -> None: 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/sdk", sdk_index, 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"]), diff --git a/test/test_editor.py b/test/test_editor.py index f0021c0..d747b84 100644 --- a/test/test_editor.py +++ b/test/test_editor.py @@ -823,3 +823,61 @@ def test_a_running_cell_is_stopped_and_the_kernel_stays_usable(self) -> None: if kernel is not None: kernel.stop() server.should_exit = True + + +class ApiIndexTest(unittest.TestCase): + """The SDK's API as data, for the cell editor's completion.""" + + def setUp(self) -> None: + from simplecadapi.editor.apiindex import build_index + + self.index = build_index() + self.scad = {entry["n"]: entry for entry in self.index["namespaces"]["scad"]} + + def test_it_is_plain_json_and_small(self) -> None: + text = json.dumps(self.index, separators=(",", ":")) + self.assertLess(len(text), 400_000) + + def test_a_function_lists_its_parameters_and_which_are_required(self) -> None: + box = self.scad["make_box_rsolid"] + self.assertEqual(box["k"], "f") + required = [p["n"] for p in box["p"] if p["r"]] + self.assertEqual(required, ["width", "height", "depth"]) + optional = {p["n"]: p for p in box["p"] if not p["r"]} + self.assertIn("bottom_face_center", optional) + self.assertEqual(optional["bottom_face_center"]["d"], "(0, 0, 0)") + self.assertTrue(box["d"]) # the first line of its docstring + + def test_star_args_are_marked_and_not_required(self) -> None: + cut = self.scad["cut_rsolid"] + solids = next(p for p in cut["p"] if p["n"] == "solids") + self.assertEqual((solids["k"], solids["r"]), ("a", False)) + + def test_classes_list_their_constructor_without_private_fields(self) -> None: + part = self.scad["Part"] + self.assertEqual(part["k"], "c") + names = [p["n"] for p in part["p"]] + self.assertEqual([p["n"] for p in part["p"] if p["r"]], ["part_id", "body"]) + self.assertFalse([n for n in names if n.startswith("_")]) + + def test_namespaces_and_submodules(self) -> None: + namespaces = self.index["namespaces"] + self.assertIn("faces", {e["n"] for e in namespaces["scad.ql"]}) + self.assertIn("make_spur_gear_rsolid", {e["n"] for e in namespaces["scad.std.gear"]}) + self.assertEqual(self.index["submodules"]["scad"], ["ql", "std"]) + self.assertIn("gear", self.index["submodules"]["scad.std"]) + # nothing private, and the errors ql re-exports are not ql functions + for entries in namespaces.values(): + self.assertFalse([e["n"] for e in entries if e["n"].startswith("_")]) + self.assertNotIn("SimpleCADError", {e["n"] for e in namespaces["scad.ql"]}) + + def test_the_server_serves_it(self) -> None: + from starlette.testclient import TestClient + + from simplecadapi.editor.server import create_app + + with tempfile.TemporaryDirectory() as tmp: + app = create_app(WorkspaceState(Path(tmp))) + response = TestClient(app).get("/api/sdk") + self.assertEqual(response.status_code, 200) + self.assertIn("scad", response.json()["namespaces"]) From c04f8b55288075d419eadc6f04d0708d6b16f2bd Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Wed, 14 Oct 2026 00:24:48 +0800 Subject: [PATCH 2/4] feat(editor): create a notebook from the workspace tab A 'new' action in the notebooks header: a name (checked against the template's rule and the existing files), part or assembly (assembly starts with the first two parts of the workspace), create. An empty workspace opens on the workspace tab with the form shown. Server errors (a taken name) are shown in the form. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 3 + editor-web/src/App.tsx | 14 ++- editor-web/src/components/NotebookList.tsx | 134 ++++++++++++++++++++- editor-web/src/lib/api.ts | 11 +- 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1604527..6c2c2d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ below was added since 2.1.4, the notebook runtime of 2.2.0b1 included. ### Added +- Editor: a **new** action in the workspace tab starts a notebook from the SDK's template (a part, + or an assembly that uses the first two parts of the workspace), with the name checked as you + type. An empty workspace opens on that form, so `sca edit` on an empty directory is usable. - Modeling: `scale`, affine `transform`, signed `offset`, `draft`, tapered and until-limited extrusions and multisection sweeps are recorded features, and boolean results can keep every output of a set operation; graph nodes keep diff --git a/editor-web/src/App.tsx b/editor-web/src/App.tsx index 2d481e4..a19d286 100644 --- a/editor-web/src/App.tsx +++ b/editor-web/src/App.tsx @@ -9,7 +9,7 @@ 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 { fetchPreview, getWorkspace, newNotebook, 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"; @@ -118,12 +118,14 @@ export function App() { socket.on("hello", (envelope) => { setNotebook((envelope["notebook"] as string) ?? null); setCurrentPath((envelope["path"] as string) ?? null); + // an empty workspace: the way forward is the workspace tab's "new" + if (!envelope["notebook"]) setLeftTab("workspace"); 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)}…`); + setStatus(stage ? `${stage[0].toUpperCase()}${stage.slice(1)}…` : "No notebook yet: create one in the workspace tab"); setMarimoUrl((envelope["marimo_url"] as string) ?? null); refreshWorkspace(); }); @@ -312,6 +314,13 @@ export function App() { }); }; + const handleCreate = async (name: string, kind: "part" | "assembly") => { + setStatus(`creating ${name}…`); + await newNotebook(name, kind); + // the server opens it: show its cells + setLeftTab("notebook"); + }; + return (
socketRef.current?.send({ type: "module.open", path })} + onCreate={handleCreate} /> {moduleView && ( diff --git a/editor-web/src/components/NotebookList.tsx b/editor-web/src/components/NotebookList.tsx index 0551015..21e6d05 100644 --- a/editor-web/src/components/NotebookList.tsx +++ b/editor-web/src/components/NotebookList.tsx @@ -1,11 +1,18 @@ +import { Plus } from "lucide-react"; +import { useRef, useState } from "react"; + import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import type { ModuleRow, NotebookRow } from "@/lib/api"; +/** Names the template accepts: the notebook's id and its file name. */ +const NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/; + /** 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. */ + * that imports it. A new notebook starts from the SDK's template. */ export function NotebookList({ notebooks, current, @@ -13,6 +20,7 @@ export function NotebookList({ openModule, onOpen, onOpenModule, + onCreate, }: { notebooks: NotebookRow[]; /** Workspace-relative path of the open notebook. */ @@ -21,13 +29,27 @@ export function NotebookList({ openModule: string | null; onOpen: (path: string) => void; onOpenModule: (path: string) => void; + /** Write a new notebook and open it; rejects with the reason it could not. */ + onCreate: (name: string, kind: "part" | "assembly") => Promise; }) { - if (notebooks.length === 0 && modules.length === 0) { - return

No SimpleCAD notebooks in this workspace.

; - } + const [adding, setAdding] = useState(false); + const empty = notebooks.length === 0; return ( -

notebooks

+
+ notebooks + +
+ {(adding || empty) && ( + setAdding(false)} onCancel={() => setAdding(false)} /> + )}
    {notebooks.map((row) => (
  • @@ -82,3 +104,105 @@ export function NotebookList({ ); } + +/** The form for a new notebook: a name and what it is. */ +function NewEntry({ + notebooks, + first, + onCreate, + onDone, + onCancel, +}: { + notebooks: NotebookRow[]; + /** The workspace has no notebook yet. */ + first: boolean; + onCreate: (name: string, kind: "part" | "assembly") => Promise; + onDone: () => void; + onCancel: () => void; +}) { + const [name, setName] = useState(""); + const [kind, setKind] = useState<"part" | "assembly">("part"); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const input = useRef(null); + const parts = notebooks.filter((row) => row.kind === "part"); + const taken = (value: string) => notebooks.some((row) => row.path === `${value}.py`); + + const submit = async () => { + const value = name.trim(); + if (!NAME.test(value)) return setError("Use letters, digits, - and _, starting with a letter or _."); + if (taken(value)) return setError(`${value}.py already exists.`); + setBusy(true); + setError(null); + try { + await onCreate(value, kind); + onDone(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Could not create the notebook."); + } finally { + setBusy(false); + } + }; + + return ( +
    { + event.preventDefault(); + void submit(); + }} + > + {first &&

    An empty workspace. Start with a part: a notebook that builds one solid.

    } + { + setName(event.target.value); + setError(null); + }} + onKeyDown={(event) => event.key === "Escape" && !first && onCancel()} + placeholder="name, e.g. bracket" + aria-label="Name of the new notebook" + spellCheck={false} + className="w-full border-2 border-input bg-transparent px-2 py-1 focus:border-focus focus:outline-none" + /> +
    + {(["part", "assembly"] as const).map((option) => ( + + ))} +
    + {kind === "assembly" && parts.length > 0 && ( +

    + Starts with {parts.slice(0, 2).map((row) => row.name).join(" and ")}; change what it uses in its cells. +

    + )} + {error &&

    {error}

    } +
    + + {!first && ( + + )} +
    +
    + ); +} diff --git a/editor-web/src/lib/api.ts b/editor-web/src/lib/api.ts index 8509c6c..d7e601f 100644 --- a/editor-web/src/lib/api.ts +++ b/editor-web/src/lib/api.ts @@ -47,13 +47,16 @@ export function openNotebook(path: string): Promise<{ ok: boolean }> { }).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", { +/** Provision a new entry notebook from the server template and open it. Rejects with the server's reason (a bad or taken name). */ +export async function newNotebook(name: string, kind: "part" | "assembly"): Promise<{ created: string }> { + const response = await fetch("/api/workspace/new", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, kind }), - }).then((response) => json<{ created: string }>(response)); + }); + const body = (await response.json().catch(() => null)) as { created?: string; error?: string } | null; + if (!response.ok || !body?.created) throw new Error(body?.error ?? `${response.status} ${response.statusText}`); + return { created: body.created }; } /** What the viewport shows (PNG data URL), for coding agents to look at. */ From 4a03c47147f2aa8d2beec52c920701ece473551d Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Wed, 14 Oct 2026 00:58:26 +0800 Subject: [PATCH 3/4] release: 2.2.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bump, changelog heading, update notes (EN/中文), SKILL.md metadata, README. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 ++ README.md | 8 ++++++-- README.zh-CN.md | 7 +++++-- docs/skill/SKILL.md | 4 ++-- docs/updates/2.2.2.md | 27 +++++++++++++++++++++++++++ docs/updates/2.2.2.zh-CN.md | 25 +++++++++++++++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 8 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 docs/updates/2.2.2.md create mode 100644 docs/updates/2.2.2.zh-CN.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e84b0..989b14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ## Unreleased +## 2.2.2 — 2026-10-14 + ### Added - Editor: code completion from the SDK itself. `scad.` (and `scad.ql.`, `scad.std.gear.`, `ql.`) diff --git a/README.md b/README.md index ad8d5cd..47337c5 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,14 @@ the full crank cycle sweeps clean in the same sim · --- -## Update Notes (2.2.1) +## Update Notes (2.2.2) > Validate generated definitions, assembly constraints, and manufacturing > geometry before production use. +- **2.2.2.** The editor completes `scad.` from the SDK itself (a call is written with every required + parameter as `name=`, Tab moves between them) and creates notebooks from the workspace tab; see the + [update notes](docs/updates/2.2.2.md). - **New editor.** `sca edit` is part of the SDK now (`pip install "simplecadapi[editor]"`). It runs a workspace's notebooks in a marimo kernel with the SDK's own cell executor, previews the live model straight from the @@ -199,7 +202,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.1`. +Current release: `simplecadapi==2.2.2`. ## What It Provides @@ -714,6 +717,7 @@ and the [STEP reconstruction workflow](docs/skill/references/workflows/step-reco ## Documentation +- 2.2.2 update notes: [`docs/updates/2.2.2.md`](docs/updates/2.2.2.md) - 2.2.1 update notes: [`docs/updates/2.2.1.md`](docs/updates/2.2.1.md) - 2.1.3b1 update notes: [`docs/updates/2.1.3b1.md`](docs/updates/2.1.3b1.md) - 2.1.2 update notes: [`docs/updates/2.1.2.md`](docs/updates/2.1.2.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 161501c..ef7da77 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -133,10 +133,12 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真 --- -## 更新日志(2.2.1) +## 更新日志(2.2.2) > 投入生产前请验证生成的定义、装配约束和制造几何。 +- **2.2.2。** 编辑器直接从 SDK 补全 `scad.`(调用会把每个必填参数写成 `name=`,Tab 在它们之间跳转),并能在工作区页面创建 + notebook;见[更新说明](docs/updates/2.2.2.zh-CN.md)。 - **新编辑器。** `sca edit` 现在是 SDK 的一部分(`pip install "simplecadapi[editor]"`)。它用 SDK 自己的 cell 执行器在 marimo 内核里运行工作 区的 notebook,直接由实体生成实时预览,把你在视口里的点选写成选择代码,并让 @@ -173,7 +175,7 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真 SimpleCADAPI 是一个基于 OCP 的 Python CAD SDK,提供清晰的函数式建模操作和可重放的模型图。它在 OpenCascade 几何内核之上提供精简的公共 API,可用于创建实体、应用特征、添加语义标签、查询拓扑、导出制造文件,以及将记录的模型转换为 FreeCAD 工作流。 -当前版本:`simplecadapi==2.2.1`。 +当前版本:`simplecadapi==2.2.2`。 ## 核心能力 @@ -599,6 +601,7 @@ uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py ## 文档 +- 2.2.2 更新说明:[`docs/updates/2.2.2.zh-CN.md`](docs/updates/2.2.2.zh-CN.md) - 2.2.1 更新说明:[`docs/updates/2.2.1.zh-CN.md`](docs/updates/2.2.1.zh-CN.md) - 2.1.3b1 更新说明:[`docs/updates/2.1.3b1.zh-CN.md`](docs/updates/2.1.3b1.zh-CN.md) - 2.1.2 更新说明:[`docs/updates/2.1.2.zh-CN.md`](docs/updates/2.1.2.zh-CN.md) diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md index 9cd131c..81dc3a8 100644 --- a/docs/skill/SKILL.md +++ b/docs/skill/SKILL.md @@ -4,9 +4,9 @@ description: Build, assemble, inspect, reconstruct, and export parametric CAD mo license: Apache-2.0 metadata: project: simplecadapi - version: 2.2.1 + version: 2.2.2 package-name: simplecadapi - package-version: 2.2.1 + package-version: 2.2.2 --- # SimpleCAD SDK Skill diff --git a/docs/updates/2.2.2.md b/docs/updates/2.2.2.md new file mode 100644 index 0000000..bea6c78 --- /dev/null +++ b/docs/updates/2.2.2.md @@ -0,0 +1,27 @@ +# 2.2.2 Update Notes + +Two additions to the code-first editor (`sca edit`), and a fix that made one of them usable. + +## Code completion from the SDK + +`scad.` (and `scad.ql.`, `scad.std.gear.`, `ql.`, `Q.`) lists the real functions with their +signatures; the side panel shows every parameter with its default and the docstring. + +- Accepting a function, or typing `(` after a known name, writes the call with every + **required** parameter as `name=`: `scad.make_box_rsolid(width=, height=, depth=)`. + **Tab** moves to the next parameter. +- Inside a call, the parameters not given yet complete as `name=`, required ones first. +- The names come from the running package (`GET /api/sdk`), so they cannot drift from the code. + +## Create a notebook from the workspace tab + +A **new** action in the notebooks header starts a notebook from the SDK's template: a part, or an +assembly that uses the first two parts of the workspace. The name is checked as you type. An empty +workspace opens on that form, so `sca edit` on an empty directory is usable. + +## Fixed + +The cell editors rebuilt their configuration on every render, which dropped a snippet in progress +and could disturb typing; the editor's `onChange` and options are stable now. + +See the [changelog](../../CHANGELOG.md) for the complete list. diff --git a/docs/updates/2.2.2.zh-CN.md b/docs/updates/2.2.2.zh-CN.md new file mode 100644 index 0000000..c2592d9 --- /dev/null +++ b/docs/updates/2.2.2.zh-CN.md @@ -0,0 +1,25 @@ +# 2.2.2 更新说明 + +`sca edit` 编辑器的两项新增,以及一个让其中一项能正常使用的修复。 + +## 来自 SDK 的代码补全 + +输入 `scad.`(以及 `scad.ql.`、`scad.std.gear.`、`ql.`、`Q.`)会列出真实存在的函数和签名; +右侧面板显示每个参数、默认值和文档说明。 + +- 选中一个函数,或在已知函数名后输入 `(`,会把每个**必填**参数写成 `name=`: + `scad.make_box_rsolid(width=, height=, depth=)`。**Tab** 跳到下一个参数。 +- 在调用括号内,还没写的参数会补全成 `name=`,必填的排在最前。 +- 名字来自正在运行的包(`GET /api/sdk`),不会和代码不一致。 + +## 在工作区页面创建 notebook + +notebooks 标题处新增 **new**,按 SDK 模板创建 notebook:一个零件,或一个使用工作区前两个零件的装配。 +边输入边检查名字。空工作区会直接打开这个表单,所以对空目录运行 `sca edit` 也能用了。 + +## 修复 + +cell 编辑器每次渲染都会重建配置,导致进行中的占位跳转丢失,也可能干扰输入;现在编辑器的 +`onChange` 和选项都是稳定的。 + +完整列表见[更新日志](../../CHANGELOG.md)。 diff --git a/pyproject.toml b/pyproject.toml index 91a9e5e..461392f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simplecadapi" -version = "2.2.1" +version = "2.2.2" description = "A simplified OCP-native CAD modeling Python API" readme = "README.md" license = { file = "LICENSE" } diff --git a/uv.lock b/uv.lock index 7d795ef..6650fff 100644 --- a/uv.lock +++ b/uv.lock @@ -3227,7 +3227,7 @@ wheels = [ [[package]] name = "simplecadapi" -version = "2.2.1" +version = "2.2.2" source = { editable = "." } dependencies = [ { name = "cadquery-ocp" }, From f2ef7fa2ab5ff3b6fc735fb23663612fb83df13a Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Wed, 14 Oct 2026 00:59:47 +0800 Subject: [PATCH 4/4] docs(readme): the update notes cover only the latest version The section accumulated 2.2.1 and 2.1.3b1 items; it now describes 2.2.2 and points to the changelog and docs/updates for earlier versions. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 42 +++++++++++++----------------------------- README.zh-CN.md | 31 +++++++++---------------------- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 47337c5..430b377 100644 --- a/README.md +++ b/README.md @@ -154,35 +154,19 @@ the full crank cycle sweeps clean in the same sim · > Validate generated definitions, assembly constraints, and manufacturing > geometry before production use. -- **2.2.2.** The editor completes `scad.` from the SDK itself (a call is written with every required - parameter as `name=`, Tab moves between them) and creates notebooks from the workspace tab; see the - [update notes](docs/updates/2.2.2.md). -- **New editor.** `sca edit` is part of the SDK now (`pip install - "simplecadapi[editor]"`). It runs a workspace's notebooks in a marimo kernel - with the SDK's own cell executor, previews the live model straight from the - solids, writes your viewport picks as selection code, and lets an agent see - what you see (`sca snapshot`). **Re-Studio is removed**: the reverse- - engineering studio (the `Re-mode` workspace of the standalone sca-web-editor) - is gone, and the standalone sca-web-editor is replaced by `sca edit`. STEP - reconstruction continues through the inspection API and the skill's - step-reconstruction workflow. -- **One runtime, one cache.** The editor, `sca run` and - `simplecadapi.runtime.run_notebook()` share the per-cell cache, so what one - computed the others restore. Notebook parameters come from `# ---- params - ----` cells with an optional `# ui:` comment; local modules (shared - dimensions) get their own view in the editor. -- **Landing path.** `docs/landing.md` is the script an agent follows to install - the SDK and its skill and open an example; `sca example u_link` copies the - bundled workspace and `sca skill where` shows where the skill goes. -- Earlier: 2.1.3b1 shipped the addon ecosystem (`sca addon ...`, the - `sca-addon.toml` descriptor, the `.scadpkg` format spec); see its - [update notes](docs/updates/2.1.3b1.md). - -Every example model is a notebook; its export scripts load the product with -`run_notebook`, capture one `.scadpkg`, and translate AP242 `.step` and -editable `.FCStd` from that same package. The AP242/Gmsh example under -`examples/ap242_gmsh_volume_mesh/` exposes each export and FEM stage as a -separate directly runnable script. +- **Code completion from the SDK.** In the editor, `scad.` (and `scad.ql.`, + `scad.std.gear.`) lists the real functions with their signatures and + docstrings. Accepting one, or typing `(` after its name, writes the call with + every required parameter as `name=`, and Tab moves between them; inside a call + the parameters not given yet complete as `name=`. +- **New notebooks from the workspace tab.** A **new** action starts a part, or + an assembly that uses the first two parts, from the SDK's template; an empty + workspace opens on that form. +- **Fix.** The cell editors no longer rebuild their configuration on every + render, which dropped a snippet in progress. + +Full notes for this version: [`docs/updates/2.2.2.md`](docs/updates/2.2.2.md). +Earlier versions: the [changelog](CHANGELOG.md) and [`docs/updates/`](docs/updates/). --- diff --git a/README.zh-CN.md b/README.zh-CN.md index ef7da77..801891d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -137,28 +137,15 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真 > 投入生产前请验证生成的定义、装配约束和制造几何。 -- **2.2.2。** 编辑器直接从 SDK 补全 `scad.`(调用会把每个必填参数写成 `name=`,Tab 在它们之间跳转),并能在工作区页面创建 - notebook;见[更新说明](docs/updates/2.2.2.zh-CN.md)。 -- **新编辑器。** `sca edit` 现在是 SDK 的一部分(`pip install - "simplecadapi[editor]"`)。它用 SDK 自己的 cell 执行器在 marimo 内核里运行工作 - 区的 notebook,直接由实体生成实时预览,把你在视口里的点选写成选择代码,并让 - agent 看到你看到的(`sca snapshot`)。**Re-Studio 已移除**:逆向工程 studio - (独立 sca-web-editor 的 `Re-mode` 工作区)不再存在,独立的 sca-web-editor 由 - `sca edit` 取代。STEP 重建继续通过检查 API 和 skill 的 step-reconstruction - 工作流进行。 -- **一个运行时,一份缓存。** 编辑器、`sca run` 和 - `simplecadapi.runtime.run_notebook()` 共用逐 cell 缓存,一方算过的另一方直接 - 复用。notebook 参数来自 `# ---- params ----` cell,可加 `# ui:` 注释指定控件; - 本地模块(共用尺寸)在编辑器里有独立视图。 -- **落地路径。** `docs/landing.md` 是 agent 安装 SDK 和 skill、打开示例时遵循的 - 脚本;`sca example u_link` 复制内置工作区,`sca skill where` 显示 skill 的安装位置。 -- 之前:2.1.3b1 带来插件生态(`sca addon ...`、`sca-addon.toml` 描述文件、 - `.scadpkg` 格式规范),见[更新说明](docs/updates/2.1.3b1.zh-CN.md)。 - -每个示例模型都是 notebook;导出脚本用 `run_notebook` 载入产品,捕获一个 -`.scadpkg`,再从同一个产品包转换出 AP242 `.step` 和可编辑 `.FCStd`。 -`examples/ap242_gmsh_volume_mesh/` 下的 AP242/Gmsh 示例把每个导出和 FEM 阶段 -都做成可直接运行的独立脚本。 +- **来自 SDK 的代码补全。** 在编辑器里,输入 `scad.`(以及 `scad.ql.`、`scad.std.gear.`) + 会列出真实存在的函数、签名和文档说明。选中一个函数,或在函数名后输入 `(`,会把每个必填 + 参数写成 `name=`,Tab 在它们之间跳转;在调用括号内,还没写的参数会补全成 `name=`。 +- **在工作区页面创建 notebook。** 新增的 **new** 按 SDK 模板创建一个零件,或一个使用前两个 + 零件的装配;空工作区会直接打开这个表单。 +- **修复。** cell 编辑器不再每次渲染都重建配置,这会让进行中的占位跳转丢失。 + +本版本的完整说明:[`docs/updates/2.2.2.zh-CN.md`](docs/updates/2.2.2.zh-CN.md)。 +更早的版本:见[更新日志](CHANGELOG.md)和 [`docs/updates/`](docs/updates/)。 ---