Symptom

Switching the expanded VIEW face directly from a Project.Diagram node to a node of a different type (e.g. a DataPoint) — without closing the first one — threw a ClassCastException and put a modal Java error dialog over the whole app:

1
2
class krill.zone.shared.krillapp.datapoint.DataPointMetaData cannot be cast to class
krill.zone.shared.krillapp.project.diagram.DiagramMetaData

Reachable via ScreenCore.viewExpanded() (the view:<id> demo-control path in main.kt, or the equivalent UI action), which fires from a background thread with no synchronization against the Compose recomposition it triggers.

Root cause

KrillScreenContent() (composeApp/.../startup/KrillScreen.kt) routes the expanded face by the currently-selected node’s type. Its Project.Diagram and Project.Camera branches each read the selected node and hard-cast its meta:

1
val meta = n.meta as DiagramMetaData   // and, in the Camera branch: as CameraMetaData

Every other place in the codebase that reads one of these metadata shapes (DiagramScreen, MiniDiagram, CameraScreen, EditCamera, …) already used a safe cast (as?) with a graceful fallback — these two routing-layer reads were the outliers. A fast switch between two expanded nodes of different types can produce, for one recomposition, a read where the node’s type still routes into the old branch while its meta already reflects the newly-selected node — the exact “torn” shape the hard cast had no defense against. NodeSummaryAndEditor.kt already carried a key(n.id, n.type) wrapper specifically to force full subtree recreation on this kind of switch (its own comment: “force full recreation when switching between different node types”) — the equivalent routing block in KrillScreenContent never got the same treatment.

Fix

Prevention