Skip to main content

jellyflow_core/core/model/
node.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::core::ids::{GroupId, NodeKindKey, PortId};
5
6use super::geometry::{CanvasPoint, CanvasRect, CanvasSize};
7
8fn is_false(v: &bool) -> bool {
9    !*v
10}
11
12/// Node instance.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Node {
15    /// Node kind identifier.
16    pub kind: NodeKindKey,
17    /// Node kind version (for per-kind migrations).
18    pub kind_version: u32,
19    /// Top-left position in canvas space.
20    pub pos: CanvasPoint,
21
22    /// Optional node origin override (XyFlow `node.origin`).
23    ///
24    /// When omitted, runtime uses the global `NodeGraphInteractionState.node_origin`.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub origin: Option<NodeOrigin>,
27
28    /// Whether the node can be selected (XyFlow `node.selectable`).
29    ///
30    /// When omitted, the global `NodeGraphInteractionState.elements_selectable` decides.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub selectable: Option<bool>,
33
34    /// Whether the node can receive keyboard focus (XyFlow `node.focusable`).
35    ///
36    /// When omitted, the global `NodeGraphInteractionState.nodes_focusable` decides.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub focusable: Option<bool>,
39
40    /// Whether the node can be dragged with pointer interactions (XyFlow `node.draggable`).
41    ///
42    /// When omitted, the global `NodeGraphInteractionState.nodes_draggable` decides.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub draggable: Option<bool>,
45
46    /// Whether the node can be used for creating connections via editor interactions (XyFlow
47    /// `node.connectable`).
48    ///
49    /// When omitted, the global `NodeGraphInteractionState.nodes_connectable` decides.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub connectable: Option<bool>,
52
53    /// Whether the node can be deleted via editor interactions (XyFlow `node.deletable`).
54    ///
55    /// When omitted, the global `NodeGraphInteractionState.nodes_deletable` decides.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub deletable: Option<bool>,
58
59    /// Optional group container id (subflow / parent frame).
60    ///
61    /// This is an editor-structure concept (XyFlow `parentId` mental model) and is intentionally
62    /// orthogonal to semantic subgraphs (see ADR 0126).
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub parent: Option<GroupId>,
65
66    /// Optional per-node movement/resize extent override.
67    ///
68    /// This mirrors XyFlow's `node.extent` concept. It is an editor-structure constraint (UI-facing),
69    /// not a semantic graph rule.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub extent: Option<NodeExtent>,
72
73    /// Whether moving/resizing this node can expand its parent container (if any).
74    ///
75    /// This mirrors XyFlow's `node.expandParent` behavior.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub expand_parent: Option<bool>,
78
79    /// Optional explicit node size in logical px at zoom=1 (semantic sizing).
80    ///
81    /// The editor converts this into canvas space by dividing by the current zoom so node content
82    /// remains readable under semantic zoom.
83    ///
84    /// When `None`, the editor derives the size from measured geometry or style defaults.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub size: Option<CanvasSize>,
87
88    /// Whether the node is hidden (XyFlow `node.hidden`).
89    ///
90    /// Hidden nodes are excluded from derived geometry (hit-testing, rendering, fit-view).
91    #[serde(default, skip_serializing_if = "is_false")]
92    pub hidden: bool,
93
94    /// Whether the node is collapsed.
95    #[serde(default)]
96    pub collapsed: bool,
97
98    /// Stable port ordering for this node (UI-facing).
99    #[serde(default, skip_serializing_if = "Vec::is_empty")]
100    pub ports: Vec<PortId>,
101
102    /// Opaque node payload (domain-owned).
103    ///
104    /// This must be preserved for unknown node kinds.
105    #[serde(default)]
106    pub data: Value,
107}
108
109/// Per-node origin override, expressed as a normalized fraction of the node rect.
110#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
111pub struct NodeOrigin {
112    pub x: f32,
113    pub y: f32,
114}
115
116impl NodeOrigin {
117    pub fn is_finite(self) -> bool {
118        self.x.is_finite() && self.y.is_finite()
119    }
120
121    pub fn as_tuple(self) -> (f32, f32) {
122        (self.x, self.y)
123    }
124}
125
126/// Per-node movement/resize extent.
127#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
128#[serde(tag = "kind", rename_all = "snake_case")]
129pub enum NodeExtent {
130    /// Constrain to the node's parent container (if any).
131    Parent,
132    /// Constrain to the given rect in canvas space.
133    Rect { rect: CanvasRect },
134}