Custom Blocks
Pictorus allows users to create custom blocks written in Rust. These blocks can then be included in diagrams just like core blocks. This is useful for adding functionality that doesn't yet exist in Pictorus, or for adding custom hardware drivers. Custom blocks can pull in external Rust crates, so many complex use cases can be handled simply by creating a thin wrapper around an existing library.
Creating a Custom Block
To create a custom block, click on the "Custom Blocks" tab in the left sidebar. Then click the "Create Block" button. This will open a new block editor window that will ask you to input some basic details about your block, such as name, inputs/outputs, and any crates you want to import. Once you've finished with the "Block Definition" tab, you can switch to the "Code" tab. Here you will need to implement the required BlockDef
trait used to define block behavior. At this point you can run "Save & Check", which will save your block as a draft and attempt to validate it. Once checks have passed, you will see an option to "Add Block". Clicking this will make the draft live and add the new custom block to your block palette.
Editing a Custom Block
Blocks can be edited after they've been published to your library. To edit a block, click on the "Custom Blocks" tab in the left sidebar. Then click on the block you want to edit. This will open the code view of your block editor window. You can make any changes you want. You will then need to run "Save & Check" to validate your changes, and then you can click "Update Block" to make the changes live.
Note on editing blocks
Currently only one published version of a block can exist at a time. This means that if you make changes to a block and publish them, any diagrams that use the old version of the block will be updated to use the new version. If your changes are likely to break existing apps, we suggest creating a new block instead of editing the existing one. You can easily fork a block by clicking the three dots on the block card and selecting "Duplicate".
Custom Block Traits and API
Custom blocks are implemented by creating a struct that implements the BlockDef
trait which is shown below. Custom blocks are proper Rust structs that can have any number of fields and methods. The BlockDef
trait is how Pictorus will interact with your struct but you can add any other methods or traits you want to your struct. For example if you have to do any clean up after the block is done you could implement the Drop
trait for your struct.
#![allow(unused)] fn main() { // Trait for defining a block pub trait BlockDef { /// Create a new block instance /// /// This receives the name and parameters of the associated block as specified in the /// Pictorus app UI. fn new(name: &'static str, params: &dyn Index<&str, Output = BlockParam>) -> Self; /// Run a single iteration of this block /// /// This receives a list of inputs corresponding to upstream blocks passing data into this block /// and a list of outputs corresponding to data that will be passed to downstream blocks. /// /// Each iteration of this block should modify the output data in place to reflect the current state fn run(&mut self, inputs: &[impl BlockDataRead], outputs: &mut [impl BlockDataWrite]); } }
The new()
function takes a name and a map of parameters as input and should return a new instance of the block. The parameter input maps parameter names as &str
to an instance of the BlockParam
enum:
#![allow(unused)] fn main() { /// Param types passed into block constructors #[derive(Debug, Clone)] pub enum BlockParam<'a> { /// Scalar number value Number(f64), /// String value String(&'a str), /// Matrix value as a tuple of (nrows, ncols, `Vec<f64>`) Matrix(usize, usize, &'a [f64]), } impl BlockParam<'_> { pub fn as_number(&self) -> Option<f64> { match self { BlockParam::Number(n) => Some(*n), _ => None, } } pub fn as_string(&self) -> Option<&str> { match self { BlockParam::String(s) => Some(s), _ => None, } } pub fn as_matrix(&self) -> Option<(usize, usize, &[f64])> { match self { BlockParam::Matrix(nrows, ncols, data) => Some((*nrows, *ncols, data)), _ => None, } } } }
The run()
function is called once per iteration of the block. It receives an immutable slice of inputs and a mutable slice of outputs. A custom block should modify the output data in place to reflect the results of the block update. The number of Inputs and Outputs it determined by the options set in the "Block Definition" panel of the editor. The "Block Definition" also defines the element order of the slices, as is standard in Rust they are zero indexed. Even if you have only one input or output they will still be provided as slices of length 1 (i.e. they can be accessed as inputs[0]
or outputs[0]
respectively).
The input slice contains BlockDataRead
trait objects. This trait is implemented by the BlockData
enum:
#![allow(unused)] fn main() { // Traits for setting and retrieving block data pub trait BlockDataRead { /// Retrieve a scalar value fn get_scalar(&self) -> f64; /// Retrieve a matrix value as a tuple of (nrows, ncols, &[f64]) /// Data is output in column-major order /// For example, the matrix: /// | 1.0 2.0 | /// | 3.0 4.0 | /// /// will be returned as (2, 2, &[1.0, 3.0, 2.0, 4.0]) fn get_matrix(&self) -> (usize, usize, &[f64]); } }
The mutable output slice contains BlockDataWrite
trait objects. This trait is implemented by the BlockData
enum:
#![allow(unused)] fn main() { pub trait BlockDataWrite { /// Set a scalar value fn set_scalar_value(&mut self, value: f64); /// Set a matrix value /// Data is input in column-major order /// For example, set_matrix_value(2, 2, &[1.0, 3.0, 2.0, 4.0]) would set the matrixdata to: /// | 1.0 2.0 | /// | 3.0 4.0 | fn set_matrix_value(&mut self, nrows: usize, ncols: usize, data: &[f64]); } }
The size and shape of the input and output slices will be determined by the block's inputs and outputs as defined in the "Block Definition" tab of the block editor.