diff --git a/skills/rhino3d-scripts/SKILL.md b/skills/rhino3d-scripts/SKILL.md index d7b467b0..59a0cb83 100644 --- a/skills/rhino3d-scripts/SKILL.md +++ b/skills/rhino3d-scripts/SKILL.md @@ -137,6 +137,12 @@ else: - **GUID strings vs `System.Guid`.** `rhinoscriptsyntax` accepts either; RhinoCommon wants `System.Guid`. Convert with `System.Guid(str_id)` if needed. - **Don’t call `doc.Views.Redraw()` inside a tight loop.** Toggle redraw once outside the loop. - **`.rvb` is just `.vbs` renamed** with a Rhino-specific extension so Rhino’s `LoadScript` recognizes it. Same VBScript engine. +- **`Rhino.RhinoApp.IsHeadless` may not exist on older Rhino 8 builds.** Use `getattr(Rhino.RhinoApp, "IsHeadless", None)` and check for `None` before using. Fall back to a heuristic (e.g. `sc.doc.Views.Count == 0`) or assume GUI present. +- **`RhinoMath` is at `Rhino.RhinoMath`, not `Rhino.DocObjects.RhinoMath`.** Accessing `Rhino.DocObjects.RhinoMath` raises `AttributeError`. +- **`doc.Objects.AddBrep()` returns `System.Guid.Empty` on failure.** In Rhino 8 CPython the `System` namespace may not be directly importable; check the return value as a string: `str(obj_id) == "00000000-0000-0000-0000-000000000000"`. +- **`rhinoscriptsyntax` has no type stubs.** Static analysers (Pylance/Pyright) flag `import rhinoscriptsyntax as rs` as unresolvable. Suppress with `# type: ignore` on the import line; the module is always available at Rhino runtime. +- **Never name a script after a Python standard-library module** (e.g. `random.py`, `math.py`, `os.py`). IronPython 2.7 (`_-RunPythonScript`) resolves the script directory before stdlib, so any `import random` inside the stdlib (e.g. `tempfile` imports `random` internally) will find your file instead and fail with `Cannot import name `. CPython 3 (`rhinocode`) is unaffected because it resolves stdlib first. Rename the script or avoid importing modules that pull in the shadowed name. +- **Em dashes and other non-ASCII characters silently break `_-RunPythonScript` (IronPython 2.7).** `rhinocode script` uses CPython 3 (UTF-8 by default) so the same file works there, making the failure non-obvious. IronPython 2.7 raises `SyntaxError: Non-ASCII character '\xe2'` at the first offending byte. The most common culprit is the **em dash** (`--` auto-converted to `--` by many editors). Add `# -*- coding: utf-8 -*-` as line 1 of every script that must run under both runtimes, and replace typographic characters with ASCII equivalents: em dash `--`, arrow `->`, multiplication `x`. ## Troubleshooting @@ -150,6 +156,10 @@ else: | Undo undoes only the last object of a batch | Wrap the batch in `BeginUndoRecord` / `EndUndoRecord`. | | Script works alone but fails as a startup script | Startup runs before any document is open — return early or skip document-dependent work when `sc.doc is None`. | | `rs.Command("...")` returns `False` | The macro string is malformed. Prefix with `!` and `-`, end every prompt with `_Enter` or a value. | +| `AttributeError: type object 'RhinoApp' has no attribute 'IsHeadless'` | Property added in a later Rhino 8 build. Use `getattr(Rhino.RhinoApp, "IsHeadless", None)` and guard against `None`. | +| `rhinocode script` ignores arguments after the script path | rhinocode concatenates extra tokens onto the file URI. Pass data via a temp file or a Rhino dialog instead. See `references/macros-and-loading.md`. | +| `Cannot import name ` inside stdlib (e.g. `tempfile`, `os`) when using `_-RunPythonScript` | Script filename shadows a stdlib module (e.g. `random.py` shadows `random`). IronPython 2.7 searches the script directory before stdlib. Rename the script, or remove the `import` that pulls in the shadowed module and replace it with a direct alternative (e.g. read `%TEMP%` via `os.environ` instead of `import tempfile`). | +| `SyntaxError: Non-ASCII character '\xe2' ... but no encoding declared` | IronPython 2.7 (`_-RunPythonScript`) hit an em dash or similar character. Add `# -*- coding: utf-8 -*-` as line 1, or replace the character: em dash `--`, arrow `->`. The same file runs fine under `rhinocode` (CPython 3), which hides the problem. | ## References diff --git a/skills/rhino3d-scripts/references/macros-and-loading.md b/skills/rhino3d-scripts/references/macros-and-loading.md index df524781..632d67b5 100644 --- a/skills/rhino3d-scripts/references/macros-and-loading.md +++ b/skills/rhino3d-scripts/references/macros-and-loading.md @@ -93,3 +93,56 @@ rs.Command("! _-Line 0,0,0 10,0,0", echo=False) ``` `echo=False` suppresses command-history output but does **not** suppress prompts — always use `-` and complete every prompt within the macro string. + +## rhinocode CLI (Rhino 8) + +`rhinocode` is the Rhino 8 command-line tool for running scripts and commands against a running Rhino instance from an external terminal. + +### Basic commands + +```text +rhinocode script "C:\path\to\MyScript.py" # run a Python script +rhinocode command "_Circle 0,0,0 5 _Enter" # run a Rhino command +rhinocode --rhino script "MyScript.py" # target a specific instance +``` + +`` looks like `rhinocode_remotepipe_75029`. Find the ID in Rhino's title bar or +by running `StartScriptServer` in Rhino, which prints the pipe name to the command line. + +### Architecture — pipe server + +rhinocode does **not** spawn a new Rhino process. It connects to a persistent server that Rhino +exposes (`StartScriptServer`). Scripts execute inside that server process, which means: + +- **Environment variables are isolated.** Variables set in the calling shell (`set FOO=bar`) + are NOT visible inside the script via `os.environ`. The server was started before your shell. +- **`os.getcwd()` is the server's working directory**, not the directory you called rhinocode + from. Do not rely on it for output paths; pass the path explicitly. +- **`print()` output IS piped back** to the calling terminal — use it freely for status messages. + +### Passing data into a script + +rhinocode does not support positional arguments after the script path — any extra tokens are +concatenated onto the file URI, causing a "file does not exist" error. Workarounds: + +|Channel|How|Notes| +|---|---|---| +|Temp file|Caller writes a file to a known location; script reads and deletes it.|Use a path derived from `__file__` (see below), not `%TEMP%` — the server may resolve a different temp dir.| +|Rhino dialog|Script calls `rhinoscriptsyntax.ListBox` / `GetString`|Always works; user sees a prompt in Rhino.| + +### `__file__` is a URI + +When running via rhinocode, `__file__` is set as a `file:///` URI with URL-encoded characters +(spaces become `%20`). Decode it before using it as a filesystem path: + +```python +import os, sys, urllib.parse + +def _script_dir(): + raw = __file__ + if raw.startswith("file:///"): + raw = urllib.parse.unquote(raw[len("file:///"):]) + if sys.platform == "win32": + raw = raw.replace("/", os.sep) + return os.path.dirname(os.path.abspath(raw)) +```