Snapshot: V4 plugin API and host integration

This commit is contained in:
Sebastian committed 2026-08-12 21:01:39 +02:00
1 parent 448869ac35
commit 29c41a4041
36 files changed
+5463 -253

No files matched your search

+3
View File
@@ -1692,6 +1692,9 @@ pub enum ArrowKey {
#[derive(Debug, Clone)]
pub enum Message {
Tick(Instant),
/// Periodic drain of plugin-to-host requests that arrived outside a host
/// call (e.g. mutations from the Python REPL).
DrainPluginRequests,
/// Web: periodic check for per-script fonts a drawing needs but hasn't
/// fetched yet (#141). Native: never emitted.
PollWebFonts,
+76 -9
View File
@@ -4,16 +4,18 @@ use std::any::Any;
use acadrust::tables::AppId;
use acadrust::xdata::ExtendedDataRecord;
use acadrust::{CadDocument, EntityType, Handle};
use ocs_plugin_api::host::HostApi;
use ocs_plugin_api::host::{CadDocument, EntityType, Handle, HostApi};
use ocs_plugin_api::shm::{DocumentSnapshotStore, DocumentViewData};
#[cfg(not(target_arch = "wasm32"))]
use crate::plugin::v4_support;
use super::OpenCADStudio;
/// Session adapter: one active document tab, command line, undo.
pub(crate) struct HostSession<'a> {
app: &'a mut OpenCADStudio,
tab: usize,
doc_store: Option<ocs_plugin_api::shm::DocumentSnapshotStore>,
doc_store: Option<DocumentSnapshotStore<DocumentViewData>>,
}
impl<'a> HostSession<'a> {
@@ -29,6 +31,20 @@ impl<'a> HostSession<'a> {
self.tab
}
pub fn tab_id(&self) -> u64 {
self.app.tabs[self.tab].id
}
#[cfg(not(target_arch = "wasm32"))]
pub fn document_view_v4(&mut self, tab_id: u64) -> Option<ocs_plugin_api::shm::DocumentViewInfo> {
v4_support::open_document_view_v4(tab_id, self.document())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn close_document_view_v4(&mut self, tab_id: u64) {
v4_support::close_document_view_v4(tab_id);
}
pub fn document(&self) -> &CadDocument {
&self.app.tabs[self.tab].scene.document
}
@@ -38,10 +54,9 @@ impl<'a> HostSession<'a> {
}
pub fn document_view(&mut self) -> Option<ocs_plugin_api::shm::DocumentViewInfo> {
use ocs_plugin_api::shm::DocumentSnapshotStore;
if self.doc_store.is_none() {
let mut store = DocumentSnapshotStore::new(self.tab, 8 * 1024 * 1024).ok()?;
store.publish(self.document()).ok()?;
let mut store = DocumentSnapshotStore::<DocumentViewData>::new(self.tab as u64, 8 * 1024 * 1024).ok()?;
store.publish(&self.document().into()).ok()?;
self.doc_store = Some(store);
}
let store = self.doc_store.as_ref()?;
@@ -53,14 +68,18 @@ impl<'a> HostSession<'a> {
fn publish_document_view(&mut self) {
let doc = &self.app.tabs[self.tab].scene.document;
// Only publish views that have been opened by a consumer. V3 is lazily
// created by document_view(); V4 is tracked per-tab by the V4 manager.
if let Some(store) = self.doc_store.as_mut() {
if let Err(e) = store.publish(doc) {
if let Err(e) = store.publish(&doc.into()) {
eprintln!(
"[host] failed to publish document view for tab {}: {e}",
self.tab
);
}
}
#[cfg(not(target_arch = "wasm32"))]
v4_support::publish_document_view_v4(self.tab_id(), doc);
}
pub fn add_entity(&mut self, entity: EntityType) -> Handle {
@@ -69,6 +88,12 @@ impl<'a> HostSession<'a> {
handle
}
pub fn add_entities(&mut self, entities: Vec<EntityType>) -> Vec<Handle> {
let handles = self.app.tabs[self.tab].scene.add_entities(entities);
self.publish_document_view();
handles
}
pub fn bump_geometry(&mut self) {
self.app.tabs[self.tab].scene.bump_geometry();
}
@@ -239,6 +264,9 @@ impl HostApi for HostSession<'_> {
fn add_entity(&mut self, entity: EntityType) -> Handle {
self.add_entity(entity)
}
fn add_entities(&mut self, entities: Vec<EntityType>) -> Vec<Handle> {
self.add_entities(entities)
}
fn update_entity(&mut self, entity: EntityType) -> bool {
self.update_entity(entity)
}
@@ -305,6 +333,17 @@ impl HostApi for HostSession<'_> {
fn document_view(&mut self) -> Option<ocs_plugin_api::shm::DocumentViewInfo> {
self.document_view()
}
fn tab_id(&self) -> u64 {
self.tab_id()
}
#[cfg(not(target_arch = "wasm32"))]
fn document_view_v4(&mut self, tab_id: u64) -> Option<ocs_plugin_api::shm::DocumentViewInfo> {
self.document_view_v4(tab_id)
}
#[cfg(not(target_arch = "wasm32"))]
fn close_document_view_v4(&mut self, tab_id: u64) {
self.close_document_view_v4(tab_id)
}
}
/// Bridges a plugin's [`InteractiveCommand`](ocs_plugin_api::host::InteractiveCommand)
@@ -506,6 +545,34 @@ mod tests {
);
}
#[test]
fn add_entities_batch_assigns_handles_and_publishes_once() {
let mut app = OpenCADStudio::new_for_test();
app.tabs[0].is_start = false;
let mut host = HostSession::new(&mut app, 0);
let pts: Vec<EntityType> = (0..5)
.map(|i| {
EntityType::Point(Point::at(acadrust::types::Vector3::new(
i as f64, i as f64, 0.0,
)))
})
.collect();
let handles = host.add_entities(pts);
assert_eq!(handles.len(), 5);
assert!(handles.iter().all(|h| !h.is_null()));
// Each handle is unique.
let mut set = std::collections::HashSet::new();
for h in &handles {
assert!(set.insert(h.value()));
}
// All entities are in the document.
for h in &handles {
assert!(host.document().get_entity(*h).is_some());
}
}
#[test]
fn update_entity_replaces_in_place_preserving_handle() {
let mut app = OpenCADStudio::new_for_test();
@@ -704,7 +771,7 @@ mod tests {
let mut host = HostSession::new(&mut app, 0);
let info = host.document_view().unwrap();
let reader =
ocs_plugin_api::shm::SharedDocumentReader::open(std::path::Path::new(&info.path))
ocs_plugin_api::shm::SharedDocumentReader::<ocs_plugin_api::shm::DocumentViewData>::open(std::path::Path::new(&info.path))
.unwrap();
assert_eq!(reader.entity_count(), 0);
@@ -755,7 +822,7 @@ mod tests {
let mut host = HostSession::new(&mut app, 0);
let info = host.document_view().unwrap();
let reader =
ocs_plugin_api::shm::SharedDocumentReader::open(std::path::Path::new(&info.path))
ocs_plugin_api::shm::SharedDocumentReader::<ocs_plugin_api::shm::DocumentViewData>::open(std::path::Path::new(&info.path))
.unwrap();
let h = host.add_entity(EntityType::Point(Point::at(acadrust::types::Vector3::new(
+7
View File
@@ -12,6 +12,8 @@ use crate::modules::ModuleEvent;
use crate::scene::pick::grip::{
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode,
};
#[cfg(not(target_arch = "wasm32"))]
use crate::plugin::v4_support;
use crate::scene::model::object::GripApply;
use crate::scene::{
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
@@ -109,6 +111,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
return self.open_unsaved_dialog_window();
}
let tab_id = self.tabs.get(idx).map(|t| t.id);
// This tab is closing for good — drop its autosave recovery copy.
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(self.autosave_target(idx));
@@ -132,6 +135,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.sync_ribbon_layers();
self.sync_ribbon_styles();
self.sync_ribbon_from_selection();
if let Some(tab_id) = tab_id {
#[cfg(not(target_arch = "wasm32"))]
v4_support::on_tab_closed(tab_id);
}
Task::none()
}
+15
View File
@@ -302,6 +302,21 @@ impl OpenCADStudio {
fn update_inner(&mut self, msg: Message) -> Task<Message> {
match msg {
// Drain plugin-to-host requests that arrived outside of a host call.
// This runs on a periodic timer so long-lived plugin sessions such
// as the Python REPL can mutate the document without requiring a
// user-generated message. Desktop only.
#[cfg(not(target_arch = "wasm32"))]
Message::DrainPluginRequests => {
if self.active_tab < self.tabs.len() {
let mut host = crate::app::plugin_host::HostSession::new(self, self.active_tab);
crate::plugin::external::with_manager(|mgr| {
mgr.drain_requests(&mut host, &mut |_| {});
});
}
Task::none()
}
// Web: fetch every script queued by startup language selection or
// drawing text discovery. Each script has one shared store entry.
Message::PollWebFonts => {
+13
View File
@@ -2094,6 +2094,18 @@ impl OpenCADStudio {
let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal);
#[cfg(target_arch = "wasm32")]
let single_instance = Subscription::none();
// Drain plugin-to-host requests on a timer when any plugin is loaded.
// This lets long-lived plugin sessions (e.g. the Python REPL) mutate the
// host document without requiring a user-generated message.
#[cfg(not(target_arch = "wasm32"))]
let plugin_drain = if crate::plugin::external::with_manager(|mgr| !mgr.ids().is_empty()) {
iced::time::every(std::time::Duration::from_millis(100))
.map(|_| Message::DrainPluginRequests)
} else {
Subscription::none()
};
#[cfg(target_arch = "wasm32")]
let plugin_drain = Subscription::none();
let hatch_pattern_keys = if self.tabs[self.active_tab]
.properties
.hatch_pattern_picker_open
@@ -2111,6 +2123,7 @@ impl OpenCADStudio {
caret_blink,
web_fonts,
autosave,
plugin_drain,
single_instance,
hatch_pattern_keys,
event::listen_with(|ev, status, win_id| {