Set Connectivity Check
Problem
In modular construction, components are grouped into "sets" by an IFC property. A set is only valid if its members are physically connected - a stray part breaks assembly and QA. Solibri has no built-in check for connectivity inside a set.
Approach
A custom Java rule that groups components by the set property and merges touching members into clusters with union-find. A pair counts as connected when the real geometry is within tolerance - or, for nested parts like a column sunk into a wall, when the boxes interpenetrate and the gap is within an embedded clearance.
Parameters
| Components | what to check |
| Set Property | the IFC property that defines set membership |
| Distance Tolerance | largest gap still counted as touching (0.01 m) |
| Embedded Clearance | tolerance for nested members (0.05 m) |
Result
The largest cluster is the main group; every other cluster is reported once, with its size, the nearest member of the main group and the distance to it. On a 56,000-component model the first, exhaustive version took about four hours - for a fully connected set of 200 members union-find cuts the exact distance calls from 19,900 to 199.
How the code works
if (touch) {
dsu.union(i, j);
if (dsu.getClusterCount() == 1) {
break scan; // everything is connected - no disconnected cluster can exist
}
}
// Union-find with path halving: members already in one cluster
// never need the expensive exact distance test again.
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
- Broad phaseThe bounding-box gap is a lower bound on the real distance - far-apart pairs are dropped cheaply before any solid geometry is touched.
- Narrow phaseComponent.distance() is the true surface-to-surface distance between the solids, the most expensive call in the rule.
- Union-find, not BFSPairs already in the same cluster are skipped, and the scan stops as soon as the whole set has merged. The clusters come out identical to the exhaustive version; only work that was certain to be redundant is saved.
Duplicate Check
Problem
Solibri's built-in intersection rule lists every duplicate with no way to filter - yet some duplicates are legitimate: objects in the same place that differ in their own type or subtype. Coordinators end up wading through hundreds of false alarms.
Approach
A custom Java rule that only complains when ALL nominated distinguishing properties match. Properties are compared before geometry, because reading a property is a table lookup while an intersection test has to cut solids.
Parameters
| Components | the set duplicates are looked for in |
| Distinguishing Property 1..4 | a difference here makes a duplicate legitimate |
Result
One result per pair, listing the compared properties and their values. If a chosen property exists nowhere in the model, the rule says so loudly instead of returning a quiet "no errors".
How the code works
for (Component target : SMC.getModel().getComponents(candidateFilter)) {
// Look at each pair once, from its lower-GUID member.
if (component.getGUID().compareTo(target.getGUID()) >= 0) {
continue;
}
// Properties before geometry: a lookup is cheap, intersecting solids is not.
if (!PropertyMatcher.allMatch(ownValues, valuesOf(target))) {
continue;
}
if (!isDuplicatePair(component, target)) {
continue;
}
results.add(createResult(component, target, ownValues, resultFactory));
}
- Each pair onceTaking the pair only from the member with the smaller GUID halves the work and drops the component matching itself.
- Cheap test firstA pair told apart by a property is not an error, so it is dropped without touching geometry at all.
- Solibri decides 'duplicate'The geometric criterion is left to Intersection.isDuplicate(), so the rule behaves exactly like the built-in one - only with the legitimate cases filtered out.
Centered Between Check
Problem
"This window must sit midway between the beams to its left and right." Built-in Solibri rules measure the distance to ONE object, and "in the middle" cannot be expressed that way - the whole point is comparing two distances.
Approach
For each object the rule finds its nearest reference object by real geometry, takes the direction towards it, then looks for the nearest reference on the opposite side within an angular tolerance - and compares the two distances. It works from a direction vector, not coordinate axes, so a model rotated by any angle gives the same result.
Parameters
| Components / Reference Components | e.g. windows / beams |
| Search Radius | beyond this nothing is looked for (2.0 m) |
| Distance Tolerance | how far the two distances may differ (5 mm) |
| Opposite Direction Tolerance | deviation from exactly opposite, in degrees |
Result
Off-centre objects are flagged with BOTH reference objects and both distances in the description. A missing opposite partner is an error too - an unchecked object must not look checked.
How the code works
static boolean isOpposite(Candidate reference, Candidate candidate, double toleranceDegrees) {
double referenceLength = reference.offsetLength();
double candidateLength = candidate.offsetLength();
if (referenceLength == 0.0 || candidateLength == 0.0) {
return false;
}
double dot = reference.dx * candidate.dx
+ reference.dy * candidate.dy
+ reference.dz * candidate.dz;
double cosine = dot / (referenceLength * candidateLength);
double tolerance = Math.min(Math.max(toleranceDegrees, 0.0), MAX_TOLERANCE_DEGREES);
return cosine <= -Math.cos(Math.toRadians(tolerance));
}
- Direction, not axesThe first version picked X, Y or Z - and broke on a wall at 37 degrees, where the 'main axis' became a coin toss. A dot product between direction vectors is rotation-invariant.
- Box vs. sphereCandidates come from a bounding-box filter, then are cut down by real distance, because a box grows as a cube while the search radius is a sphere.
- Capped toleranceAbove 90 degrees 'opposite' would start letting through objects on the same side, so the tolerance is clamped instead of trusted.
Cross-Model Comparison Check
Problem
Our model and the fabricator's model must agree on the small things - holes, screws, bolts, openings. A few millimetres of drift only shows up on site, and nothing in Solibri pairs "our hole" with "their hole" and measures the difference.
Approach
The rule pairs each of our objects with its counterpart in the fabricator's model and measures how far the centres drifted and how the overall size differs. Pairing is greedy by distance: the closest pair is fixed first and both members leave the pool. Objects left without a partner are reported from both sides.
Parameters
| Reference / Compared Components | our objects / the fabricator's |
| Search Radius | beyond this no partner is looked for (50 mm) |
| Tolerance | one tolerance for displacement and size (2 mm) |
Result
Exactly one result per diverging pair, with displacement and size difference to one decimal place - plus "no counterpart" and "extra object" for the unpaired. Confirmed on a real federated model.
How the code works
public static Matching match(List<Box> reference, List<Box> compared, double radius) {
int[] comparedForReference = new int[reference.size()];
int[] referenceForCompared = new int[compared.size()];
Arrays.fill(comparedForReference, -1);
Arrays.fill(referenceForCompared, -1);
List<Candidate> candidates = collectCandidates(reference, compared, radius);
sortByDistanceThenId(candidates, reference, compared);
for (Candidate candidate : candidates) {
if (comparedForReference[candidate.referenceIndex] != -1
|| referenceForCompared[candidate.comparedIndex] != -1) {
continue; // one of the two is already taken by a closer pair
}
comparedForReference[candidate.referenceIndex] = candidate.comparedIndex;
referenceForCompared[candidate.comparedIndex] = candidate.referenceIndex;
}
return new Matching(comparedForReference, referenceForCompared);
}
- Sweep, not n×mThe compared boxes are sorted by X once; each reference box looks only at the slice within the radius, found by binary search - then the real distance filters the slab down to a sphere.
- Greedy, on purposeNot 'everyone takes their nearest' - in a row of holes two of ours would grab the same one of theirs. Not the Hungarian algorithm either - minimising the total, it would move the blame from the hole that really moved onto its neighbours.
- One finding, not twoThe first real run showed that position and size errors are one physical fact: a hole that grows by d on one side moves its centre by d/2. So they are reported together.
Cross-Model Property Match
Problem
A facade is painted in sections, and the colour of the timber framing around each panel is assigned by hand in the fabricator's model - a manual step where mistakes happen. There is no one-to-one pairing: one panel can be surrounded by several pieces of framing.
Approach
For each object in our model the rule finds every neighbour from the fabricator's model within a search radius and compares a chosen property on both sides - numbers within an epsilon, text case-insensitively. Entity values (building, storey) are unwrapped to their names, and the federated-model prefix Solibri adds is stripped.
Parameters
| Component A / Component B | e.g. our cladding / the fabricator's framing |
| Property on A / Property on B | usually named differently on each side |
| Search Radius | beyond this no neighbour is checked (10 mm) |
| Group Results By | collect results under A or under B |
Result
Mismatches carry the VALUES in the title, both objects in the description, and are drawn red against blue so the pair is readable for colour-blind viewers too. A missing value is never a match - otherwise a typo in a property name would pass every pair silently.
How the code works
static Outcome compare(Optional<String> valueA, Optional<String> valueB) {
if (valueA.isEmpty() && valueB.isEmpty()) {
return Outcome.MISSING_BOTH; // not a match - a typo must not pass silently
}
if (valueA.isEmpty()) {
return Outcome.MISSING_A;
}
if (valueB.isEmpty()) {
return Outcome.MISSING_B;
}
return valuesEqual(valueA.get(), valueB.get()) ? Outcome.MATCH : Outcome.MISMATCH;
}
// Numbers within EPSILON = 1e-3, otherwise case-insensitive text.
- Learned on a real modelThe first run gave 4,282 mismatches out of 4,282 pairs: Solibri returned the building entity itself, whose toString is an internal ID unique per model. The value is now unwrapped through getName().
- Federated prefixesIn a federated model Solibri prefixes names with the short model name - the same building read as '(B) ...' against '(A) ...'. The prefix is read off the carrying component and stripped.
- Readable output4,282 results over 142 neighbours could not be read. A Group Results By parameter folds them up per object A or per neighbour B.