Solids Engine

DAG CSG

DAG CSG is the primary modeling representation for Constructive Solid Geometry (CSG) in the SDK. It provides built-in support for common unary and binary operations, along with extensible building blocks that enable developers to create more advanced and customized operations.

What DAG CSG Represents

DAG CSG is designed specifically for the attribute-rich DAG data model. Unlike traditional CSG systems that operate solely on geometry, DAG CSG processes both geometry and associated attributes as first-class data.

CSG operations evaluate not only the spatial relationships between input solids, but also their attributes. During an operation, attributes can be combined, replaced, or selectively propagated from either input model. The exact behavior is determined by the attribute type, allowing fine-grained and predictable control over attribute resolution.

At its core, DAG CSG defines how a solid is constructed—not just the resulting mesh. This procedural representation preserves modeling intent and enables robust downstream operations.

Why Use DAG CSG

  • Non-Destructive Authoring: Preserve full construction history and editable model intent.
  • Version-Friendly: DAG CSG integrates cleanly with SDK state/version workflows for commit, diff, and rollback.
  • Composable Modeling: Reuse graph fragments and compose larger models from smaller authored parts.
  • Resolution-Aware Processing: Expensive operations can run at the precision required by the task.
  • Deterministic Behavior: The same graph and inputs produce consistent geometry outputs.

Integration with the Solids Stack

DAG CSG serves as the authoring and modeling layer within the Solids stack. CSG operations are evaluated eagerly and locally, leaving the input objects unmodified and producing a new solid that can be passed to downstream systems

This makes DAG CSG particularly well-suited for workflows centered on interactive modeling, edit history, and reusable design intent. If your workflow prioritizes procedural construction and non-destructive edits, DAG CSG is the natural entry point.

DAG CSG API

For DAG-vs-DAG boolean operations, the primary API is dag_csg (or dag_csg_with_combinators when explicit attribute combinators are required).

dag_csg Inputs

dag_csg takes the following conceptual inputs:

  • txn: &dyn DagCsgTransaction
    Active transaction used for all reads/writes during the operation.
  • config: &DagCsgConfig
    Execution/configuration settings for DAG CSG evaluation.
  • op: DagCsgOp
    Boolean mode (Replace, Subtract, Intersect, Merge, Select).
  • relative_isometry: Isometry3<f64>
    Relative pose between the two models so CSG is performed in a shared frame.
  • model_a: &DagModel<G> and model_b: &DagModel<G>
    The two input DAG models (same leaf geometry type G).

When attribute merge behavior must be explicitly controlled, use dag_csg_with_combinators(...), which adds:

  • combinators: Option<HashMap<FseHeader, GradesCombinatorType>>
    Per-attribute combinator strategy for overlap handling during merge operations.

Typical Call Flow

  1. Resolve models via DagCsgContext / SharedDagModelContext.
  2. Begin a transaction: let txn = dag_context.begin().await?;.
  3. Run dag_csg(...) (or dag_csg_with_combinators(...)) with &*txn and config.
  4. Persist output model and commit with txn.commit().await?.

Example: dag_csg

use fdk_client_plugin::fsl_lib::prelude::*;
use fdk_client_plugin::fsl_lib::solids_engine::core::dag_model::{
    DagCsgConfig, DagCsgOp, LeafAabb, dag_csg,
};

async fn my_subtract(
    dag_context: &DagCsgContext,
    model_a: &DagModel<LeafAabb>,
    model_b: &DagModel<LeafAabb>,
) -> Result<DagModel<LeafAabb>, DagCsgError> {
    let txn = dag_context.begin().await?;

    let result = dag_csg(
        &*txn,
        &DagCsgConfig::default(),
        DagCsgOp::Subtract,
        Isometry3::identity(),
        model_a,
        model_b,
    )
    .await?;

    txn.commit().await?;
    Ok(result)
}

This example performs model_a - model_b in a shared coordinate frame (Isometry3::identity()).

General DAG CSG Operations

In addition to DAG-vs-DAG boolean ops, general CSG helpers exist for targeted workflows:

  • dag_subtract_classifier(...): Subtract a classifier (for example, mesh-derived cutter geometry) from a DAG model.
  • dag_filter_map(...): Construct a new DAG model from the filter-mapped leaves of the input DAG model.

Use these general operations when the workflow is not a DAG-vs-DAG boolean in a shared coordinate frame.


Lazy DAG CSG

Lazy DAG CSG is the high-performance execution layer for DAG-based CSG operations. It is designed for large datasets and deferred computation, where geometry and spatial queries are evaluated only when needed.

Execution Model

Unlike authoring-focused DAG CSG, Lazy DAG CSG emphasizes when and where computation happens:

  • Deferred Evaluation: Operations are planned first and executed on demand.
  • Composable Operations: Build complex lazy DAG compute graphs from reusable components.
  • Server-Side Scale: Heavy operations can be offloaded to backend infrastructure.

This model is ideal for large block models, point clouds, and terrain-aligned analysis.

Typical Use Cases

  • Subtracting a designed excavation shape from a large terrain or block model.
  • Running repeated spatial classifications over changing regions of interest.
  • Performing compute-intensive boolean analysis without materializing full geometry each frame.

DAG CSG + Lazy DAG CSG

The two systems are complementary:

  1. Author in DAG CSG: Define the logic of your operation in dag csg code.
  2. Execute in Lazy DAG: Heavy compute is offloaded to backend infrastructure.

This separation lets teams keep modeling workflows expressive while keeping heavy compute fast and economical.

When to Choose Lazy DAG CSG

Use Lazy DAG CSG when your bottleneck is data scale or query cost rather than model authoring complexity. For user-facing modeling and history-aware edits, begin with DAG CSG and hand off to Lazy DAG CSG for execution.

Building Your Own LazyDagOperation

  1. Define an operation struct with lazy model handles (LazyDagModel<G>) and immutable inputs.
  2. Implement LazyDagOperation (execute, description, name).
  3. In execute(...), load input model(s) from LazyDagOperationsState, run the DAG function, and set outputs back into state.
  4. Queue the operation in LazyDagOperationQueue, then run the queue via execute(...) or execute_minimal(...).

Example operation:

use fdk::libs::solids_engine::core::block_model::SamplingParams;
use fdk::libs::solids_engine::core::classifier::SharedClassifier;
use fdk::libs::solids_engine::dag_lazy::{
    CustomInputLoader, LazyDagModel, LazyDagOperation, LazyDagOperationDescription,
    LazyDagOperationsState, LazyDagResult,
};
use fdk::libs::solids_engine::core::dag_model::{
    DagCsgConfig, DagCsgTransaction, LeafAabb, dag_subtract_classifier,
};

#[derive(Clone)]
pub struct LazyDagSubtractClassifier {
    model: LazyDagModel<LeafAabb>,
    classifier_with_pose: SharedClassifier,
    subtract_exterior: bool,
    sampling_params: SamplingParams,
    result: LazyDagModel<LeafAabb>,
}

#[async_trait::async_trait]
impl LazyDagOperation for LazyDagSubtractClassifier {
    async fn execute(
        &self,
        txn: &dyn DagCsgTransaction,
        config: &DagCsgConfig,
        _loader: &dyn CustomInputLoader,
        state: &mut LazyDagOperationsState,
    ) -> LazyDagResult<()> {
        let model = state.get_or_cache(&self.model, txn).await?;
        let result = dag_subtract_classifier(
            txn,
            config,
            &model,
            self.classifier_with_pose.classifier.clone(),
            self.classifier_with_pose.position,
            self.subtract_exterior,
            self.sampling_params,
            |block| Some(*block),
        )
        .await?;

        state.set(self.result, result)?;
        Ok(())
    }

    fn description(&self) -> LazyDagOperationDescription {
        LazyDagOperationDescription::unary_op(self.model, self.result)
    }

    fn name(&self) -> String {
        "SubtractClassifier".to_string()
    }
}

Register the operation type token:

crate::implementor_token_generic_leaf_type!(
    LazyDagSubtractClassifier,
    "b09e8a27-0b41-4f89-a2fe-5780b8e56759",
    "2a480ad6-0f79-48c1-aca0-9c60f3ce6107"
);

This registration step is required so the lazy operation can be serialized/deserialized and resolved by the lazy execution system for each supported leaf geometry type.

Executor-style usage:

use fdk::libs::solids_engine::dag_lazy::{
    CustomInputLoaderRegistry, LazyDagOperationQueue, LazyDagOperationsState,
};

let mut queue = LazyDagOperationQueue::default();
let mut state = LazyDagOperationsState::default();

let input = LazyDagModel::<LeafAabb>::new();
let output = LazyDagModel::<LeafAabb>::new();

queue.queue(LazyDagSubtractClassifier {
    model: input,
    classifier_with_pose,
    subtract_exterior: false,
    sampling_params: SamplingParams::default(),
    result: output,
});

let txn = dag_context.begin().await?;
let input_loader = CustomInputLoaderRegistry::default();

let models = queue
    .execute(
        &*txn,
        &DagCsgConfig::default(),
        &input_loader,
        &mut state,
        &[output],
    )
    .await?;

let out_model = &models[0];

Implementation tips:

  • Keep execute(...) deterministic and write outputs via state.set(...).
  • Define description() dependencies accurately so the executor can schedule correctly.