--- name: feedback-ironpython-exec-scoping description: "RhinoMCP execute_rhino_code (IronPython 2.7) scoping gotchas that caused repeated \"name X is not defined\" errors — avoid nested def/comprehensions and remember Z is independent of horizontal L/W axes" metadata: node_type: memory type: feedback originSessionId: d49157ae-9661-437c-a1d1-639728d3fff0 modified: 2026-09-21T08:00:01.071Z --- When writing Python for `mcp__rhino__execute_rhino_code` (IronPython 2.7, run via exec with separate globals/locals): **Why:** the code runs with `exec(code, globals, locals)` where globals != locals. Any `def`-ined function or list/dict/set comprehension created inside the script cannot see the script's own top-level local variables — only globals. This repeatedly produced `name 'X' is not defined` errors even though X was clearly assigned earlier in the same script. **How to apply:** - Never define a helper `def function(...)` inside an `execute_rhino_code` script if it needs to reference other local variables from that script — inline the logic instead, or recompute needed values inside the function from parameters only. - Avoid list/dict comprehensions that reference outer local variables (e.g. `[pts[i].DistanceTo(pts[i+1]) for i in range(3)]` inside the same script scope) — same failure mode. Use an explicit loop or separate statements instead. - When building 3D points from a rotated local coordinate frame (length axis L, width axis W) that lie flat in a horizontal plane (Lz = Wz = 0, as is the case for [[project_p1_pier_structure]]'s skewed-in-plan coping), the formula `Z = Oz + s*Lz + w*Wz` will always just give `Oz` — Z must be set explicitly per point, it is not derivable from the L/W parametrization. - `Rhino.Geometry.Intersect.Intersection.RayShoot(ray, [geometry], maxReflections)` returns a `Point3d[]` of hit points directly, not an array of ray parameters — don't call `ray.PointAt(hit)` on the results. - `rs.AddBlock(object_ids, base_point, name, delete_input)` only **defines** the block and deletes the source objects; it does **not** insert an instance. Follow it with `rs.InsertBlock2(name, xform)` where `xform` is `Transform.Translation(base_point)` to place it back at the original location — inserting with `Transform.Identity` puts it at world origin instead. - To hide a solid brep so embedded/buried curves or pipes become visible, use Wireframe display mode (or hide the specific object) — Shaded mode's opaque surface hides anything inside it regardless of layer visibility toggles in some cases. - **Nested layer visibility is gated by the parent.** Setting `rs.LayerVisible("Parent::Child", True)` has no visible effect if `"Parent"` itself is off — worse, turning a parent off with `rs.LayerVisible("Parent", False)` can also flip the child's own visibility flag to False as a side effect. When isolating a nested rebar mark layer (e.g. `P1_두부_주철근_1CYCLE::C26_H19`) by looping `rs.LayerNames()` and turning everything off except a keep-list, always add the PARENT layer name to the keep-list too, not just the full child path — otherwise the mark silently fails to render even though the objects exist and its own layer flag reads True. This caused repeated "rebar isn't visible" reports from the user in one session ([[project_p1_pier_structure]]) before being traced to this cause — always verify with `rs.LayerVisible(parent)` AND `rs.LayerVisible(child)` when rebar seems to vanish, before suspecting a placement/scale bug. - After any isolate-for-screenshot operation, restore ALL layer visibility immediately afterward (loop `rs.LayerNames()` and set every one to `True`) — don't leave the document in an isolated state, since the user may look at the live Rhino viewport themselves between turns. - **`rs.AddBlock` re-bases raw geometry relative to `base_point`, but does NOT re-base nested block-instance objects** — when the objects passed to `rs.AddBlock` are themselves `InstanceReference`s (e.g. bundling several already-placed block instances into one wrapper block), their world-coordinate transform is captured as-is inside the new definition. Re-inserting with `Transform.Translation(base_point)` then double-applies the offset (coordinates come out roughly doubled). For a wrapper block made of existing instances, insert with `Transform.Identity` instead. For a block made from raw curves/breps freshly created at `base_point`, `Transform.Translation(base_point)` is correct (this is the case in [[feedback_coping_haunch_rebar_method]]'s leaf-level boxes). When in doubt, insert once and check the resulting bounding box against the expected world coordinates before moving on. - To bulk-remove unused/orphaned block definitions (e.g. after re-generating geometry and leaving old block names behind), loop `sc.doc.InstanceDefinitions`, check `idef.UseCount()` (no arguments — the 1-arg overload doesn't exist here), and call `sc.doc.InstanceDefinitions.Delete(idef.Index, True, True)` for any with a zero count. - **Whether a freshly-`AddBlock`-ed instance needs `Transform.Identity` or `Transform.Translation(base_point)` on reinsertion is NOT reliably predictable** when the bundled objects are themselves block instances (as opposed to raw curves/breps) — it varies between calls in the same session for reasons not pinned down (possibly stale/colliding block-definition names from earlier failed attempts, seen once as a confirmed cause). The robust pattern: insert with `Identity`, check the resulting bbox's X against the known expected world X (from the pre-bundle `base_point`); if off by more than ~1 unit, delete that instance and reinsert with `Translation(base_point)` instead. Always verify this way rather than assuming either transform is correct. A large constant (non-varying) Z across many wrapped stations is not automatically a bug — check whether that span of the structure is genuinely flat (e.g. sits on a flat bearing shelf) before assuming the transform is broken. - Before reusing a block base-name across repeated build/rebuild attempts, purge stale unused definitions matching that name pattern first (see the `UseCount()`-zero purge above) — leftover definitions from an earlier failed attempt can collide with or shadow a later rebuild using the same names and produce corrupted (wrong-position) instances. **This bit twice in the same session** (C18's station 1/19, then C15's entire station level) — the safe sequence every time a mark gets rebuilt at the same station-numbering scheme is: (1) delete the mark's layer objects, (2) explicitly purge every definition whose name starts with that mark (`idef.Name.startswith("C15")` etc, not just an exact-name match — the per-station and per-box-type sub-block names share the prefix), (3) only then regenerate. Also always delete throwaway verification instances (e.g. from `rs.InsertBlock2` used just to check a definition's position) immediately — a leftover one keeps that definition's `UseCount()` above zero and defeats the purge. - **`Rhino.Geometry.PipeCapMode.None` is a syntax error** (invalid syntax, not a runtime error) — `None` is a reserved Python keyword and can't follow a dot as an attribute name. The actual enum member for "no cap" is spelled `Rhino.Geometry.PipeCapMode.NONE` (all-caps) in this RhinoCommon build — check with `dir(Rhino.Geometry.PipeCapMode)` if unsure rather than guessing the casing. - **To lengthen just one leg of an existing ⊓/⊔ box leaf (e.g. extending a lid's leg while its overlap point with a separate base piece must stay fixed — see [[project_p1_pier_structure]]'s C13/14/15/16/17 lid-extend fix), don't rigidly translate the whole Brep.** Instead: duplicate the Brep, translate the duplicate by the desired offset, then bridge the gap with a new straight filler pipe from the original (fixed) end point to the shifted end point (`Brep.CreatePipe(LineCurve(Line(p, p_shifted)), radius, False, PipeCapMode.NONE, False, tol, angleTol)`), and put `[shifted_original, ...unrelated_pieces_unchanged, filler1, filler2]` back into the definition via `ModifyGeometry`. This preserves the exact original bend/corner geometry (never touched) and the exact original overlap length with any neighboring piece, while genuinely adding real bar length at the correct end — the general pattern is "shift + fill the gap with a new straight segment," not "stretch/scale the existing solid." To find each leg's exact fixed end point, use `brep.Vertices` — for a simple open-ended (uncapped) pipe swept along a bent rail with no sharp corners, the topological vertex list contains exactly the 2 open-end points (both located at the Brep's own bbox Zmin if it opens downward, or Zmax if it opens upward) and nothing else; this also doubles as a fast way to tell two overlapping box pieces apart (the one whose vertices sit at ITS OWN Zmin opens downward/"lid"-style, the one whose vertices sit at ITS OWN Zmax opens upward/"base"-style). - **ROOT CAUSE of "rebar color keeps not matching its diameter after any edit" (recurring user complaint): `InstanceDefinitions.ModifyGeometry(index, geometryList)` called with the 2-arg (geometry-only) overload silently RESETS the `ObjectAttributes` of every object in that definition — including ones passed through unchanged by reference — to `ColorSource=ColorFromLayer` on whatever incidental layer index the definition happened to store (seen: layer `P1_두부(코핑)`, not the mark's own rebar layer).** The correct/original state (confirmed by comparing against an untouched box-def, e.g. `C12_1005_01`) is `ColorSource=ColorFromParent` on every leaf object — that's what makes a nested block instance actually display the mark layer's diameter-based color (e.g. H22=dark red 104,0,25) instead of whatever the leaf's own stored layer says. **Any use of the geometry-only `ModifyGeometry` overload (as used throughout [[project_p1_pier_structure]]'s C13/14/15/16/17 lid-extend fix on 2026-09-21) silently breaks color for every object in that definition, not just newly-added ones.** Fix (queued for [[project_p1_pier_structure]]'s next session, not yet applied): use the 3-arg overload instead — `InstanceDefinitions.ModifyGeometry(idef.Index, geometryList, attributesList)` — where `attributesList` is a matching list of `Rhino.DocObjects.ObjectAttributes()` with `.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromParent` set on each (this overload exists; confirmed via `InstanceDefinitionTable.ModifyGeometry.__doc__` listing 3 overloads including `(Int32, IEnumerable, IEnumerable)`). Going forward, **always pass an attributes list alongside geometry** on any `ModifyGeometry` call on these rebar box-defs, never the bare 2-arg form, to avoid re-breaking color on every future edit. - **To rigidly move ONE station's sub-block within a nested "mark" block definition (e.g. shift just `C13_13_r3` inside the `C13` definition up in Z), `rs.TransformObject`/`sc.doc.Objects.Transform` on the id from `idef.GetObjectIds()` silently no-ops** (returns `None`, bbox unchanged) — those ids are not transformable as live doc objects. The working method: `objs = idef.GetObjects()`; for each `o`, `geo = o.Geometry` (an `InstanceReferenceGeometry` whose `.Xform` is the placement transform and `.ParentIdefId` names the nested sub-block); build a **new** list where the target entries are replaced by `Rhino.Geometry.InstanceReferenceGeometry(geo.ParentIdefId, Rhino.Geometry.Transform.Translation(0,0,dz) * geo.Xform)` and all others keep the original `geo`; then call `sc.doc.InstanceDefinitions.ModifyGeometry(idef.Index, newlist)` (note: **`.Index` not `.Id`** — passing the Guid throws a type-conversion error since the method wants the integer index) and `sc.doc.Views.Redraw()`. Mechanically verified working (the `.Index` vs `.Id` call itself succeeds and redraws correctly), but **this whole-sub-block rigid-move was the WRONG fix for [[project_p1_pier_structure]]'s C13/14/15/16/17 datum-raise** — it was tried first, then reverted, because those stations are lid+base overlapping boxes and rigidly moving the whole thing also drags the base (which must stay put); see the lid-extend bullet above for the corrected approach. Keep this `ModifyGeometry`/`.Index` mechanism in mind for cases where a genuine whole-station rigid move IS correct (e.g. no base/lid split, or intentionally repositioning an entire self-contained station). Identify which sub-block belongs to which named station via `sc.doc.InstanceDefinitions.FindId(geo.ParentIdefId).Name` (not `.Find(guid)` — that overload wants a string and throws).