Solibri SDP (API) - Code migration guide (Pre-Beta to Beta)

The first beta release of the Solibri SDP brings substantial improvements in structure, features and usability of the rule development API. With those changes comes the need to break compatibility with the previous pre-beta releases of the API, requiring manual migration of the previously implemented custom rules. This document is provided as a guide to help carrying out that process, presenting the new rule development concepts in relation to the old ones. Each section of the document describes how a specific concept was implemented in the pre-beta and how it has been replaced in the first beta.


Access to API functionality

In the pre-beta:

A custom rule implementation would extend AbstractRule, which provided access to API functionalities via a Checking "context" object, containing "services" retrieved through getContext().get<Service Name>(). Those services would provide specific utilities, like model search, distance calculation, geometry intersection and other features.

In the current beta:

API functionality is directly accessible through the instances of the types it relates to, without passing through an intermediate checking context. The various services have been removed and replaced as follows:

  • DistanceService: Distance between components or components and points can now be computed directly from the instances of the involved components.
    In the Component interface, the methods distance, horizontalDistance, verticalDistance and projectionOnXYPlaneDistance can be used to compute distances between components and components and points.
    If instead the shortest path is required not simply as a double, but instead as a segment with the appropriate starting and ending point of the distance measurement, the methods shortestPathTo, shortestHorizontalPathTo, shortestVerticalPathTo, shortest2dPathInXYPlaneProjectionTo in TriangleMesh can be used.

    Examples:
// Compute the distance between two components
double distanceAB = componentA.distance(componentB).orElse(0.0);

// Compute the distance between a component and a point
double distanceToPoint = componentA.distance(point).orElse(0.0);

// Compute the closest points between a component and another
Segment3d shortestPath = componentA.getTriangleMesh().shortestPath(componentB.getTriangleMesh());
Vector3d closestPointOnA = shortestPath.getStartPoint();
Vector3d closestPointOnB = shortestPath.getEndPoint();
  • SearchService: Spatial searches are now performed using filters, while the bounding box of a component is retrieved through component.getBoundingBox().
    Examples of usages of filters for spatial search of components:
/*
 * Compute the distance between all the components within a certain
 * distance from another component, and that also satisfy a given filter
 */
ComponentFilter withinDistanceFilter = AABBIntersectionFilter
    .ofComponentBounds(component, searchDistance, searchDistance)
    .and(anotherFilter);
  • FootprintService: The footprint of components can now be retrieved directly from the Component interface, through the getFootprint() method.
    Examples:
// Compute the footprint of a component
Footprint footprint = component.getFootprint();
  • UnitService: The format of a PropertyType can now be retrieved directly from the PropertyType's getFormat() methods.
    Examples:
double componentHeight = component.getBoundingBox().getSizeZ();

/*
* Create a formatted string of the component's height. The format uses the
* units that are set in the application settings.
*/
String formattedComponentHeight = PropertyType.LENGTH.getFormat().format(componentHeight);
  • DisplayNameService: The displayable name of the component can now be retrieved from component.getName().

  • GeometryService: The PolygonBrep interface has been replaced by TriangleMesh (a mesh of triangles representing the surface of a component). The TriangleMesh of a component can be retrieved through component.getTriangleMesh().

  • BuildingElementService: Building elements are now represented by interfaces extending components. Specialized building element components can be retrieved via the SMC.getModel().getComponents(ComponentFilter, Class) method, or can be obtained from a Component with an instanceof check followed by a cast to the appropriate building element interface.
    Examples:

// Retrieve all the walls in the model
Collection<Wall> walls = SMC.getModel().getComponents(ComponentFilter.ACCEPT_ALL, Wall.class);
// Given a component, check if it is a stair and retrieve the bottom flight if present
if (component instanceof Stair) {
    Stair stair = (Stair) component;
    Optional<StairFlight> optionalStairFlight = stair.getBottomFlight();
    if (optionalStairFlight.isPresent()) {
    	StairFlight bottomFlight = optionalStairFlight.get();
    	// ... Do something with the stair flight
    }
}
  • IntersectionService: Information about intersections of components can now be retrieved directly through the Component's getIntersections method. To simply check if two component's surfaces geometrically intersect, use the faster TriangleMesh's "intersects" method instead. Examples:
// Retrieve intersection information about two components
Set<Intersection> intersectionsAB = componentA.getIntersections(componentB);
// Check if two components intersect
if (componentA.getTriangleMesh().intersects(componentB.getTriangleMesh())) {
	// The two components intersect
} else {
	// The two components do not intersect
}
  • VisualizationItemFactory: The VisualizationItem objects are now created via static methods. The supported VisualizationItem types are contained in the "visualization" package. VisualizationItemFactory is still available in the API but should not be directly used, as it is now marked as internal and not supported by Solibri.
    Examples:
// Create a Text VisualizationItem
Text text = Text.create(color, normal, up, position, horizontalScaling, verticalScaling, text, size);
  • ReadModelService: The functionality in this interface was not implemented and it is not fully provided in the current version.
    A list of the functionality promised by ReadModelService that can be achieved through other means:
  • ReadModelService's getEntityType(Entity) is replaced by Component's getComponentType() method;
  • ReadModelService's getGuid(Entity) is replaced by Component's getGUID() method;
  • ReadModelService's getRelatedEntities(...) is replaced by Component's getRelated(Relation) method;
  • ReadModelService's getEntities() is replaced by SMC.getModel().getComponents(ComponentFilter, Class) method;
  • ReadModelService's getValue(Entity, PropertyReference) is replaced by Component's getPropertyValue(PropertyReference) method;
  • ReadModelService's getPsetPropertyValue functionality is replaced by Component's getPropertySets method and the resulting PropertySet objects;


  • ParameterService: Parameter for rules are now created via the RuleParameters object, retrievable by calling RuleParameters.of(this) in the constructor of a Rule. See the example rules.


  • ResultService: Results for a Rule are now created using the ResultFactory object received as parameter in the overridden check method of a custom rule implementation. See the example rules.


The checking flow

In the pre-beta:

A custom rule implementation needed to extend AbstractRule, which provided overridable methods preCheck(), check(Entity) and postCheck(). By overriding this methods a custom rule's developer was able to implement a restricted checking flow that required the check method to be executed once for every Entity. Sometimes, when data from multiple entities was needed, it was necessary to implement some of the checking logic in preCheck() or postCheck().

In the current beta:

The checking flow is now more general, to avoid having to implement checking logic in preCheck() and postCheck() when data from multiple components needs to be checked at the same time.

Implement a simple one by one rule in the current beta

To implement a simple rule that checks one by one components coming from a filter, the OneByOneRule abstract class can be extended. A default filter parameter will be provided. All the components passing the filter contained in the default filter parameter will be passed to the check method one by one. Before the rule is executed, if the default filter produces at least one component, the preCheck() method is called. If this method is overridden a PreCheckResult object must be returned, indicating whether the rule should be executed or not. The preCheck() method is NOT meant to carry out any other computation or check, and all the component checking logic should be located in the check method. When implementing check, a collection of Result objects must be returned for each component. These will constitute the results shown in the SMC UI after running the rule. After the rule has been run, the postCheck() method is going to be executed. This method meant for resource cleanup and can be used also to provide actions to be executed after the rule has been run. This method is not meant for component's checking, and Result objects cannot be produced there. If an uncaught exception is produced in this method, it will not cause an error to be displayed in the checking results, but it will instead produce a log message.

Example:

/**
* Example of the structure of a OneByOneRule.
*/
class ExampleRule extends OneByOneRule {
    /**
    * Overriding this method is not mandatory.
    * The method will be called only if the default filter parameter filled in by the user
    * in the properties window of the rule produces some component.
    * The returned PreCheckResult will determine if the checking is executed or not.
    */
    @Override 
    public PreCheckResult preCheck() {
        // Code that verifies if the rule should be run
        if (<some condition>) {
        	return PreCheckResult.createIrrelevant();
        } else {
            return PreCheckResult.createRelevant();
        }
    }
    
    /**
    * If the PreCheckResult was relevant, each component that passed the default filter will
    * be passed as parameter of this method, one by one.
    * For each component being checked the method should return a collection of results.
    * A Result is created using the ResultFactory received as method parameter.
    */
    @Override
    public Collection<Result> check(Component component, ResultFactory resultFactory) {
        return Collections.singleton(
            resultFactory.create("result name", "result description for component " + component.getName())
        );
    }
    
    /**
     * Overriding this method is not mandatory.
     * The method will be called after the rule has been run, even if PreCheckResult has produced
     * an irrelevant result.
     * Resources that require explicit cleanup can be released here and post checking actions can 
     * be executed from here. If an uncaught exception is produced in this method, it will not cause
     * an error to be displayed in the checking results, but it will instead produce a log message.
     * This method is not meant for component checking, and its not possible to create new checking
     * results from here.
     */
    @Override 
    public void postCheck() {
        //Cleanup and post checking actions
    }
}

What if instead we need to execute more global checks that do not fit a one by one flow? For example, imagine a situation in which the height of single components passing a filter should be compared with the average height of all the components passing a filter, producing a problem result in case the component has a height exceeding the average by a certain tolerance. This requires a global view of all the components, to be able to compute the average height before checking each individual one.
To tackle this kind of situation, generalized checking is provided and described below.

Implement a more general checking flow in the current beta

To implement a rule with generalized checking flow (not forced to be one by one), instead of extending OneByOneRule, the Rule interface will have to be implemented. The structure of the custom rule is similar as before, with preCheck, check and postCheck methods, but this time there is no predefined default filter parameter, components are not passed to check one by one and results are not returned from the check method, but registered explicitly during the checking using CheckingSelection (allowing to accurately update the checking progress bar while checking). The components to be passed to check as selected in the preCheck method, using ComponentSelector, and passed to the check method via CheckingSelection. Additional components can also be retrieved during the execution of the check method by using SMC.getModel().getComponents(...), but in this case they will not be counted towards the initial maximum of the checking progress bar. When possible, it is advisable to select the components to be checked in the preCheck via ComponentSelector as a preferred method. All the component selected via ComponentSelector must be marked as checked using CheckingSelection's pass or fail methods. If there are selected components not marked as either pass or fail by the end of the checking, an IllegalStateException will be thrown, to help the developer catch possible bugs due to forgetting to check some component. If the developer is confident that the remaining components do not need to be checked, it is possible to mark all the remaining components as passing with a single method call of passRemaining in CheckingSelection.


Example (components exceeding average height):

    /**
    * Example of a rule with generalized checking flow.
    */
    class ExceedingAverageHeightRule implements Rule {
    	
    	/**
    	* Create a parameter that will define the tolerance for height deviation from the average.
        */
    	DoubleParameter toleranceParameter = 
    	    RuleParameters.of(this).createDouble("tolerance", PropertyType.PERCENTAGE);
    	
    	/**
    	* In generalized checking's preCheck method we select the components to be checked by the rule and also verify
    	* if the rule should be executed.
        */
        @Override
        public PreCheckResult preCheck(ComponentSelector components) {
            // Select for checking the components that have a "Pset_OpeningElementCommon" ifc property set
            components.select(component ->
                component
                    .getPropertySets("Pset_OpeningElementCommon")
                    .stream()
                    .findFirst().isPresent()
            );
            
            // Decide if the rule should be executed
            if (components.hasSelectedComponents()) {
                return PreCheckResult.createRelevant();
            } else {
            	return PreCheckResult.createIrrelevant("No components with the required property set.");
            }
        }

        /**
        * In case the PreCheckResult is relevant, the components selected for checking during the preCheck will be
        * available in this method via CheckingSelection. Result objects can be build for these components using
        * ResultFactory, and each component must be marked at least once as either passing or failing with one or 
        * more results, via CheckingSelection. If there are selected components not marked by the end of the
        * checking as either passing or failing, an IllegalStateException will be thrown.
        * Additional components can be retrieved for checking using SMC.getModel().getComponents(...). 
        */
        @Override
        public void check(CheckingSelection components, ResultFactory resultFactory) {
        	double heightSum = 0.0;
        	// Iterate through all the selected components to compute their average height
        	for (Component component : components.getRemaining()) {
        		heightSum += component.getBoundingBox().getSizeZ();
        	}
        	double avgHeight = heightSum / components.getRemaining().size();
        	
        	// Compute the maximum allowed tolerance from the average height given an percentage tolerance
        	double maxAllowedHeight = avgHeight * (1 + toleranceParameter.getValue());
        	
        	// Iterate again, this time to check the components height against the average
        	for (Component component : components.getRemaining()) {
        		if (component.getBoudingBox().getSizeZ() > maxAllowedHeight) {
        			components.fail(component, resultFactory.create("Exceeded height", "The component " + component.getName() + " exceeds the maximum height"));
        		} else {
        			components.pass(component);
        		}
        	}
        	
            /*
             * A call to the following method would mark all the remaining components as passed.
             * Use only when confident that the remaining still unmarked components do not need
             * to be checked.
             */
            // components.passRemaining();
        }
    }


Creating more descriptive results

In the pre-beta:

Result objects were created using ResultService and information about the result was added by mutating the newly created result with setter methods. This allowed to create invalid results, missing basic information, or mutate by mistake a previous result instead of creating a new one.

In the current beta:

Result objects are now immutable and created through the ResultFactory passed as parameter to the check method of the custom rule. ResultFactory will always require at least a name and a description to create a valid result. Additional information can be added by calling method on the newly created result that will create and return a new result copying the original one while adding additional information. This allows to build rich results by chaining methods with the dot notation.

Example:

   class ResultWithAdditionalInfo extends OneByOneRule {
   		@Override
   		public Collection<Result> check(Component component, ResultFactory resultFactory) {
   			return Collections.singleton(
   				resultFactory.create("result name", "result description")
   					.withCategory(resultFactory.createCategory("category name", "category description"))
   					.withCategory(resultFactory.createCategory("another category name", "another category description"))
   					.withLocation("Descriptive location")
   					.withSeverity(Severity.LOW)
   			);
   		}
   	}



WARNING: The following code contains a bug!

    Result result = resultFactory.create("result name", "result description");
    
    /*
     * This is a bug!
     * Calling a method to enrich a result does NOT modify
     * the original result, but produces a new one.
     * The severity of the produced result will NOT be set to LOW.
     */
    result.withSeverity(Severity.LOW);
    
    return Collections.singleton(result);

The correct way to rewrite the code above is:

    Result result = resultFactory.create("result name", "result description");
    result = result.withSeverity(Severity.LOW);
    return Collections.singleton(result);

Or better:

    Result result = resultFactory
        .create("result name", "result description");
        .withSeverity(Severity.LOW);
    return Collections.singleton(result);

Some of the additional pieces of information that can be added to a result are involved components. Involved components are components that are not part of the components that produced the result, but are in some way involved in its creation. They are, by default, visualized with a different color than the components that produced the result. To give an example of their usage, in an accessible route check the component producing the result could be the route component itself (being obstructed), while involved components could be the components obstructing such route. Involved components can be added by using the withInvolvedComponent or withInvolvedComponents methods of Result. The distinction between components producing results and involved components was not present in the pre-beta.


Custom visualizations for checking results

Another change in result handling involves the definition of a custom visualization. It might be useful to create custom visualizations for results, maybe to change the color of the highlighted components, add some text or even create 3d shapes from scratch.

In the pre-beta:

To customize a visualization, a mutable Visualization object was retrieved from a newly created Result, and modifications were applied directly to such object.
An example of old pre-beta code to add a component (called an Entity in the old API) a 3d mesh (called brep in the old API) to a visualization:

    Result result = getContext().getResultService().createResult(entity.getUUID().toString());
    result.setName("Result name");
    result.setDescription("Result description");
    Brep brep = getContext().getVisualizationItemFactory().createBrep(triangles);
    Visualization visualization = result.getVisualization();
    visualization.addEntity(entity, TRANSPARENCY_CONSTANT);
    visualization.addVisualizationItem(brep);

In the current beta:

The process of defining a custom visualization has been adapted to work with immutable results. Given a Result, the withVisualization and withReplacedVisualization are provided. The former can be used to customize the default visualization, while the latter will create one from scratch, discarding any default or previously added visualization. When withVisualization or withReplacedVisualization is called on a Result, an anonymous function taking one argument is required. This anonymous function will received the previous visualization (in case of withVisualization) or an empty visualization (in case of withReplacedVisualization). This received visualization can be mutated by adding VisualizationItem objects to it. A new Result with the newly defined visualization is then going to be returned, while the visualization of the original Result is not going to be affected. Multiple calls of withVisualization will cumulate their effect, while every call of withReplacedVisualization will always start from an empty visualization.

Example - Add a component and a mesh to a result's visualization:

    Result resultA = resultFactory
        .create("result name", "result description")
        .withVisualization(visualization -> 
            visualization.addComponent(component, transparency);
            visualization.addVisualizationItem(Mesh.create(triangles));
   

More examples:

    /*
     * This creates a result that will be visualized with additional text in
     * the model 3d view, while still showing the components that caused the result. 
     */
    Result resultA = resultFactory
        .create("result name", "result description");
        .withVisualization(visualization -> 
            visualization.addVisualizationItem(
                Text.create(color, upDirection, normalDirection, position, 1.0, 1.0, "hello", 1.0)));
        
    /*
     * This creates a result that will be visualized as only text in the model 3d view,
     * while the components causing the result will not be shown.
     */
    Result resultB = resultFactory
        .create("result name", "result description");
        .withReplacedVisualization(visualization ->
            visualization.addVisualizationItem(
                Text.create(color, upDirection, normalDirection, position, 1.0, 1.0, "hello", 1.0)));


Custom unique keys for checking results

In the pre-beta:

Every Result needed to be created with a unique key. If multiple results were created with the same key, they would be merged in the same result, and any modification on one of them would apply to all the other.

In the current beta:

Result do not need anymore a unique key to be provided on creation, since one is automatically generated from name, description, source, involved components and other information. However, it is still possible to specify a unique key for the result, overriding the autogenerated one. This can be useful if the custom rule needs to be localized in multiple languages and the users of such rule need to switch often between languages. In this case, the autogenerated key would be different if the users switched language, since the name and description of the result would be different. Overriding the default unique key could be a solution. To produce a Result with a custom unique key, use the withCustomUniqueKey method of Result. If multiple results with the same unique key are registered, their details will be merged together and the result registered later will override the information of the previous ones.

Example:

    Result resultA = resultFactory
        .create("result name", "result description")
        .withCustomUniqueKey("MYRESULTKEY " + component.getName());