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
109impl Node {
110    /// Clears the ordered port list.
111    pub fn clear_ports(&mut self) {
112        self.ports.clear();
113    }
114
115    /// Retains ports that satisfy `f`.
116    pub fn retain_ports(&mut self, f: impl FnMut(&PortId) -> bool) {
117        self.ports.retain(f);
118    }
119}
120
121/// Per-node origin override, expressed as a normalized fraction of the node rect.
122#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
123pub struct NodeOrigin {
124    pub x: f32,
125    pub y: f32,
126}
127
128impl NodeOrigin {
129    pub fn is_finite(self) -> bool {
130        self.x.is_finite() && self.y.is_finite()
131    }
132
133    pub fn as_tuple(self) -> (f32, f32) {
134        (self.x, self.y)
135    }
136}
137
138/// Per-node movement/resize extent.
139#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
140#[serde(tag = "kind", rename_all = "snake_case")]
141pub enum NodeExtent {
142    /// Constrain to the node's parent container (if any).
143    Parent,
144    /// Constrain to the given rect in canvas space.
145    Rect { rect: CanvasRect },
146}