jellyflow-runtime 0.3.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use crate::runtime::connection::{
    ConnectionHandleRef, ConnectionTargetCandidate, ConnectionTargetFromHandlesInput,
    ConnectionTargetHandle, ResolvedConnectionTarget, resolve_connection_target_from_handles,
};
use crate::runtime::geometry::{
    EdgeEndpointInput, EdgeInteractionFacts, EdgePosition, HandlePosition, edge_position,
    resolve_edge_route_path,
};
use crate::runtime::measurement::{
    LayoutEdgePosition, LayoutEdgeRouteFacts, LayoutFactsQueryResult, LayoutNodeMeasurementStatus,
    NodeMeasurementStatus, resolve_handle_measurement,
};
use crate::runtime::utils::get_node_rect;
use jellyflow_core::core::{CanvasPoint, CanvasSize, EdgeId, PortDirection};

use super::backend::NodeGraphQuerySnapshot;
use super::rendering::resolve_rendering_read_model;

pub(crate) fn resolve_layout_facts_read_model(
    snapshot: &NodeGraphQuerySnapshot<'_>,
    viewport_size: CanvasSize,
) -> LayoutFactsQueryResult {
    let rendering = resolve_rendering_read_model(snapshot, viewport_size);
    let visible_edge_positions: Vec<LayoutEdgePosition> = rendering
        .visible_edge_render_order
        .iter()
        .copied()
        .filter_map(|edge| {
            edge_position_from_layout_facts(snapshot, edge)
                .map(|position| LayoutEdgePosition::new(edge, position))
        })
        .collect();
    let visible_edge_route_facts =
        visible_edge_route_facts_from_layout_facts(snapshot, &visible_edge_positions);
    let connection_target_candidates = connection_target_candidates_from_layout_facts(snapshot);

    let node_measurement_statuses = rendering
        .visible_node_ids
        .iter()
        .copied()
        .map(|node| {
            LayoutNodeMeasurementStatus::new(
                node,
                snapshot
                    .lookups
                    .node_lookup
                    .get(&node)
                    .map(|entry| entry.measurement_status())
                    .unwrap_or(NodeMeasurementStatus::Missing),
            )
        })
        .collect::<Vec<_>>();

    LayoutFactsQueryResult::new(
        snapshot.layout_facts_revision,
        rendering,
        visible_edge_positions,
        connection_target_candidates,
    )
    .with_node_measurement_statuses(node_measurement_statuses)
    .with_edge_route_facts(visible_edge_route_facts)
}

pub(crate) fn visible_edge_route_facts_from_layout_facts(
    snapshot: &NodeGraphQuerySnapshot<'_>,
    edge_positions: &[LayoutEdgePosition],
) -> Vec<LayoutEdgeRouteFacts> {
    edge_positions
        .iter()
        .filter_map(|edge_position| {
            let edge = snapshot.graph.edges().get(&edge_position.edge)?;
            let policy = snapshot.interaction.edge_interaction_policy(edge);
            resolve_edge_route_path(edge, edge_position.position)
                .map(|facts| {
                    facts
                        .with_hit_test(edge_route_hit_test_options(
                            snapshot.interaction.edge_hit_test_options_for(edge),
                            edge.view.hit_target_width,
                        ))
                        .with_interaction(EdgeInteractionFacts {
                            selectable: policy.selectable,
                            selected: snapshot
                                .view_state
                                .selected_edges
                                .contains(&edge_position.edge),
                            focusable: policy.focusable,
                            deletable: policy.deletable,
                            reconnect_source: policy.reconnect_source,
                            reconnect_target: policy.reconnect_target,
                        })
                })
                .map(|facts| LayoutEdgeRouteFacts::new(edge_position.edge, facts))
        })
        .collect()
}

fn edge_route_hit_test_options(
    base: crate::runtime::geometry::EdgeHitTestOptions,
    hit_target_width: Option<f32>,
) -> crate::runtime::geometry::EdgeHitTestOptions {
    hit_target_width
        .filter(|width| width.is_finite() && *width > 0.0)
        .map(
            |interaction_width| crate::runtime::geometry::EdgeHitTestOptions {
                interaction_width,
                ..base
            },
        )
        .unwrap_or(base)
}

pub(crate) fn connection_target_candidates_from_layout_facts(
    snapshot: &NodeGraphQuerySnapshot<'_>,
) -> Vec<ConnectionTargetCandidate> {
    let mut candidates = Vec::new();

    for (node_id, node) in snapshot.graph.nodes() {
        if node.hidden {
            continue;
        }
        let Some(entry) = snapshot.lookups.node_lookup.get(node_id) else {
            continue;
        };
        let Some(node_rect) =
            get_node_rect(snapshot.lookups, *node_id, snapshot.node_origin(), None)
        else {
            continue;
        };

        for port_id in &entry.ports {
            let Some(port) = snapshot.graph.ports().get(port_id) else {
                continue;
            };
            let handle = ConnectionHandleRef::new(*node_id, *port_id, port.dir);
            let Some(bounds) =
                resolve_handle_measurement(snapshot.graph, snapshot.lookups, handle).bounds
            else {
                continue;
            };
            let policy = snapshot.interaction.port_interaction_policy(node, port);
            candidates.push(ConnectionTargetCandidate::new(
                ConnectionTargetHandle::new(
                    handle,
                    policy.connectable,
                    policy.can_accept_connection(),
                ),
                node_rect,
                bounds,
            ));
        }
    }

    candidates
}

pub(crate) fn resolve_connection_target_from_layout_facts(
    snapshot: &NodeGraphQuerySnapshot<'_>,
    pointer: CanvasPoint,
    from: ConnectionHandleRef,
) -> ResolvedConnectionTarget {
    let connection = snapshot.interaction.connection_interaction();
    let candidates = connection_target_candidates_from_layout_facts(snapshot);
    resolve_connection_target_from_handles(ConnectionTargetFromHandlesInput::new(
        pointer,
        connection.connection_radius,
        from,
        &candidates,
        connection.connection_mode,
    ))
}

pub(crate) fn edge_position_from_layout_facts(
    snapshot: &NodeGraphQuerySnapshot<'_>,
    edge: EdgeId,
) -> Option<EdgePosition> {
    let edge = snapshot.graph.edges().get(&edge)?;
    let from_port = snapshot.graph.ports().get(&edge.from)?;
    let to_port = snapshot.graph.ports().get(&edge.to)?;
    let source_node = snapshot.graph.nodes().get(&from_port.node)?;
    let target_node = snapshot.graph.nodes().get(&to_port.node)?;
    if source_node.hidden || target_node.hidden {
        return None;
    }

    let source_rect = get_node_rect(
        snapshot.lookups,
        from_port.node,
        snapshot.node_origin(),
        None,
    )?;
    let target_rect = get_node_rect(snapshot.lookups, to_port.node, snapshot.node_origin(), None)?;

    edge_position(
        EdgeEndpointInput {
            node_rect: source_rect,
            handle: resolve_handle_measurement(
                snapshot.graph,
                snapshot.lookups,
                ConnectionHandleRef::new(from_port.node, edge.from, from_port.dir),
            )
            .bounds,
            fallback_position: fallback_handle_position(from_port.dir),
        },
        EdgeEndpointInput {
            node_rect: target_rect,
            handle: resolve_handle_measurement(
                snapshot.graph,
                snapshot.lookups,
                ConnectionHandleRef::new(to_port.node, edge.to, to_port.dir),
            )
            .bounds,
            fallback_position: fallback_handle_position(to_port.dir),
        },
    )
}

fn fallback_handle_position(direction: PortDirection) -> HandlePosition {
    HandlePosition::fallback_for_direction(direction)
}