Skip to main content

jellyflow_layout/
engine.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::sync::Arc;
4
5use jellyflow_core::{
6    CanvasPoint, CanvasRect, CanvasSize, EdgeId, Graph, GraphOp, GraphTransaction, NodeId,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::family::{LayoutEngineMetadata, LayoutFamilyId, LayoutFamilyMetadata};
11
12/// Stable engine id for the built-in Dagre-compatible `dugong` engine.
13pub const DUGONG_LAYOUT_ENGINE_ID: &str = "dugong";
14/// Stable engine id for the built-in radial mind-map engine.
15pub const MIND_MAP_RADIAL_LAYOUT_ENGINE_ID: &str = "mind_map_radial";
16/// Stable engine id for the built-in freeform mind-map engine.
17pub const MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID: &str = "mind_map_freeform";
18
19/// Stable identifier for a layout engine.
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct LayoutEngineId(String);
23
24impl LayoutEngineId {
25    /// Creates a new engine id.
26    pub fn new(id: impl Into<String>) -> Self {
27        Self(id.into())
28    }
29
30    /// Returns the built-in `dugong` engine id.
31    pub fn dugong() -> Self {
32        Self::new(DUGONG_LAYOUT_ENGINE_ID)
33    }
34
35    /// Returns the built-in radial mind-map engine id.
36    pub fn mind_map_radial() -> Self {
37        Self::new(MIND_MAP_RADIAL_LAYOUT_ENGINE_ID)
38    }
39
40    /// Returns the built-in freeform mind-map engine id.
41    pub fn mind_map_freeform() -> Self {
42        Self::new(MIND_MAP_FREEFORM_LAYOUT_ENGINE_ID)
43    }
44
45    /// Returns this id as a string slice.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49}
50
51impl fmt::Display for LayoutEngineId {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str(&self.0)
54    }
55}
56
57impl From<&str> for LayoutEngineId {
58    fn from(value: &str) -> Self {
59        Self::new(value)
60    }
61}
62
63impl From<String> for LayoutEngineId {
64    fn from(value: String) -> Self {
65        Self::new(value)
66    }
67}
68
69/// Direction for a layered graph layout.
70#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum LayoutDirection {
73    /// Top to bottom.
74    #[default]
75    TopToBottom,
76    /// Bottom to top.
77    BottomToTop,
78    /// Left to right.
79    LeftToRight,
80    /// Right to left.
81    RightToLeft,
82}
83
84/// Spacing knobs passed through to layout engines that support layered spacing.
85#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
86pub struct LayoutSpacing {
87    pub nodesep: f32,
88    pub ranksep: f32,
89    pub edgesep: f32,
90}
91
92impl Default for LayoutSpacing {
93    fn default() -> Self {
94        Self {
95            nodesep: 50.0,
96            ranksep: 50.0,
97            edgesep: 20.0,
98        }
99    }
100}
101
102/// Options shared by Jellyflow layout adapters.
103#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
104pub struct LayoutOptions {
105    pub direction: LayoutDirection,
106    pub spacing: LayoutSpacing,
107    pub margin: CanvasSize,
108    pub default_node_size: CanvasSize,
109    /// Fallback node origin used when a node has no per-node origin override.
110    pub node_origin: (f32, f32),
111}
112
113impl Default for LayoutOptions {
114    fn default() -> Self {
115        Self {
116            direction: LayoutDirection::TopToBottom,
117            spacing: LayoutSpacing::default(),
118            margin: CanvasSize {
119                width: 0.0,
120                height: 0.0,
121            },
122            default_node_size: CanvasSize {
123                width: 172.0,
124                height: 36.0,
125            },
126            node_origin: (0.0, 0.0),
127        }
128    }
129}
130
131impl LayoutOptions {
132    /// Uses a different fallback node size for nodes without explicit or measured size.
133    pub fn with_default_node_size(mut self, size: CanvasSize) -> Self {
134        self.default_node_size = size;
135        self
136    }
137
138    /// Uses a different layered layout direction.
139    pub fn with_direction(mut self, direction: LayoutDirection) -> Self {
140        self.direction = direction;
141        self
142    }
143
144    /// Uses a different fallback node origin.
145    pub fn with_node_origin(mut self, node_origin: (f32, f32)) -> Self {
146        self.node_origin = node_origin;
147        self
148    }
149}
150
151/// Which nodes a layout request should include.
152#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(tag = "kind", rename_all = "snake_case")]
154pub enum LayoutScope {
155    /// Include all non-hidden nodes.
156    #[default]
157    All,
158    /// Include only these nodes. Hidden nodes are still ignored.
159    Nodes { nodes: BTreeSet<NodeId> },
160}
161
162impl LayoutScope {
163    pub(crate) fn contains(&self, node: NodeId) -> bool {
164        match self {
165            Self::All => true,
166            Self::Nodes { nodes } => nodes.contains(&node),
167        }
168    }
169}
170
171/// A headless layout request.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct LayoutRequest {
174    pub options: LayoutOptions,
175    #[serde(default)]
176    pub scope: LayoutScope,
177    /// Adapter-reported node sizes. Graph node sizes win over these facts.
178    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
179    pub measured_node_sizes: BTreeMap<NodeId, CanvasSize>,
180}
181
182impl Default for LayoutRequest {
183    fn default() -> Self {
184        Self {
185            options: LayoutOptions::default(),
186            scope: LayoutScope::All,
187            measured_node_sizes: BTreeMap::new(),
188        }
189    }
190}
191
192impl LayoutRequest {
193    /// Creates a request for all visible nodes.
194    pub fn all() -> Self {
195        Self::default()
196    }
197
198    /// Creates a request for a selected set of nodes.
199    pub fn nodes(nodes: impl IntoIterator<Item = NodeId>) -> Self {
200        Self {
201            scope: LayoutScope::Nodes {
202                nodes: nodes.into_iter().collect(),
203            },
204            ..Self::default()
205        }
206    }
207
208    /// Adds adapter-reported node sizes.
209    pub fn with_measured_node_sizes(
210        mut self,
211        sizes: impl IntoIterator<Item = (NodeId, CanvasSize)>,
212    ) -> Self {
213        self.measured_node_sizes.extend(sizes);
214        self
215    }
216
217    /// Sets layout options.
218    pub fn with_options(mut self, options: LayoutOptions) -> Self {
219        self.options = options;
220        self
221    }
222}
223
224/// Request for one selected layout engine.
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub struct LayoutEngineRequest {
227    pub engine: LayoutEngineId,
228    pub layout: LayoutRequest,
229}
230
231impl Default for LayoutEngineRequest {
232    fn default() -> Self {
233        Self {
234            engine: LayoutEngineId::dugong(),
235            layout: LayoutRequest::default(),
236        }
237    }
238}
239
240impl LayoutEngineRequest {
241    /// Creates an engine request.
242    pub fn new(engine: impl Into<LayoutEngineId>, layout: LayoutRequest) -> Self {
243        Self {
244            engine: engine.into(),
245            layout,
246        }
247    }
248
249    /// Creates a request for the built-in `dugong` engine.
250    pub fn dugong(layout: LayoutRequest) -> Self {
251        Self::new(LayoutEngineId::dugong(), layout)
252    }
253
254    /// Creates a request for the built-in radial mind-map engine.
255    pub fn mind_map_radial(layout: LayoutRequest) -> Self {
256        Self::new(LayoutEngineId::mind_map_radial(), layout)
257    }
258
259    /// Creates a request for the built-in freeform mind-map engine.
260    pub fn mind_map_freeform(layout: LayoutRequest) -> Self {
261        Self::new(LayoutEngineId::mind_map_freeform(), layout)
262    }
263
264    /// Uses a different engine id.
265    pub fn with_engine(mut self, engine: impl Into<LayoutEngineId>) -> Self {
266        self.engine = engine.into();
267        self
268    }
269
270    /// Sets the layout request.
271    pub fn with_layout(mut self, layout: LayoutRequest) -> Self {
272        self.layout = layout;
273        self
274    }
275}
276
277/// Runtime or host-owned context available to layout engines.
278#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
279pub struct LayoutContext {
280    /// Runtime-reported node sizes. Graph node sizes and request-local measured sizes win.
281    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
282    pub measured_node_sizes: BTreeMap<NodeId, CanvasSize>,
283    /// Nodes that engines should treat as fixed when they support pinning.
284    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
285    pub pinned_nodes: BTreeSet<NodeId>,
286    /// Runtime fallback node origin, usually resolved from interaction state.
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub node_origin: Option<(f32, f32)>,
289}
290
291impl LayoutContext {
292    /// Creates an empty layout context.
293    pub fn new() -> Self {
294        Self::default()
295    }
296
297    /// Adds runtime-reported node sizes.
298    pub fn with_measured_node_sizes(
299        mut self,
300        sizes: impl IntoIterator<Item = (NodeId, CanvasSize)>,
301    ) -> Self {
302        self.measured_node_sizes.extend(sizes);
303        self
304    }
305
306    /// Adds nodes that engines should keep fixed when possible.
307    pub fn with_pinned_nodes(mut self, nodes: impl IntoIterator<Item = NodeId>) -> Self {
308        self.pinned_nodes.extend(nodes);
309        self
310    }
311
312    /// Sets the runtime fallback node origin.
313    pub fn with_node_origin(mut self, node_origin: (f32, f32)) -> Self {
314        self.node_origin = Some(node_origin);
315        self
316    }
317}
318
319/// A node position produced by a layout run.
320#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
321pub struct LayoutNodePosition {
322    pub node: NodeId,
323    pub pos: CanvasPoint,
324    pub center: CanvasPoint,
325    pub size: CanvasSize,
326}
327
328/// A layout-produced edge route.
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330pub struct LayoutEdgeRoute {
331    pub edge: EdgeId,
332    pub points: Vec<CanvasPoint>,
333}
334
335/// Result of a headless layout run.
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub struct LayoutResult {
338    /// Engine-produced results contain at most one entry per node. If a caller manually constructs a
339    /// result with duplicates, lookup returns the first entry and transaction conversion fails.
340    pub nodes: Vec<LayoutNodePosition>,
341    #[serde(default, skip_serializing_if = "Vec::is_empty")]
342    pub edge_routes: Vec<LayoutEdgeRoute>,
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub bounds: Option<CanvasRect>,
345}
346
347impl LayoutResult {
348    /// Finds one node position in this result.
349    pub fn node_position(&self, node: NodeId) -> Option<LayoutNodePosition> {
350        self.nodes
351            .iter()
352            .find(|position| position.node == node)
353            .copied()
354    }
355
356    /// Converts node position changes into a Jellyflow transaction.
357    pub fn to_transaction(&self, graph: &Graph) -> Result<GraphTransaction, LayoutError> {
358        let mut seen = BTreeSet::new();
359        let mut ops = Vec::new();
360
361        for node in &self.nodes {
362            if !seen.insert(node.node) {
363                return Err(LayoutError::DuplicateResultNode(node.node));
364            }
365
366            let from = graph
367                .nodes
368                .get(&node.node)
369                .ok_or(LayoutError::MissingTransactionNode(node.node))?
370                .pos;
371            if from != node.pos {
372                ops.push(GraphOp::SetNodePos {
373                    id: node.node,
374                    from,
375                    to: node.pos,
376                });
377            }
378        }
379
380        Ok(GraphTransaction::from_ops(ops).with_label("Layout graph"))
381    }
382}
383
384/// Errors reported by layout projection, registry lookup, or layout output conversion.
385#[derive(Debug, thiserror::Error, PartialEq)]
386pub enum LayoutError {
387    #[error("layout engine id is already registered: {0}")]
388    DuplicateLayoutEngine(LayoutEngineId),
389    #[error("layout family id is already registered: {0}")]
390    DuplicateLayoutFamily(LayoutFamilyId),
391    #[error("layout engine metadata is already registered: {0}")]
392    DuplicateLayoutEngineMetadata(LayoutEngineId),
393    #[error("layout engine is not registered: {0}")]
394    MissingLayoutEngine(LayoutEngineId),
395    #[error("layout default node size must be positive and finite: {0:?}")]
396    InvalidDefaultNodeSize(CanvasSize),
397    #[error("layout spacing values must be non-negative and finite: {0:?}")]
398    InvalidSpacing(LayoutSpacing),
399    #[error("layout margin must be non-negative and finite: {0:?}")]
400    InvalidMargin(CanvasSize),
401    #[error("layout node size must be positive and finite for node {node:?}: {size:?}")]
402    InvalidNodeSize { node: NodeId, size: CanvasSize },
403    #[error("layout scope references missing node: {0:?}")]
404    MissingScopeNode(NodeId),
405    #[error("layout edge references missing source port: {0:?}")]
406    MissingSourcePort(EdgeId),
407    #[error("layout edge references missing target port: {0:?}")]
408    MissingTargetPort(EdgeId),
409    #[error("layout edge source port references missing node: {edge:?}")]
410    MissingSourceNode { edge: EdgeId },
411    #[error("layout edge target port references missing node: {edge:?}")]
412    MissingTargetNode { edge: EdgeId },
413    #[error("layout engine did not return a node position for node {0:?}")]
414    MissingNodePosition(NodeId),
415    #[error("layout result contains a duplicate node position for node {0:?}")]
416    DuplicateResultNode(NodeId),
417    #[error("layout result references missing graph node: {0:?}")]
418    MissingTransactionNode(NodeId),
419    #[error("layout engine returned a non-finite node position for node {node:?}: ({x}, {y})")]
420    NonFiniteNodePosition { node: NodeId, x: f64, y: f64 },
421}
422
423/// Headless layout engine contract.
424pub trait LayoutEngine: Send + Sync {
425    /// Stable engine id.
426    fn id(&self) -> LayoutEngineId;
427
428    /// Runs this layout engine.
429    fn layout(
430        &self,
431        graph: &Graph,
432        request: &LayoutRequest,
433        context: &LayoutContext,
434    ) -> Result<LayoutResult, LayoutError>;
435}
436
437/// Caller-owned registry of layout engines.
438#[derive(Default, Clone)]
439pub struct LayoutEngineRegistry {
440    engines: BTreeMap<LayoutEngineId, Arc<dyn LayoutEngine>>,
441    families: BTreeMap<LayoutFamilyId, LayoutFamilyMetadata>,
442    metadata: BTreeMap<LayoutEngineId, LayoutEngineMetadata>,
443}
444
445impl fmt::Debug for LayoutEngineRegistry {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        f.debug_struct("LayoutEngineRegistry")
448            .field("engines", &self.engine_ids().collect::<Vec<_>>())
449            .field("families", &self.family_ids().collect::<Vec<_>>())
450            .finish()
451    }
452}
453
454impl LayoutEngineRegistry {
455    /// Creates an empty registry.
456    pub fn new() -> Self {
457        Self::default()
458    }
459
460    /// Registers an engine.
461    pub fn insert<E>(&mut self, engine: E) -> Result<(), LayoutError>
462    where
463        E: LayoutEngine + 'static,
464    {
465        self.insert_shared(Arc::new(engine))
466    }
467
468    /// Registers a shared engine object.
469    pub fn insert_shared(&mut self, engine: Arc<dyn LayoutEngine>) -> Result<(), LayoutError> {
470        let id = engine.id();
471        if self.engines.contains_key(&id) {
472            return Err(LayoutError::DuplicateLayoutEngine(id));
473        }
474        self.engines.insert(id, engine);
475        Ok(())
476    }
477
478    /// Registers layout family metadata.
479    pub fn insert_family(&mut self, family: LayoutFamilyMetadata) -> Result<(), LayoutError> {
480        let id = family.id.clone();
481        if self.families.contains_key(&id) {
482            return Err(LayoutError::DuplicateLayoutFamily(id));
483        }
484        self.families.insert(id, family);
485        Ok(())
486    }
487
488    /// Registers layout engine discovery metadata.
489    pub fn insert_metadata(&mut self, metadata: LayoutEngineMetadata) -> Result<(), LayoutError> {
490        let id = metadata.engine.clone();
491        if self.metadata.contains_key(&id) {
492            return Err(LayoutError::DuplicateLayoutEngineMetadata(id));
493        }
494        self.metadata.insert(id, metadata);
495        Ok(())
496    }
497
498    /// Returns an engine by id.
499    pub fn get(&self, id: &LayoutEngineId) -> Option<&dyn LayoutEngine> {
500        self.engines.get(id).map(Arc::as_ref)
501    }
502
503    /// Returns family metadata by id.
504    pub fn family(&self, id: &LayoutFamilyId) -> Option<&LayoutFamilyMetadata> {
505        self.families.get(id)
506    }
507
508    /// Returns engine discovery metadata by engine id.
509    pub fn metadata(&self, id: &LayoutEngineId) -> Option<&LayoutEngineMetadata> {
510        self.metadata.get(id)
511    }
512
513    /// Returns registered engine ids in deterministic order.
514    pub fn engine_ids(&self) -> impl Iterator<Item = &LayoutEngineId> {
515        self.engines.keys()
516    }
517
518    /// Returns registered family ids in deterministic order.
519    pub fn family_ids(&self) -> impl Iterator<Item = &LayoutFamilyId> {
520        self.families.keys()
521    }
522
523    /// Returns registered families in deterministic order.
524    pub fn families(&self) -> impl Iterator<Item = &LayoutFamilyMetadata> {
525        self.families.values()
526    }
527
528    /// Returns registered engine metadata in deterministic order.
529    pub fn engine_metadata(&self) -> impl Iterator<Item = &LayoutEngineMetadata> {
530        self.metadata.values()
531    }
532
533    /// Returns engine metadata for one family in deterministic engine-id order.
534    pub fn engines_for_family(
535        &self,
536        family: &LayoutFamilyId,
537    ) -> impl Iterator<Item = &LayoutEngineMetadata> {
538        self.metadata
539            .values()
540            .filter(move |metadata| &metadata.family == family)
541    }
542
543    /// Runs the engine named by the request.
544    pub fn layout(
545        &self,
546        graph: &Graph,
547        request: &LayoutEngineRequest,
548        context: &LayoutContext,
549    ) -> Result<LayoutResult, LayoutError> {
550        let engine = self
551            .get(&request.engine)
552            .ok_or_else(|| LayoutError::MissingLayoutEngine(request.engine.clone()))?;
553        engine.layout(graph, &request.layout, context)
554    }
555}
556
557/// Runs a layout engine by id.
558pub fn layout_graph_with_engine(
559    graph: &Graph,
560    request: &LayoutEngineRequest,
561    registry: &LayoutEngineRegistry,
562    context: &LayoutContext,
563) -> Result<LayoutResult, LayoutError> {
564    registry.layout(graph, request, context)
565}
566
567/// Runs a layout engine and converts the node positions into a Jellyflow transaction.
568pub fn layout_graph_to_transaction_with_engine(
569    graph: &Graph,
570    request: &LayoutEngineRequest,
571    registry: &LayoutEngineRegistry,
572    context: &LayoutContext,
573) -> Result<GraphTransaction, LayoutError> {
574    layout_graph_with_engine(graph, request, registry, context)?.to_transaction(graph)
575}
576
577pub(crate) fn validate_request(graph: &Graph, request: &LayoutRequest) -> Result<(), LayoutError> {
578    if !request.options.default_node_size.is_positive_finite() {
579        return Err(LayoutError::InvalidDefaultNodeSize(
580            request.options.default_node_size,
581        ));
582    }
583    if !is_non_negative_finite_spacing(request.options.spacing) {
584        return Err(LayoutError::InvalidSpacing(request.options.spacing));
585    }
586    if !is_non_negative_finite_size(request.options.margin) {
587        return Err(LayoutError::InvalidMargin(request.options.margin));
588    }
589
590    if let LayoutScope::Nodes { nodes } = &request.scope {
591        for node in nodes {
592            if !graph.nodes.contains_key(node) {
593                return Err(LayoutError::MissingScopeNode(*node));
594            }
595        }
596    }
597
598    Ok(())
599}
600
601pub(crate) fn resolve_node_size(
602    graph: &Graph,
603    request: &LayoutRequest,
604    context: &LayoutContext,
605    node: NodeId,
606) -> Result<CanvasSize, LayoutError> {
607    let size = graph
608        .nodes
609        .get(&node)
610        .and_then(|node| node.size)
611        .or_else(|| request.measured_node_sizes.get(&node).copied())
612        .or_else(|| context.measured_node_sizes.get(&node).copied())
613        .unwrap_or(request.options.default_node_size);
614
615    if size.is_positive_finite() {
616        Ok(size)
617    } else {
618        Err(LayoutError::InvalidNodeSize { node, size })
619    }
620}
621
622pub(crate) fn resolve_node_origin(
623    origin: Option<jellyflow_core::NodeOrigin>,
624    request_fallback: (f32, f32),
625    context: &LayoutContext,
626) -> (f32, f32) {
627    let fallback = context.node_origin.unwrap_or(request_fallback);
628    let (x, y) = origin.map(|origin| origin.as_tuple()).unwrap_or(fallback);
629    (normalize_origin_component(x), normalize_origin_component(y))
630}
631
632pub(crate) fn position_from_center(
633    center: CanvasPoint,
634    size: CanvasSize,
635    origin: (f32, f32),
636) -> CanvasPoint {
637    CanvasPoint {
638        x: center.x - size.width * (0.5 - origin.0),
639        y: center.y - size.height * (0.5 - origin.1),
640    }
641}
642
643pub(crate) fn node_rect_from_position(node: &LayoutNodePosition) -> CanvasRect {
644    CanvasRect {
645        origin: CanvasPoint {
646            x: node.center.x - node.size.width * 0.5,
647            y: node.center.y - node.size.height * 0.5,
648        },
649        size: node.size,
650    }
651}
652
653pub(crate) fn union_bounds(bounds: Option<CanvasRect>, next: CanvasRect) -> Option<CanvasRect> {
654    if !next.is_positive_finite() {
655        return bounds;
656    }
657
658    let Some(bounds) = bounds else {
659        return Some(next);
660    };
661
662    let min_x = bounds.origin.x.min(next.origin.x);
663    let min_y = bounds.origin.y.min(next.origin.y);
664    let max_x = (bounds.origin.x + bounds.size.width).max(next.origin.x + next.size.width);
665    let max_y = (bounds.origin.y + bounds.size.height).max(next.origin.y + next.size.height);
666
667    Some(CanvasRect {
668        origin: CanvasPoint { x: min_x, y: min_y },
669        size: CanvasSize {
670            width: max_x - min_x,
671            height: max_y - min_y,
672        },
673    })
674}
675
676fn is_non_negative_finite_spacing(spacing: LayoutSpacing) -> bool {
677    spacing.nodesep.is_finite()
678        && spacing.ranksep.is_finite()
679        && spacing.edgesep.is_finite()
680        && spacing.nodesep >= 0.0
681        && spacing.ranksep >= 0.0
682        && spacing.edgesep >= 0.0
683}
684
685fn is_non_negative_finite_size(size: CanvasSize) -> bool {
686    size.is_finite() && size.width >= 0.0 && size.height >= 0.0
687}
688
689fn normalize_origin_component(component: f32) -> f32 {
690    if component.is_finite() {
691        component.clamp(0.0, 1.0)
692    } else {
693        0.0
694    }
695}