feat(app): dispatch HostNotifications to native plugins (API v9)

Close the no-load-callback gap: on_notification was a dead dispatch sink —
a plugin had no way to learn that the active document changed, the selection
changed, or that a document opened, so runtime designations could not be
re-seeded after a load.

Dispatch the existing plain variants at the fingerprint-guarded boundaries
the v4 bridge already uses: DocumentChanged (the active tab's settled
geometry epoch) and SelectionChanged (the selection set) leave through
BuiltinPlugin::on_notification at the post-message boundary — after the
mutation settles and pending history scopes closed, never mid-undo or
mid-pick — and at the automation post-op boundary. Plugin-initiated writes
publish immediately after settling, like the drawing-changed event; the
boundary fingerprint guard deduplicates.

Add the additive HostNotification::DocumentOpened { tab_id } variant
(discriminant 10, the counterpart of DocumentTabClosed): fired exactly once
per whole-document install — file open, template new, system open — keyed on
a new per-tab document generation, so an edit can never repeat it and a
reload fires it once. The API version stays v9 (the bump is unreleased).

Write semantics for handlers, tested and documented: a handler may call
HostApi write methods through the same session (set_reference_only at
minimum); its write bumps the epoch, the next boundary notifies once more,
and because re-marking already-designated geometry is a documented no-op —
no epoch bump, no undo step, no publish — the loop terminates. History
(undo/redo) replay itself emits nothing; the boundary is the only channel.
No Python mapping, like the rest of this increment.
This commit is contained in:
Sebastian committed 2026-10-14 21:15:42 +02:00
1 parent 50db12b169
commit 2cdc1df021
11 files changed
+536 -11

No files matched your search

+20 -3
View File
@@ -396,6 +396,15 @@ pub enum HostNotification {
tab_id: u64,
epoch: u64,
},
/// V9 a document became resident in a tab: a whole document replaced the
/// tab's previous content — opened from file, created from a template, or
/// loaded by an automation/system open. The counterpart of
/// [`HostNotification::DocumentTabClosed`]: with both, a plugin can keep
/// per-document state (e.g. runtime designations) in sync from load to
/// close. Discriminant 10.
DocumentOpened {
tab_id: u64,
},
/// Fallback for notification variants added in future minor revisions.
/// Carries the raw bincode payload so an older peer can ignore it without
/// failing deserialization.
@@ -447,6 +456,10 @@ impl Serialize for HostNotification {
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
bincode::serialize_into(&mut bytes, epoch).map_err(serde::ser::Error::custom)?;
}
HostNotification::DocumentOpened { tab_id } => {
bytes.push(10);
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
}
HostNotification::Unknown(raw) => bytes.extend_from_slice(raw),
}
bytes.serialize(serializer)
@@ -490,6 +503,9 @@ impl<'de> Deserialize<'de> for HostNotification {
9 => bincode::deserialize(rest)
.map(|(tab_id, epoch)| HostNotification::DrawingChanged { tab_id, epoch })
.map_err(serde::de::Error::custom),
10 => bincode::deserialize(rest)
.map(|tab_id| HostNotification::DocumentOpened { tab_id })
.map_err(serde::de::Error::custom),
_ => Ok(HostNotification::Unknown(bytes)),
}
}
@@ -1195,9 +1211,10 @@ pub trait HostApi {
/// handle: it is not written to the saved DWG/DXF (the host stores no
/// state of its own in the file), it lives and dies with the drawing tab,
/// and a reloaded drawing starts undesignated — the owning plugin
/// re-marks its entities when the document opens, which makes the
/// in-memory designation fully equivalent to a persisted one for its use
/// case.
/// re-marks its entities when
/// [`on_notification`](BuiltinPlugin::on_notification) delivers
/// [`HostNotification::DocumentOpened`], making the in-memory designation
/// fully equivalent to a persisted one for its use case.
///
/// Fails when the entity does not exist or is on a locked layer (the same
/// policy as the XDATA record methods). A call that would not change the