1use super::EngineState;
3use crate::history::History;
4use brep_plugins::{PackageBundle, PluginPin, Runtime};
5use serde_json::{json, Value};
6use std::collections::VecDeque;
7
8#[derive(Default)]
9pub(super) struct Plugins {
10 packages: Vec<(PackageBundle, bool)>,
11 runtime: Option<Runtime>,
12 revision: u64,
13 next_action: u64,
14 pending: Option<PendingAction>,
15 results: VecDeque<Result<Value, String>>,
16 status: Value,
17 install_pending: Option<PendingInstall>,
18 install_status: Value,
19 deferred_run: bool,
20}
21
22struct PendingInstall {
23 id: u64,
24 revision: u64,
25 packages: Vec<(PackageBundle, bool)>,
26}
27
28fn parse_installations(packages: Value) -> Result<Vec<(PackageBundle, bool)>, String> {
29 let rows = packages
30 .as_array()
31 .ok_or("installed plugins must be an array")?;
32 let mut installed = Vec::new();
33 let mut seen = std::collections::BTreeSet::new();
34 for row in rows {
35 let bundle: PackageBundle = serde_json::from_value(row["bundle"].clone())
36 .map_err(|e| format!("plugin bundle: {e}"))?;
37 let pin = bundle.pin()?;
38 if !seen.insert((pin.id.clone(), pin.digest)) {
39 return Err(format!("duplicate installed package {}", pin.id));
40 }
41 let enabled = row["enabled"]
42 .as_bool()
43 .ok_or("plugin enabled must be boolean")?;
44 installed.push((bundle, enabled));
45 }
46 Ok(installed)
47}
48
49struct PendingAction {
50 id: u64,
51 revision: u64,
52 registry_revision: u64,
53 selection: String,
54 generation: u64,
55 pins: Vec<PluginPin>,
56 staged: Option<History>,
57 notifications: Vec<String>,
58 annotation_id: Option<String>,
59 allowed_annotation_errors: Vec<AnnotationFailure>,
60}
61
62impl Plugins {
63 fn bundles(&self) -> Vec<PackageBundle> {
64 self.packages
65 .iter()
66 .filter(|(_, enabled)| *enabled)
67 .map(|(p, _)| p.clone())
68 .collect()
69 }
70 fn rebuild(&mut self, packages: Vec<(PackageBundle, bool)>) -> Result<(), String> {
71 let enabled = packages
72 .iter()
73 .filter(|(_, enabled)| *enabled)
74 .map(|(p, _)| p.clone())
75 .collect();
76 let runtime = Runtime::new(enabled)?;
77 self.packages = packages;
78 self.runtime = Some(runtime);
79 self.revision = self.revision.wrapping_add(1);
80 Ok(())
81 }
82}
83
84impl EngineState {
85 pub fn restore_plugins(&mut self, packages: Value) -> Result<(), String> {
87 self.plugins.rebuild(parse_installations(packages)?)?;
88 self.plugins_changed();
89 Ok(())
90 }
91
92 pub fn submit_plugin_installations(&mut self, installations: Value) -> Result<u64, String> {
95 if self.plugins.install_pending.is_some() {
96 return Err("plugin installation already pending".into());
97 }
98 let packages = parse_installations(installations)?;
99 self.plugins.next_action += 1;
100 let id = self.plugins.next_action;
101 let enabled: Vec<_> = packages
102 .iter()
103 .filter(|(_, enabled)| *enabled)
104 .map(|(p, _)| p)
105 .collect();
106 if !self
107 .runner
108 .submit_plugin_install(crate::runner::PluginInstallRequest {
109 id,
110 packages: json!(enabled),
111 })
112 {
113 return Err("runner does not support asynchronous plugin installation".into());
114 }
115 self.plugins.install_pending = Some(PendingInstall {
116 id,
117 revision: self.plugins.revision,
118 packages,
119 });
120 self.plugins.install_status = json!({"state":"pending","id":id});
121 self.pump();
122 Ok(id)
123 }
124
125 pub(super) fn defer_plugin_run(&mut self) -> bool {
126 if self.plugins.install_pending.is_none() {
127 return false;
128 }
129 self.plugins.deferred_run = true;
130 self.run_generation += 1;
131 true
132 }
133 pub(super) fn plugin_work_pending(&self) -> bool {
134 self.plugins.install_pending.is_some() || self.plugins.pending.is_some()
135 }
136
137 pub(super) fn plugin_installation_pending(&self) -> bool {
138 self.plugins.install_pending.is_some()
139 }
140
141 pub(super) fn plugin_runner_replaced(&mut self) {
142 if self.plugins.pending.take().is_some() {
143 self.plugin_result(Err(
144 "plugin transaction cancelled because runner changed".into()
145 ));
146 }
147 if let Some(pending) = &self.plugins.install_pending {
148 let enabled: Vec<_> = pending
149 .packages
150 .iter()
151 .filter(|(_, enabled)| *enabled)
152 .map(|(p, _)| p)
153 .collect();
154 if !self
155 .runner
156 .submit_plugin_install(crate::runner::PluginInstallRequest {
157 id: pending.id,
158 packages: json!(enabled),
159 })
160 {
161 self.plugins.install_pending = None;
162 self.plugins.install_status =
163 json!({"state":"error","error":"replacement runner cannot validate plugins"});
164 }
165 }
166 }
167
168 pub fn plugin_installation_status(&self) -> Value {
169 self.plugins.install_status.clone()
170 }
171
172 pub(super) fn pump_plugin_installations(&mut self) {
173 while let Some(reply) = self.runner.poll_plugin_install() {
174 if !self
175 .plugins
176 .install_pending
177 .as_ref()
178 .is_some_and(|p| p.id == reply.id)
179 {
180 continue;
181 }
182 let pending = self.plugins.install_pending.take().unwrap();
183 let result = if pending.revision != self.plugins.revision {
184 Err("plugin installations changed during validation".to_string())
185 } else {
186 reply.result.and_then(|registry| {
187 let enabled = pending
188 .packages
189 .iter()
190 .filter(|(_, enabled)| *enabled)
191 .map(|(p, _)| p.clone())
192 .collect();
193 Runtime::from_validated(enabled, registry)
194 })
195 };
196 let deferred = std::mem::take(&mut self.plugins.deferred_run);
197 match result {
198 Ok(runtime) => {
199 self.plugins.runtime = Some(runtime);
200 self.plugins.packages = pending.packages;
201 self.plugins.revision = self.plugins.revision.wrapping_add(1);
202 self.plugins.install_status = json!({"state":"complete","id":pending.id});
203 self.plugins_changed();
204 }
205 Err(error) => {
206 self.plugins.install_status =
207 json!({"state":"error","id":pending.id,"error":error});
208 if deferred {
209 self.rerun_history();
210 }
211 }
212 }
213 }
214 }
215
216 pub fn set_plugin_packages(&mut self, packages: Vec<PackageBundle>) -> Result<(), String> {
218 self.plugins
219 .rebuild(packages.into_iter().map(|p| (p, true)).collect())?;
220 self.plugins_changed();
221 Ok(())
222 }
223
224 pub fn install_plugin(&mut self, package: Value) -> Result<Value, String> {
225 let package: PackageBundle =
226 serde_json::from_value(package).map_err(|e| format!("plugin package: {e}"))?;
227 let pin = package.pin()?;
228 let mut packages = self.plugins.packages.clone();
229 packages.retain(|(p, _)| p.pin().ok().as_ref() != Some(&pin));
230 packages.push((package, true));
231 self.plugins.rebuild(packages)?;
232 self.plugins_changed();
233 Ok(json!(pin))
234 }
235
236 pub fn installed_plugins(&self) -> Value {
237 Value::Array(
238 self.plugins
239 .packages
240 .iter()
241 .filter_map(|(p, enabled)| {
242 p.pin().ok().map(|pin| {
243 json!({"id":pin.id,"version":pin.version,"apiVersion":pin.api_version,
244 "digest":pin.digest,"name":p.manifest.name,"enabled":enabled})
245 })
246 })
247 .collect(),
248 )
249 }
250
251 pub fn export_plugin(&self, id: &str, digest: &str) -> Result<Value, String> {
252 let (package, _) = self
253 .plugins
254 .packages
255 .iter()
256 .find(|(p, _)| {
257 p.pin()
258 .is_ok_and(|pin| pin.id == id && pin.digest == digest)
259 })
260 .ok_or_else(|| format!("plugin {id} at {digest} is not installed"))?;
261 serde_json::to_value(package).map_err(|e| e.to_string())
262 }
263
264 pub fn enable_plugin(&mut self, id: &str, digest: &str, enabled: bool) -> Result<(), String> {
265 let mut packages = self.plugins.packages.clone();
266 let (_, state) = packages
267 .iter_mut()
268 .find(|(p, _)| {
269 p.pin()
270 .is_ok_and(|pin| pin.id == id && pin.digest == digest)
271 })
272 .ok_or_else(|| format!("plugin {id} at {digest} is not installed"))?;
273 *state = enabled;
274 self.plugins.rebuild(packages)?;
275 self.plugins_changed();
276 Ok(())
277 }
278
279 pub fn remove_plugin(&mut self, id: &str, digest: &str) -> Result<(), String> {
280 let mut packages = self.plugins.packages.clone();
281 let before = packages.len();
282 packages.retain(|(p, _)| {
283 !p.pin()
284 .is_ok_and(|pin| pin.id == id && pin.digest == digest)
285 });
286 if packages.len() == before {
287 return Err(format!("plugin {id} at {digest} is not installed"));
288 }
289 self.plugins.rebuild(packages)?;
290 self.plugins_changed();
291 Ok(())
292 }
293
294 fn plugins_changed(&mut self) {
295 self.cancel_plugin_action();
296 self.runner.reset();
297 self.rerun_history();
298 }
299
300 fn saved_plugin_pins(&self) -> Result<Vec<PluginPin>, String> {
301 serde_json::from_value(
302 self.history
303 .document_block("plugins")
304 .cloned()
305 .unwrap_or_else(|| json!([])),
306 )
307 .map_err(|e| format!("document plugin pins: {e}"))
308 }
309
310 fn available_plugin_pins(&self) -> Result<Vec<PluginPin>, String> {
312 let mut pins = self.saved_plugin_pins()?;
313 for (package, enabled) in self.plugins.packages.iter().rev() {
314 if !enabled {
315 continue;
316 }
317 let pin = package.pin()?;
318 if !pins.iter().any(|p| p.id == pin.id) {
319 pins.push(pin);
320 }
321 }
322 Ok(pins)
323 }
324
325 pub fn plugin_catalogue(&self) -> Value {
326 let mut features = Vec::new();
327 let mut actions = Vec::new();
328 let mut workbenches = Vec::new();
329 let mut panels = Vec::new();
330 let mut annotations = Vec::new();
331 let mut errors = Vec::new();
332 if let Some(runtime) = &self.plugins.runtime {
333 match self.available_plugin_pins() {
334 Ok(pins) => {
335 for pin in pins {
336 if self.installed_pin(&pin.id, &pin.digest).as_ref() != Ok(&pin) {
337 errors.push(format!(
338 "plugin {} at {} is missing or disabled",
339 pin.id, pin.digest
340 ));
341 continue;
342 }
343 match runtime.registry().for_document(&[pin]) {
345 Ok(registry) => {
346 features.extend(registry.features().iter().map(|f| {
347 let mut schema = f.input_params_schema.clone();
348 schema["id"] = json!({"type":"string","default_value":null});
349 json!({"type":f.id,"id":f.id,"label":f.label,"longName":f.label,"ribbonPath":f.ribbon_path,"commandSize":f.command_size,"icon":f.glyph,
350 "shortName":if f.short_name.is_empty() { f.id.rsplit('/').next().unwrap_or("Plugin") } else { &f.short_name },
351 "inputParamsSchema":schema,"package":f.package})
352 }));
353 panels.extend(registry.panels().iter().map(|p| json!(p)));
354 annotations.extend(registry.annotations().iter().map(|a| {
355 let mut schema = a.input_params_schema.clone();
356 schema["id"] = json!({"type":"string","default_value":null});
357 json!({"type":a.id,"id":a.id,"label":a.label,"longName":a.label,
358 "shortName":"ANN","inputParamsSchema":schema,"package":a.package})
359 }));
360 actions.extend(registry.actions().iter().map(|a| json!(a)));
361 workbenches.extend(registry.workbenches().iter().map(|w| json!(w)));
362 }
363 Err(error) => errors.push(error),
364 }
365 }
366 }
367 Err(error) => errors.push(error),
368 }
369 } else if let Ok(pins) = self.saved_plugin_pins() {
370 errors.extend(pins.iter().map(|p| {
371 format!(
372 "plugin {} at {} is not installed or enabled",
373 p.id, p.digest
374 )
375 }));
376 }
377 json!({"revision":self.plugins.revision,"documentRevision":self.history.revision(),
378 "features":features,"actions":actions,"workbenches":workbenches,"panels":panels,"annotations":annotations,"errors":errors})
379 }
380
381 pub fn plugin_workbenches(&self) -> Value {
382 self.plugin_catalogue()["workbenches"].clone()
383 }
384
385 pub fn feature_catalogue(&self) -> Value {
387 let mut catalogue = crate::features::feature_catalogue();
388 if let Some(features) = catalogue["features"].as_array_mut() {
389 features.extend(
390 self.plugin_catalogue()["features"]
391 .as_array()
392 .into_iter()
393 .flatten()
394 .cloned(),
395 );
396 }
397 catalogue
398 }
399 pub fn feature_schema(&self, feature_type: &str) -> Option<Value> {
400 crate::features::feature_schema(feature_type).or_else(|| {
401 self.plugin_catalogue()["features"]
402 .as_array()?
403 .iter()
404 .find(|f| f["type"] == feature_type)
405 .cloned()
406 })
407 }
408 pub fn feature_form_fields(&self, feature_type: &str) -> Vec<crate::style::FormField> {
409 if let Some(schema) = crate::features::feature_schema(feature_type) {
410 return crate::features::form_fields_from_schema(&schema);
411 }
412 self.feature_schema(feature_type)
413 .as_ref()
414 .map(crate::features::plugin_form_fields_from_schema)
415 .unwrap_or_default()
416 }
417 pub fn feature_default_params(&self, feature_type: &str) -> Value {
418 let mut params = serde_json::Map::new();
419 if let Some(schema) = self.feature_schema(feature_type) {
420 if let Some(props) = schema["inputParamsSchema"].as_object() {
421 for (name, spec) in props {
422 params.insert(name.clone(), spec["default_value"].clone());
423 }
424 }
425 }
426 Value::Object(params)
427 }
428
429 pub fn select_plugin(&mut self, id: &str, digest: &str) -> Result<(), String> {
431 self.require_plugin_editable()?;
432 let pin = self.installed_pin(id, digest)?;
433 let mut pins = self.saved_plugin_pins()?;
434 if let Some(old) = pins.iter().find(|p| p.id == id) {
435 return if old == &pin {
436 Ok(())
437 } else {
438 Err("use migrate_plugin to validate a version change".into())
439 };
440 }
441 pins.push(pin);
442 self.history
443 .set_document_block("plugins", Some(json!(pins)), None);
444 self.rerun_history();
445 Ok(())
446 }
447
448 fn installed_pin(&self, id: &str, digest: &str) -> Result<PluginPin, String> {
449 self.plugins
450 .packages
451 .iter()
452 .filter(|(_, enabled)| *enabled)
453 .find_map(|(p, _)| {
454 p.pin()
455 .ok()
456 .filter(|pin| pin.id == id && pin.digest == digest)
457 })
458 .ok_or_else(|| format!("plugin {id} at {digest} is not installed and enabled"))
459 }
460 fn require_plugin_editable(&self) -> Result<(), String> {
461 if self.plugins.install_pending.is_some() {
462 return Err("wait for plugin installation validation".into());
463 }
464 if let Some(reason) = self.history.locked() {
465 return Err(format!("the document is read-only: {reason}"));
466 }
467 if self.plugins.pending.is_some() {
468 return Err("a plugin transaction is already pending".into());
469 }
470 if self.run_pending() {
471 return Err("wait for the current history recompute".into());
472 }
473 Ok(())
474 }
475
476 pub fn migrate_plugin(&mut self, id: &str, digest: &str) -> Result<u64, String> {
477 self.require_plugin_editable()?;
478 let pin = self.installed_pin(id, digest)?;
479 let mut pins = self.saved_plugin_pins()?;
480 pins.retain(|p| p.id != id);
481 pins.push(pin);
482 let pending = self.new_plugin_pending(pins.clone());
483 let action_id = pending.id;
484 let mut staged = self.history.clone();
485 staged.set_document_block_no_undo("plugins", Some(json!(pins)));
486 self.plugins.pending = Some(pending);
487 if let Err(error) = self.submit_plugin_candidate(staged) {
488 self.plugins.pending = None;
489 return Err(error);
490 }
491 self.pump();
492 Ok(action_id)
493 }
494
495 fn new_plugin_pending(&mut self, pins: Vec<PluginPin>) -> PendingAction {
496 self.plugins.next_action += 1;
497 PendingAction {
498 id: self.plugins.next_action,
499 revision: self.history.revision(),
500 registry_revision: self.plugins.revision,
501 selection: self.selection_json(),
502 generation: self.run_generation,
503 pins,
504 staged: None,
505 notifications: Vec::new(),
506 annotation_id: None,
507 allowed_annotation_errors: Vec::new(),
508 }
509 }
510
511 pub fn submit_plugin_action(&mut self, id: &str, params: Value) -> Result<(), String> {
512 let selection = serde_json::from_str(&self.selection_json()).unwrap_or(Value::Null);
513 self.plugin_action(id, params, selection).map(|_| ())
514 }
515
516 pub fn plugin_action(
518 &mut self,
519 id: &str,
520 params: Value,
521 selection: Value,
522 ) -> Result<u64, String> {
523 self.require_plugin_editable()?;
524 let pins = self.available_plugin_pins()?;
525 let runtime = self
526 .plugins
527 .runtime
528 .as_ref()
529 .ok_or("no plugins installed")?;
530 let registry = runtime.registry().for_document(&pins)?;
531 if !registry.actions().iter().any(|a| a.id == id) {
532 return Err(format!("plugin action {id} is unavailable"));
533 }
534 let pending = self.new_plugin_pending(pins.clone());
535 let action_id = pending.id;
536 let input = json!({"params":params,"selection":selection,
537 "document":serde_json::from_str::<Value>(&self.history.request_json()).map_err(|e| e.to_string())?});
538 self.sync_plugin_runner();
539 let request = crate::runner::PluginActionRequest {
540 id: action_id,
541 pins,
542 action: id.into(),
543 input,
544 script: None,
545 };
546 if !self.runner.submit_plugin_action(request) {
547 return Err("the installed runner does not support plugin actions".into());
548 }
549 self.plugins.pending = Some(pending);
550 self.plugins.status = json!({"state":"pending","id":action_id});
551 self.pump();
552 Ok(action_id)
553 }
554
555 pub fn plugin_action_status(&self) -> Value {
556 self.plugins.status.clone()
557 }
558
559 pub fn run_javascript(&mut self, source: &str) -> Result<u64, String> {
562 self.require_plugin_editable()?;
563 if source.len() > 1024 * 1024 {
564 return Err("script exceeds 1 MiB".into());
565 }
566 let pins = self.available_plugin_pins()?;
567 let pending = self.new_plugin_pending(pins.clone());
568 let id = pending.id;
569 let input = json!({"params":{},
570 "selection":serde_json::from_str::<Value>(&self.selection_json()).unwrap_or(Value::Null),
571 "document":serde_json::from_str::<Value>(&self.history.request_json()).map_err(|e| e.to_string())?});
572 self.sync_plugin_runner();
573 let request = crate::runner::PluginActionRequest {
574 id, pins, action: String::new(), input, script: Some(source.into()),
575 };
576 if !self.runner.submit_plugin_action(request) {
577 return Err("the installed runner does not support JavaScript execution".into());
578 }
579 self.plugins.pending = Some(pending);
580 self.plugins.status = json!({"state":"pending","id":id});
581 self.pump();
582 Ok(id)
583 }
584 pub fn poll_plugin_action(&mut self) -> Option<Result<Value, String>> {
585 self.pump();
586 self.plugins.results.pop_front()
587 }
588 pub fn cancel_plugin_action(&mut self) -> bool {
589 if self.plugins.pending.take().is_none() {
590 return false;
591 }
592 self.plugin_result(Err("plugin transaction cancelled".into()));
593 self.run_generation += 1;
595 self.applied_generation = self.run_generation;
596 self.runner.reset();
597 self.rerun_history();
598 true
599 }
600
601 pub(super) fn sync_plugin_runner(&mut self) {
602 let plugins = &self.plugins;
603 self.runner
604 .sync_plugins(plugins.revision, &mut || json!(plugins.bundles()));
605 }
606
607 fn plugin_result(&mut self, result: Result<Value, String>) {
608 self.plugins.status = match &result {
609 Ok(value) => json!({"state":"complete","result":value}),
610 Err(error) => json!({"state":"error","error":error}),
611 };
612 self.plugins.results.push_back(result);
613 }
614
615 fn plugin_pending_valid(&self, pending: &PendingAction) -> bool {
616 pending.revision == self.history.revision()
617 && pending.registry_revision == self.plugins.revision
618 && pending.selection == self.selection_json()
619 && pending.generation == self.run_generation
620 && self.history.locked().is_none()
621 }
622
623 pub(super) fn pump_plugin_callbacks(&mut self) {
624 if self
625 .plugins
626 .pending
627 .as_ref()
628 .is_some_and(|p| !self.plugin_pending_valid(p))
629 {
630 let staged = self.plugins.pending.take().unwrap().staged.is_some();
631 self.plugin_result(Err(
632 "plugin transaction invalidated by a document or selection change".into(),
633 ));
634 if staged {
635 self.runner.reset();
636 self.rerun_history();
637 }
638 }
639 while let Some(reply) = self.runner.poll_plugin_action() {
640 let Some(pending) = self.plugins.pending.as_ref() else {
641 continue;
642 };
643 if pending.id != reply.id {
644 continue;
645 }
646 if !self.plugin_pending_valid(pending) {
647 self.plugins.pending = None;
648 self.plugin_result(Err(
649 "plugin transaction invalidated by a document or selection change".into(),
650 ));
651 continue;
652 }
653 if let Ok(plan) = &reply.result {
654 if plan.commands.is_empty() {
655 self.plugins.pending = None;
656 self.plugin_result(Ok(
657 json!({"id":reply.id,"notifications":plan.notifications}),
658 ));
659 continue;
660 }
661 }
662 let staged = reply.result.and_then(|plan| {
663 let mut staged = self.history.clone();
664 let pending = self.plugins.pending.as_mut().unwrap();
665 staged.set_document_block_no_undo("plugins", Some(json!(pending.pins)));
666 pending.notifications = plan.notifications;
667 let commands = serde_json::to_value(plan.commands).map_err(|e| e.to_string())?;
669 for command in commands.as_array().ok_or("invalid action commands")? {
670 match command["op"].as_str().unwrap_or("") {
671 "addFeature" => {
672 let ty = command["featureType"].as_str().ok_or("addFeature requires featureType")?;
673 let schema = self.feature_schema(ty).ok_or_else(|| format!("unknown feature type {ty}"))?;
674 let mut params = self.feature_default_params(ty);
675 let supplied = command["inputParams"].as_object().ok_or("feature parameters must be an object")?;
676 params.as_object_mut().unwrap().extend(supplied.clone());
677 let id = staged.next_feature_id(schema["shortName"].as_str().unwrap_or(ty));
678 params["id"] = json!(id);
679 staged.push_feature(json!({"type":ty,"inputParams":params,"persistentData":command["persistentData"]}));
680 }
681 "updateFeature" => {
682 let id = command["id"].as_str().ok_or("updateFeature requires id")?;
683 let index = staged.index_of(id).ok_or_else(|| format!("feature {id} not found"))?;
684 let mut params = command["inputParams"].clone();
685 if !params.is_object() { return Err("feature parameters must be an object".into()); }
686 params["id"] = json!(id);
687 staged.set_feature_params(index, params);
688 }
689 "deleteFeature" => {
690 let id = command["id"].as_str().ok_or("deleteFeature requires id")?;
691 let index = staged.index_of(id).ok_or_else(|| format!("feature {id} not found"))?;
692 staged.remove_feature(index);
693 }
694 op => return Err(format!("unsupported plugin document command {op}")),
695 }
696 }
697 Ok(staged)
698 });
699 match staged.and_then(|staged| self.submit_plugin_candidate(staged)) {
700 Ok(()) => {}
701 Err(error) => {
702 self.plugins.pending = None;
703 self.plugin_result(Err(error));
704 }
705 }
706 }
707 }
708
709 fn submit_plugin_candidate(&mut self, mut staged: History) -> Result<(), String> {
710 staged.set_rollback(staged.len().saturating_sub(1));
711 let mut request: brep_kernel::HistoryRequest =
712 serde_json::from_value(staged.prefix_request()).map_err(|e| e.to_string())?;
713 request.display_lod = self.settings.lod_factor;
714 self.run_generation += 1;
715 let pending = self
716 .plugins
717 .pending
718 .as_mut()
719 .ok_or("plugin transaction disappeared")?;
720 pending.generation = self.run_generation;
721 pending.staged = Some(staged);
722 self.plugins.status = json!({"state":"pending","id":pending.id});
723 self.runner.reset();
725 self.sync_plugin_runner();
726 self.runner.sync_parts_library(
727 brep_kernel::parts_library_revision(),
728 &mut brep_kernel::parts_library_map,
729 );
730 self.runner.submit_run(request, self.run_generation);
731 Ok(())
732 }
733
734 pub(super) fn accept_plugin_run(&mut self, reply: &crate::runner::RunReply) -> bool {
736 let Some(pending) = self.plugins.pending.as_ref() else {
737 return true;
738 };
739 if pending.staged.is_none() || pending.generation != reply.generation {
740 return true;
741 }
742 let valid = self.plugin_pending_valid(pending);
743 let mut pending = self.plugins.pending.take().unwrap();
744 let report = &reply.output.report;
745 let annotation_errors: Vec<_> = annotation_failures(report.pmi.as_ref())
746 .into_iter()
747 .filter(|failure| !pending.allowed_annotation_errors.contains(failure))
748 .map(|failure| format!("{}: {}", failure.id, failure.message))
749 .collect();
750 let result = if !valid {
751 Err("plugin transaction invalidated by a document or selection change".into())
752 } else if !report.feature_errors.is_empty()
753 || !report.unresolved.is_empty()
754 || !report.display_errors.is_empty()
755 || !annotation_errors.is_empty()
756 {
757 Err(format!(
758 "plugin transaction recompute failed: {}",
759 report
760 .feature_errors
761 .iter()
762 .chain(&report.unresolved)
763 .chain(&report.display_errors)
764 .chain(&annotation_errors)
765 .cloned()
766 .collect::<Vec<_>>()
767 .join("; ")
768 ))
769 } else {
770 fold_annotation_data(pending.staged.as_mut().unwrap(), report.pmi.as_ref());
771 self.history
772 .commit_staged(pending.staged.as_ref().unwrap())
773 .map(|_| json!({"id":pending.id,"notifications":pending.notifications,"annotationId":pending.annotation_id}))
774 };
775 let accepted = result.is_ok();
776 self.plugin_result(result);
777 if !accepted {
778 self.runner.reset();
779 self.rerun_history();
780 }
781 accepted
782 }
783
784 pub(super) fn append_plugin_features(&mut self, features: &[Value]) -> Result<bool, String> {
786 if !features
787 .iter()
788 .any(|f| f["type"].as_str().is_some_and(|t| t.contains('/')))
789 {
790 return Ok(false);
791 }
792 if let Some(reason) = self.history.locked() {
793 return Err(format!("the document is read-only: {reason}"));
794 }
795 let mut pins = self.saved_plugin_pins()?;
796 for feature in features {
797 let ty = feature["type"].as_str().unwrap_or("");
798 if !ty.contains('/') {
799 continue;
800 }
801 let schema = self
802 .feature_schema(ty)
803 .ok_or_else(|| format!("plugin feature {ty} is unavailable"))?;
804 let pin: PluginPin =
805 serde_json::from_value(schema["package"].clone()).map_err(|e| e.to_string())?;
806 if !pins.iter().any(|p| p.id == pin.id) {
807 pins.push(pin);
808 }
809 }
810 let mut staged = self.history.clone();
811 staged.set_document_block_no_undo("plugins", Some(json!(pins)));
812 staged.push_features(features.to_vec());
813 staged.set_rollback(staged.len().saturating_sub(1));
814 self.history.commit_staged(&staged)?;
815 Ok(true)
816 }
817
818 pub(super) fn plugin_export_ready(&self) -> Result<(), String> {
819 fn uses_plugins(value: &Value) -> bool {
820 match value {
821 Value::Object(map) => {
822 map.get("plugins")
823 .and_then(Value::as_array)
824 .is_some_and(|p| !p.is_empty())
825 || map
826 .get("type")
827 .and_then(Value::as_str)
828 .is_some_and(|ty| ty.contains('/'))
829 || map.values().any(uses_plugins)
830 }
831 Value::Array(values) => values.iter().any(uses_plugins),
832 _ => false,
833 }
834 }
835 let document: Value =
836 serde_json::from_str(&self.history.request_json()).map_err(|e| e.to_string())?;
837 if !uses_plugins(&document) {
838 return Ok(());
839 }
840 if self.run_pending()
841 || self.plugins.pending.is_some()
842 || self.plugins.install_pending.is_some()
843 {
844 return Err("plugin geometry is pending; wait for recompute before export".into());
845 }
846 let annotation_errors = plugin_annotation_errors(self.pmi_report.as_ref());
847 if !annotation_errors.is_empty() {
848 return Err(format!(
849 "unavailable plugin annotations: {}",
850 annotation_errors.join("; ")
851 ));
852 }
853 let report: Value = serde_json::from_str(&self.history_report).unwrap_or(Value::Null);
854 if report.get("error").is_some()
855 || ["featureErrors", "unresolved", "displayErrors"]
856 .iter()
857 .any(|key| {
858 report[*key]
859 .as_array()
860 .is_some_and(|items| !items.is_empty())
861 })
862 {
863 return Err("plugin document has failed or unavailable geometry; recompute successfully before export".into());
864 }
865 Ok(())
866 }
867
868 pub(super) fn plugin_resident_handles(
869 &self,
870 request: &brep_kernel::HistoryRequest,
871 ) -> Result<Vec<(String, u32)>, String> {
872 let result = self.execute_plugin_history(request);
873 let mut names = Vec::new();
874 let mut handles = std::collections::HashMap::new();
875 for feature in result.results {
876 if let Some(error) = feature.error {
877 return Err(format!("{}: {error}", feature.id));
878 }
879 for removed in feature.removed {
880 handles.remove(&removed);
881 names.retain(|n| n != &removed);
882 }
883 for added in feature.added {
884 if handles.insert(added.name.clone(), added.handle).is_none() {
885 names.push(added.name);
886 }
887 }
888 }
889 Ok(names
890 .into_iter()
891 .map(|name| {
892 let handle = handles[&name];
893 (name, handle)
894 })
895 .collect())
896 }
897
898 pub(super) fn inherit_plugins(&mut self, parent: &Self) {
899 self.plugins.packages = parent.plugins.packages.clone();
900 self.plugins.runtime = parent.plugins.runtime.clone();
901 self.plugins.revision = parent.plugins.revision;
902 }
903
904 pub(super) fn with_plugin_provider<R>(&self, f: impl FnOnce() -> R) -> R {
905 match &self.plugins.runtime {
906 Some(runtime) => runtime.with_provider(f),
907 None => f(),
908 }
909 }
910
911 pub(super) fn execute_plugin_history(
912 &self,
913 request: &brep_kernel::HistoryRequest,
914 ) -> brep_kernel::HistoryResult {
915 match &self.plugins.runtime {
916 Some(runtime) => runtime.execute_history(request),
917 None => brep_kernel::execute_history(request),
918 }
919 }
920
921 pub(super) fn run_plugin_scene(
922 &mut self,
923 request: &brep_kernel::HistoryRequest,
924 ) -> Result<crate::pipeline::SceneBuildReport, String> {
925 match &self.plugins.runtime {
926 Some(runtime) => runtime.with_provider(|| {
927 crate::pipeline::update_scene_from_history(&mut self.scene, request)
928 }),
929 None => crate::pipeline::update_scene_from_history(&mut self.scene, request),
930 }
931 }
932}
933
934
935#[derive(Clone, Debug, PartialEq, Eq)]
937struct AnnotationFailure {
938 view_id: String,
939 id: String,
940 kind: String,
941 message: String,
942}
943
944fn annotation_failures(report: Option<&brep_kernel::PmiReport>) -> Vec<AnnotationFailure> {
945 report
946 .into_iter()
947 .flat_map(|r| &r.views)
948 .flat_map(|view| {
949 view.annotations
950 .iter()
951 .filter(|a| {
952 a.enabled && a.kind.contains('/') && a.status == brep_kernel::PmiStatus::Error
953 })
954 .map(|a| AnnotationFailure {
955 view_id: view.id.clone(),
956 id: a.id.clone(),
957 kind: a.kind.clone(),
958 message: a.message.clone(),
959 })
960 })
961 .collect()
962}
963
964pub(super) fn plugin_annotation_errors(report: Option<&brep_kernel::PmiReport>) -> Vec<String> {
966 annotation_failures(report)
967 .into_iter()
968 .map(|a| format!("{}: {}", a.id, a.message))
969 .collect()
970}
971
972fn fold_annotation_data(history: &mut History, report: Option<&brep_kernel::PmiReport>) {
973 let Some(block) = history.pmi_block() else {
974 return;
975 };
976 let Ok(mut state) = serde_json::from_value::<brep_kernel::PmiState>(block.clone()) else {
977 return;
978 };
979 let mut changed = false;
980 for row in report
981 .into_iter()
982 .flat_map(|r| &r.views)
983 .flat_map(|v| &v.annotations)
984 {
985 if row.status != brep_kernel::PmiStatus::Ok || !row.kind.contains('/') {
986 continue;
987 }
988 if let (Some(data), Some(annotation)) =
989 (&row.persistent_data, state.find_annotation_mut(&row.id))
990 {
991 if annotation.kind == row.kind
992 && (annotation.persistent_data != *data
993 || annotation.plugin_replay != row.plugin_replay)
994 {
995 annotation.persistent_data = data.clone();
996 annotation.plugin_replay = row.plugin_replay.clone();
997 changed = true;
998 }
999 }
1000 }
1001 if changed {
1002 history.set_pmi_block_no_undo(Some(json!(state)));
1003 }
1004}
1005
1006impl EngineState {
1007 pub fn plugin_panels(&self) -> Value {
1008 self.plugin_catalogue()["panels"].clone()
1009 }
1010
1011 pub fn plugin_annotation_catalogue(&self) -> Value {
1012 self.plugin_catalogue()["annotations"].clone()
1013 }
1014
1015 pub fn annotation_schema(&self, kind: &str) -> Option<Value> {
1017 brep_kernel::pmi_type(kind)
1018 .map(|d| (d.schema)())
1019 .or_else(|| {
1020 self.plugin_annotation_catalogue()
1021 .as_array()?
1022 .iter()
1023 .find(|a| a["type"] == kind)
1024 .cloned()
1025 })
1026 }
1027
1028 pub(super) fn fold_plugin_annotation_data(&mut self) {
1029 if self.expression_preview.is_none() {
1030 fold_annotation_data(&mut self.history, self.pmi_report.as_ref());
1031 }
1032 }
1033
1034 pub fn plugin_add_annotation(
1035 &mut self,
1036 view_id: &str,
1037 kind: &str,
1038 mut params: Value,
1039 ) -> Result<u64, String> {
1040 self.require_plugin_editable()?;
1041 let schema = self
1042 .plugin_annotation_catalogue()
1043 .as_array()
1044 .into_iter()
1045 .flatten()
1046 .find(|s| s["type"] == kind)
1047 .cloned()
1048 .ok_or_else(|| format!("unavailable plugin annotation {kind}"))?;
1049 if !params.is_object() {
1050 return Err("annotation parameters must be an object".into());
1051 }
1052 let mut state = self.pmi_state();
1053 let id = state.next_id("ANN");
1054 params["id"] = json!(id);
1055 state
1056 .find_view_mut(view_id)
1057 .ok_or_else(|| format!("no PMI view '{view_id}'"))?
1058 .annotations
1059 .push(brep_kernel::PmiAnnotation {
1060 kind: kind.into(),
1061 enabled: true,
1062 params,
1063 persistent_data: Value::Null,
1064 plugin_replay: None,
1065 label_world: None,
1066 });
1067 let pin: PluginPin =
1068 serde_json::from_value(schema["package"].clone()).map_err(|e| e.to_string())?;
1069 let mut pins = self.saved_plugin_pins()?;
1070 if let Some(old) = pins.iter().find(|p| p.id == pin.id) {
1071 if old != &pin {
1072 return Err("annotation package does not match document pin".into());
1073 }
1074 } else {
1075 pins.push(pin);
1076 }
1077 self.submit_annotation_state(state, pins, id, false)
1078 }
1079
1080 pub fn plugin_update_annotation(&mut self, id: &str, mut params: Value) -> Result<u64, String> {
1081 self.require_plugin_editable()?;
1082 if !params.is_object() {
1083 return Err("annotation parameters must be an object".into());
1084 }
1085 let mut state = self.pmi_state();
1086 let annotation = state
1087 .find_annotation_mut(id)
1088 .ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1089 if !annotation.kind.contains('/') {
1090 return Err("annotation is not plugin-owned".into());
1091 }
1092 params["id"] = json!(id);
1093 annotation.params = params;
1094 self.submit_annotation_state(state, self.saved_plugin_pins()?, id.into(), false)
1095 }
1096
1097 pub fn plugin_delete_annotation(&mut self, id: &str) -> Result<u64, String> {
1098 self.require_plugin_editable()?;
1099 let mut state = self.pmi_state();
1100 let (v, a) = state
1101 .locate_annotation(id)
1102 .ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1103 if !state.views[v].annotations[a].kind.contains('/') {
1104 return Err("annotation is not plugin-owned".into());
1105 }
1106 state.views[v].annotations.remove(a);
1107 self.submit_annotation_state(state, self.saved_plugin_pins()?, id.into(), true)
1108 }
1109
1110 pub fn plugin_set_annotation_enabled(
1111 &mut self,
1112 id: &str,
1113 enabled: bool,
1114 ) -> Result<u64, String> {
1115 self.require_plugin_editable()?;
1116 let mut state = self.pmi_state();
1117 let annotation = state
1118 .find_annotation_mut(id)
1119 .ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1120 if !annotation.kind.contains('/') {
1121 return Err("annotation is not plugin-owned".into());
1122 }
1123 annotation.enabled = enabled;
1124 self.submit_annotation_state(state, self.saved_plugin_pins()?, id.into(), !enabled)
1125 }
1126
1127 fn submit_annotation_state(
1128 &mut self,
1129 state: brep_kernel::PmiState,
1130 pins: Vec<PluginPin>,
1131 annotation_id: String,
1132 repair: bool,
1133 ) -> Result<u64, String> {
1134 let mut pending = self.new_plugin_pending(pins.clone());
1135 if repair {
1136 let baseline = self.pmi_state();
1137 pending.allowed_annotation_errors = annotation_failures(self.pmi_report.as_ref())
1138 .into_iter()
1139 .filter(|failure| {
1140 let old = baseline
1141 .find_view(&failure.view_id)
1142 .and_then(|v| v.annotations.iter().find(|a| a.id() == failure.id));
1143 let survivor = state
1144 .find_view(&failure.view_id)
1145 .and_then(|v| v.annotations.iter().find(|a| a.id() == failure.id));
1146 old.is_some() && old == survivor
1147 })
1148 .collect();
1149 }
1150 let id = pending.id;
1151 pending.annotation_id = Some(annotation_id);
1152 let mut staged = self.history.clone();
1153 staged.set_pmi_block_no_undo(Some(json!(state)));
1154 staged.set_document_block_no_undo("plugins", Some(json!(pins)));
1155 self.plugins.pending = Some(pending);
1156 if let Err(error) = self.submit_plugin_candidate(staged) {
1157 self.plugins.pending = None;
1158 return Err(error);
1159 }
1160 self.pump();
1161 Ok(id)
1162 }
1163}