Hi McNeel team. We have ran into a bug with linked blocks that sometimes causes kind of ghost objects to appear. I have used Claude and the Rhino MCP to get to the bottom of this and provide a reproducible example outside of our normal working files. So this is a bit of a co-authored bug report, but it is verified by me. -Jirri
Summary
When a linked block is inserted, Rhino adds the block definitions nested inside the
linked file to the document’s definition table as reference definitions
(InstanceDefinition.IsReference == True, IsTenuous == True). These definitions are
findable by name.
If you then run -Insert with the name of one of those nested definitions, Rhino
creates a top level, non-reference instance object bound to that reference
definition. The resulting object is broken in three ways:
- It cannot be deleted.
_Delete,rs.DeleteObject()and
RhinoDoc.Objects.Delete()all refuse it and report no error. Only
RhinoDoc.Objects.Purge()removes it. - It is silently discarded on save. Neither the object nor the definition is
written to the 3dm. The part is simply gone when the file is reopened. No warning,
no message, nothing in the command history. - The document’s own bookkeeping is inconsistent about it.
InstanceDefinition.InUse(0)returnsFalsewhile
InstanceDefinition.GetReferences(0)returns that same object. - It doesn’t have a gumball and cannot be moved
We also have no way to recover this situation. A reference definition cannot be promoted in
place: InstanceDefinitionTable.ModifySourceArchive(...) returns True but changes
nothing, and -BlockManager Properties <name> UpdateType Embedded|Linked returns
False and refuses.
Environment
- Rhino 8 SR35, 8.35.26251.13001, Windows 11 Pro 26200
- Reproduced from a pristine, 25 KB file authored through
Rhino.FileIO.File3dm - Reproduced using stock Rhino commands only. Our own plug-in was loaded in the test
session; no part of the repro calls into it.
Files
parent.3dm— 25 KB. Contains one static block definition namedWidget
(a 100 mm cube) and one instance of it. Nothing else.
Reproduction
Start Rhino 8 with a new, empty model, then:
Step 1 — insert parent.3dm as a linked block
_-Insert _File=_Yes _LinkMode=_Link _LayerStyle=_Active "<path>\parent.3dm" _Block 0 _Enter 0
The document now has two definitions:
| name | UpdateType | IsReference | IsTenuous |
|---|---|---|---|
parent |
Linked | False | False |
Widget |
Static | True | True |
Widget came along as a nested child of the linked file. It is a reference
definition. It is returned by InstanceDefinitionTable.Find("Widget") and by rs.IsBlock("Widget").
Step 2 — insert an instance of the nested definition by name
_-Insert "Widget" _Block 500,500,0 1 0 _Enter
A cube appears at 500,500,0. Rhino accepts this without any warning.
Observed state:
Widget.IsReference True
Widget.InUse(0) False <-- claims no top level references
Widget.GetReferences(0) 1 <-- but returns one
object.IsReference False <-- a normal, user owned object
Step 3 — try to delete or move the new object
Select the cube at 500,500,0 and press Delete, or run _Delete. Or try to use Move.
Expected: the object is deleted.
Actual: nothing happens. The object stays. No error, no message.
RhinoDoc.Objects.Delete()returnsFalse. OnlyRhinoDoc.Objects.Purge()works.
Step 4 — save and reopen
Save the model, then reopen it.
Expected: the block instance is still there, or Rhino warns that it cannot be
saved.
Actual: the object and theWidgetdefinition are both absent from the written
file. Silent data loss.
verify_repro.py performs steps 1–4 and prints a pass/fail line for each.
Expected behaviour
Our prefence:
-Insertpromotes the reference definition to a real definition when a top
level instance of it is created, which is the state you get when the same block is
inserted from file before the linked parent is loaded. That state is legal and
works correctly today, so the target is well defined.
The InUse(0) / GetReferences(0) disagreement looks like a separate bookkeeping bug
and may be the underlying cause: whatever -Insert uses to decide the insert is legal
appears not to be what the object table actually records.
How we hit this in production
We keep a library of several thousand parts as linked blocks, and we assemble models
from linked module files. A module file routinely contains the same library parts a
user will later place by hand.
So the order of operations decides whether a part is healthy or poisoned:
- Insert library part first, then link the module that also uses it → correct.
The definition staysIsReference=Falseand is shared by both. We have 38 such
definitions in the affected model, all fine. - Link the module first, then insert the same library part → broken, as above.
Nothing in the UI distinguishes the two. In one production model we found 304
definitions primed for this, and one part that had already been placed and was
silently lost on every save. Tracking it down took a long time precisely because the
evidence destroys itself: isolate the block, save, and it is gone.
Scripts
Standalone, no dependencies on our codebase. Run with ScriptEditor
(Python 3): verify_repro.py: runs the four steps above and asserts each symptom.
verify_repro.py (3.8 KB)
parent.3dm (24.7 KB)