Commit graph cad-editor/crates/ocs_plugin_api
Author SHA1 Message Date
Hakan Seven
77c0c5a9d0 feat(plugin): surface loaded plugin commands in command-line autocomplete
Autocomplete suggestions came only from all_registered_command_names() —
the compile-time inventory registry returning &'static str — so
runtime-loaded plugin commands could never appear. Typing a plugin's
command prefix (e.g. `LS_`) showed nothing even with the plugin loaded
and its ribbon tab active; dispatch worked, only discovery was blind.

Collect each enabled plugin's ribbon ToolDef command ids plus its
manifest command_prefixes into a dynamic candidate pool
(OwnedRibbonGroup::command_ids → PluginManager::command_names →
plugin_command_names), refreshed in rebuild_ribbon_modules on startup
load, settings reload, and every enable/disable toggle. ranked_matches
now merges this pool with the static registry and returns owned strings.

A plugin.toml `commands = [...]` list for sub-verbs with no ribbon button
(e.g. LS_AUTOLABEL) stays a follow-up — it's a plugin-API surface change.

Closes #272

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:01:53 +03:00
Hakan Seven
35468004e6 test(core): ignore ribbon_groups microbenchmark by default
The once_lock_eliminates_allocation_after_first_call test from PR #257
is a timing microbenchmark that Box::leaks ~400k strings by design to
defeat allocator reuse. Mark it #[ignore] so it stays out of the default
suite; cache correctness is already covered by the pointer-identity test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:48:31 +03:00
Karim Jerbi
73b05e1dec perf(core): cache ribbon_groups() with OnceLock, avoid per-frame tree allocations
Change CadModule::ribbon_groups() from -> Vec<RibbonGroup> to -> &[RibbonGroup],
backed by a per-module OnceLock cache. The ribbon group tree is static data
(&'static str, Copy enums) that was being fully reconstructed — Vecs, String
clones, enum discriminants — on every call, including all three per-frame
call sites (view(), dropdown_overlay(), style_combo_overlay()).

- 7 built-in modules + 2 plugin templates cache via function-local OnceLock.
  Safe because each is a unit struct with exactly one instance per process
  (plugins run in isolated child processes; see PluginProcess::spawn()).
- SharedCadModule avoids the static pattern entirely, storing groups in an
  instance field (owned.rs).
- IPC runner converts &RibbonGroup -> OwnedRibbonGroup directly via new
  From<&T> impls, avoiding an intermediate clone.
- render_small/render_large and the two make_tool_row closures now borrow
  (&RibbonItem, &[ToolDef]) instead of taking ownership, so the view loop
  doesn't need to clone items to satisfy the old by-value signatures.
- Various match-ergonomics deref fixes (*id, *default, *icon, etc.) from
  the &RibbonItem pattern change.
2026-07-02 21:00:57 +01:00
Hakan Seven
f9f7138370 feat(plugin): persist XDATA to DWG and let plugins modify/delete entities (#249, #250)
#249 — plugin XDATA written via write_record survived only in memory: the
acadrust DWG writer dropped ExtendedData::records on save. Bump acadrust to
e88a9a6 (records now encode to EED and decode back on read) and fix the host
side that fed it:
  - ensure_app_id allocates a real APPID handle; a null handle serializes as
    0 and the EED reference can't resolve, so the XDATA vanished on reopen.
  - write_record / remove_record drop stale raw_dwg_eed for the target app so
    an edit made after a save/reopen wins over the pre-edit bytes.

#250 — out-of-process plugins got a throwaway document_mut() snapshot, so
edits to existing entities were silently discarded and deletion wasn't
expressible at all. Add the missing mutation surface:
  - UpdateEntity / RemoveEntity IPC requests + HostApi::update_entity /
    remove_entity (default in-process impls, RPC overrides on the client that
    invalidate the stale document cache).
  - Scene::update_entity replaces the entity in place, preserving its handle
    and owning block, and reseeds only its derived caches; remove reuses the
    cache-coherent erase_entities (which also honours layer locks).
  - document_mut() is documented as a local-only snapshot out-of-process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:39:26 +03:00
Hakan Seven
e3d1780a69 feat(plugin): quiet plugin IPC logs; print one "Loaded plugin" line
A normal run dumped the whole host↔runner trace (spawn, handshake, every
per-command dispatch and request/response) to the terminal. Gate that
behind OCS_PLUGIN_VERBOSE and, in normal runs, print just one line per
plugin: `Loaded plugin: <name> (<id> <version>)`.

The chatty host-side `[plugin]` lines now go through a `vlog!` macro that
only fires when OCS_PLUGIN_VERBOSE is set. Runner-side logs were already
suppressed (the runner is spawned with stderr = null); the remaining
`[plugin]` lines in the IPC client are genuine error messages, left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:28:10 +03:00
Hakan Seven
ccd249d40f fix(plugin): bound the runner handshake read with a deadline
The handshake verification used a raw blocking `recv`, the one unbounded
read in the spawn path: `accept` is guarded by `spawn_timeout` and every
host->runner `call` by `call_timeout`, but a process that won the accept
race and then sent nothing — or a runner that died mid-handshake — would
hang the host forever.

Route the handshake through the existing `recv_with_deadline` helper so
the first frame is bounded too (marking the process dead on timeout), and
reduce `verify_runner_handshake` to a pure token check on the received
message. Tests updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 10:41:01 +03:00
Sebastian
04240862e1 Adding runner handshake 2026-06-28 10:41:01 +03:00
Hakan Seven
5e59421e92 test(plugin): drop Dispatch floor in timeout test; doc stream-close path
The call-timeout test asserted the real 10 s Dispatch floor, adding 10 s+
to every suite run. Add a cfg(test)-only OCS_PLUGIN_TEST_FLOOR_SECS seam
so the test fires at its 1 s base instead; production still enforces the
hard safety floors. Test now finishes in ~1 s.

Also document in mark_dead that the host-side socket is closed indirectly
by killing the child (which unblocks and drops the detached reader
thread's Stream), not by the take() in mark_dead itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:56:24 +03:00
Sebastian
b18be4dacc Adding timeout protection to host from stuck runner 2026-06-26 10:10:56 +02:00
Hakan Seven
0efa559bd6 chore(deps): bump acadrust 0.3.4 → 0.4
Tracks acadrust main at 0.4.x: plotstyle/material common-entity
ordering fix plus in-flight DWG reader work. Patch comment updated
to match; drop the git patch once these land in a published 0.4 crate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:00:36 +03:00
Hakan Seven
b4093724f2 fix(plugin): f64 pick coords + harden out-of-process lifecycle
Follow-ups after merging the out-of-process plugin isolation work:

- Convert the out-of-process interactive adapter's `on_point` /
  `on_entity_pick` to `glam::DVec3`, matching the `CadCommand` trait,
  which moved to f64 pick coordinates. The branch predated that change
  and no longer compiled against the trait.
- Contain plugin constructor panics in `export_plugin!`: the constructor
  runs across the C ABI boundary in `ocs_plugin_register`, where an
  unwinding panic is undefined behavior. Catch it and return null, which
  the loader already treats as a failed registration.
- Reap killed runner processes. The spawn-timeout and disconnect paths
  called `child.kill()` without `wait()`, leaving a zombie on Unix until
  the host exited. Route both — and `shutdown()` — through a shared
  `reap()` that kills and waits on a detached thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 00:16:08 +03:00
Sebastian
d0003534a0 Fix 5: Allow backward-compatibility and add plugin-template-v2 2026-06-25 00:02:17 +03:00
Sebastian
2b898d6f22 Fix 4: shared-memory 2026-06-25 00:01:36 +03:00
Sebastian
a93d425002 Fix 3: optimize shutdown times 2026-06-25 00:01:36 +03:00
Sebastian
8eb199bfc7 Fix 2: optimize src 2026-06-25 00:01:36 +03:00
Sebastian
dcfaed0182 Initial commit 2026-06-25 00:01:36 +03:00
Hakan Seven
707c143212 feat(plugin): object-pick acquisition for interactive commands
Extend InteractiveCommand with needs_object_pick / on_object_pick so a
plugin tool can prompt the user to pick an existing entity (handle + point)
rather than a free point — the structure-pick path Storm Sewer's SS_PIPE
needs to connect existing structures. The host adapter delegates to the
internal entity-pick flow; over --serve the pick is fed as a hex handle.
Stays API v2 (no released plugin implements InteractiveCommand yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 01:43:53 +03:00
Hakan Seven
931eb908a9 feat(plugin): InteractiveCommand hook for click-to-place (API v2)
Plugins could dispatch commands but not register interactive (click-to-
place) tools — the gap mf4633 flagged on #100 for Storm Sewer's SS_INLET
/ SS_PIPE. Add an InteractiveCommand trait + CommandStep to ocs_plugin_api
and HostApi::start_interactive; a host adapter bridges it to the internal
CadCommand, so a plugin tool drives the host's point-collection flow.

Hybrid by construction: the same command works by clicking in the viewport
AND by feeding coordinates over --serve (run "CMD x,y x,y"). Adding a
HostApi method changes the contract vtable, so API_VERSION bumps to 2 —
v1 plugin binaries are now refused at load.

Part of the #100 extensibility epic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:43:35 +03:00
Hakan Seven
29c71be101 feat(plugin): load external cdylib plugins at runtime (phase 2)
Move BuiltinPlugin into ocs_plugin_api (host feature) so out-of-tree
crates can implement it, and add export_plugin! to emit the two C
symbols a cdylib exposes: ocs_plugin_api_version (checked first, so an
ABI-incompatible build never runs) and ocs_plugin_register -> boxed
BuiltinPlugin. The host loads every compatible package from the plugins
folder at startup via libloading (desktop only), keeps the library
resident for the session, and merges its ribbon tab + command dispatch
into the same paths as built-ins (honouring enable/disable). The Plugin
Manager shows external packages with a Loaded / incompatible status.

Approach B: the plugin hands back a boxed trait object, assuming a
matching toolchain + ocs_plugin_api version (the version symbol enforces
the latter). crates/ocs_example_plugin is the reference cdylib.

Part of the #100 extensibility epic (phase 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:48:56 +03:00
Hakan Seven
5ccf5cfe36 feat(plugin): lift host surface behind a HostApi trait (phase 1b)
Plugins targeted the host's concrete HostSession type. Add a HostApi
trait in ocs_plugin_api behind an optional `host` feature (the only thing
that pulls acadrust, so the core crate stays dependency-free). HostSession
implements it and BuiltinPlugin::dispatch now takes `&mut dyn HostApi`, so
an out-of-tree add-on compiles against the contract crate alone. Per-tab
plugin state is reached through object-safe plugin_state* helpers.

Completes the phase-1 host-surface extraction in the #100 epic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:31:05 +03:00
Hakan Seven
50825b6346 feat(plugin): ModuleEvent::PluginFileDialog for native file import
Add-ons had no way to request a file picker — only the host's own Open
dialog. PluginFileDialog lets a plugin tool ask the host to open a native
picker; on selection the host dispatches "<command> <path>" back to the
plugin with original case preserved (bypassing the command-line
upper-casing that mangles case-sensitive paths on Linux/macOS). The demo
plugin gains an Import tool exercising it.

Part of the #100 extensibility epic (surfaced in #106).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:22:08 +03:00
Hakan Seven
72ec1146df refactor(plugin): extract ocs_plugin_api crate for the stable add-on contract
Move the dependency-free, semver-versioned half of the plugin contract into a
standalone workspace crate `crates/ocs_plugin_api`:

- manifest types: PluginManifest, ApiVersion, API_VERSION
- ribbon vocabulary: CadModule trait + ToolDef/RibbonGroup/RibbonItem/IconKind/
  ModuleEvent/StyleKey

The host re-exports them from `crate::plugin::manifest` and `crate::modules`, so
every existing call site is unchanged. The acadrust-typed runtime surface
(HostSession) stays in the host binary; lifting it behind a HostApi trait in the
same crate is the remaining phase-1b step.

Part of #100. Docs updated in docs/plugin-architecture.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 01:36:04 +03:00