yucad3d

Plugin C ABI

A stable C ABI lets a DLL add new node types — with their own persistence, plan/3D rendering, properties and commands — without recompiling yu3d.

Header: sdk/include/yu3d_plugin.h. Host loader: src/pluginhost/.

Entry point & versioning

#define YU3D_PLUGIN_ABI_VERSION 1u
YU3D_PLUGIN_EXPORT int yu3d_plugin_register(const Yu3dHostApi* host);

A plugin exports exactly one function. The host passes a stable function-pointer table (Yu3dHostApi) valid for the process lifetime; the plugin returns 1 to accept. Compatibility is additive — new fields are appended to the end of Yu3dHostApi; check presence with YU3D_HOST_HAS(host, field) (a struct-size guard) before calling anything added after 1.0. Incompatible changes bump the major version.

Registering a node type

typedef struct {
  const char*        type_id;       // "acme.pole" — globally unique, stable
  Yu3dWriteFn        write;         // serialise payload
  Yu3dReadFn         read;          // deserialise -> payload
  Yu3dDestroyFn      destroy;       // free payload
  Yu3dPlanRenderFn   plan_render;   // nullable — draw in Plan
  Yu3dSceneRenderFn  scene_render;  // nullable — draw in 3D
  void*              user;          // echoed to callbacks
} Yu3dNodeType;

The payload is an opaque blob the plugin owns; the host stores it inside a node and hands it back to every callback. Opaque handles (Yu3dWriter*, Yu3dReader*, Yu3dPlanCtx*, Yu3dSceneCtx*, Yu3dDocument*) are valid only for the duration of the call — never store them.

What the host offers

AreaFunctions
Binary I/Ow_u8/w_u32/w_f64/w_str; r_u8/r_u32/r_f64/r_str (reader returns 1 on success).
Plan renderplan_add_segment/marker/text, plan_world_per_pixel, plan_set_pick_id (sub-entity picking). Colours linear 0..1; text offsets in DIPs.
3D renderscene_add_lines, scene_add_triangles (flat xyz arrays + index arrays).
Documentdoc_add_node(type_id, payload, name) — wraps a payload in a node under root as one undoable edit.
Registrationregister_property (typed, get/set), register_summary, register_command(id, label, group, run).
Civil model — buildalign_create + align_set_geometry_ex (tangent/arc/spiral) + align_set_profile; align_create_offset / align_set_bond (live derived alignments); surface_create + surface_add_breakline; pointgroup_create (COGO points); assembly_create_preset; corridor_create + corridor_bind_assembly + corridor_add_baseline (multi-baseline); run_command; doc_save / doc_save_exchange.
Civil model — readalign_kind/category, align_h_element(_count), align_v_pvi(_count), align_elevation_at, align_bond; surface_point/triangle/breakline getters; corridor_baseline/region getters; pointgroup_point/name/code getters. Enough to reconstruct everything the Civil 3D bridge serialises.

Editable properties

A plugin describes properties (Yu3dPropertyDesc: key, label, kind REAL/INT/TEXT/BOOL, read-only, get/set callbacks). The Properties dock edits any module/plugin node through the same path, with undo.

Building & reading the civil model

Beyond registering its own node types, a plugin can author and introspect the core civil model — alignments (with spirals), design profiles, live derived alignments (offset / curb-return / connected, carrying a recomputable bond), surfaces, and multi-baseline corridors — then serialise the whole thing (doc_save / doc_save_exchange). The build calls (align_*, surface_create, corridor_*) route through the host's undo stack; the read calls expose the same geometry the exchange writer sees. This surface is dogfooded by the yu3d_api_civil_rt smoke, which builds a project purely through the ABI, reads every piece back through the ABI, and asserts parity with the .yu3dx export — so an external plugin can reconstruct exactly what the Civil 3D bridge does without touching the model.

ExternNode vs ForeignNode

Plugin loadedPlugin absent
Node classdata::ExternNodedata::ForeignNode
HoldsLive opaque payload + destroy callbackRaw payload bytes
BehaviourRenders, edits, persists via the pluginInert, but round-trips byte-for-byte

On load, the plugin host registers a NodeIO for each plugin type that bridges the C ABI read/write to the engine's BinaryReader/Writer; a payload becomes an ExternNode. With the plugin missing, the same bytes load as a ForeignNode: the host keeps them verbatim and writes them back on save, so a document never loses a plugin's data just because the plugin is absent. The calls that read and write those bytes are the payload writer / reader groups.

Loading & the trust gate

// PluginHost — UI-toolkit-free
void set_load_approver(function<bool(const string& dll_path)> approver);
int  load_plugins(const string& dir);   // scans *.dll, returns count loaded

The approver is consulted before LoadLibrary — so a DLL's DllMain cannot run without consent. The policy (trusted hashes, prompt on first sight / file change) lives host-side; without an approver all plugins load (the dev default). DLLs stay resident for the process lifetime.

For the safe, in-process extension tier (no native code), see Lua scripting.