jellyflow-runtime 0.2.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use crate::io::NodeGraphInteractionState;
use crate::runtime::geometry::CanvasBounds;
use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize};

use super::super::candidates::DragCandidate;
use super::super::types::NodeDragItem;
use super::geometry::{candidate_bounds, candidate_bounds_at, normalized_rect};

pub(in crate::runtime::drag) fn drag_items(
    interaction: &NodeGraphInteractionState,
    candidates: &[DragCandidate],
    delta: CanvasPoint,
) -> Vec<NodeDragItem> {
    let node_drag = interaction.node_drag_interaction();
    let global_extent = node_drag.node_extent.and_then(normalized_rect);
    let group_bounds = (candidates.len() > 1)
        .then(|| candidate_bounds(candidates))
        .flatten()
        .map(CanvasBounds::to_rect);

    candidates
        .iter()
        .filter_map(|candidate| {
            let desired = CanvasPoint {
                x: candidate.from.x + delta.x,
                y: candidate.from.y + delta.y,
            };
            let extent = adjusted_candidate_extent(*candidate, global_extent, group_bounds);
            let to = extent
                .map(|extent| clamp_candidate_position(*candidate, desired, extent))
                .unwrap_or(desired);
            to.is_finite().then_some(NodeDragItem {
                node: candidate.node,
                from: candidate.from,
                to,
            })
        })
        .collect()
}

fn adjusted_candidate_extent(
    candidate: DragCandidate,
    global_extent: Option<CanvasRect>,
    group_bounds: Option<CanvasRect>,
) -> Option<CanvasRect> {
    if !candidate.node_extent_override
        && let (Some(global_extent), Some(group_bounds), Some(candidate_bounds)) = (
            global_extent,
            group_bounds,
            candidate_bounds_at(candidate, candidate.from).map(CanvasBounds::to_rect),
        )
    {
        let group_max_x = group_bounds.origin.x + group_bounds.size.width;
        let group_max_y = group_bounds.origin.y + group_bounds.size.height;
        let candidate_max_x = candidate_bounds.origin.x + candidate_bounds.size.width;
        let candidate_max_y = candidate_bounds.origin.y + candidate_bounds.size.height;
        let extent_max_x = global_extent.origin.x + global_extent.size.width;
        let extent_max_y = global_extent.origin.y + global_extent.size.height;

        let min = CanvasPoint {
            x: candidate_bounds.origin.x - group_bounds.origin.x + global_extent.origin.x,
            y: candidate_bounds.origin.y - group_bounds.origin.y + global_extent.origin.y,
        };
        let max = CanvasPoint {
            x: candidate_max_x - group_max_x + extent_max_x,
            y: candidate_max_y - group_max_y + extent_max_y,
        };

        return normalized_rect(CanvasRect {
            origin: min,
            size: CanvasSize {
                width: max.x - min.x,
                height: max.y - min.y,
            },
        });
    }

    candidate.extent
}

fn clamp_candidate_position(
    candidate: DragCandidate,
    target: CanvasPoint,
    extent: CanvasRect,
) -> CanvasPoint {
    let Some(bounds) = candidate_bounds_at(candidate, target).map(CanvasBounds::to_rect) else {
        return target;
    };

    let max_x = extent.origin.x + extent.size.width - bounds.size.width;
    let max_y = extent.origin.y + extent.size.height - bounds.size.height;
    let top_left = CanvasPoint {
        x: clamp(bounds.origin.x, extent.origin.x, max_x),
        y: clamp(bounds.origin.y, extent.origin.y, max_y),
    };
    CanvasPoint {
        x: top_left.x + candidate.origin.0 * candidate.size.width,
        y: top_left.y + candidate.origin.1 * candidate.size.height,
    }
}

fn clamp(value: f32, min: f32, max: f32) -> f32 {
    value.max(min).min(max)
}