FCS Boolean Operations and CSG
Constructive Solid Geometry (CSG) lets you combine or subtract 3-D volumes. FCS also supports 2-D area islands (holes inside a boundary polygon).
1. Volume Primitives
Before performing CSG you need source volumes.
# vertex-bounded prism (two curve-index ranges: bottom face points, then top)
volume {vo1} prism 1 3 # uses vertices/curves defined elsewhere
volume {vo2} prism 2 4
Optional metadata can be tagged onto any volume:
volume {vo1} prism 1 3 metadata {A:=1}
2. CSG Operations
| Operator | Meaning |
|---|---|
a-b |
a minus b (subtract) |
a+b |
union of a and b |
a*b |
intersection of a and b |
# subtract vo2 from vo1, result stored in vo3
volume {vo3} csg (vo1-vo2)
# union (parentheses group the expression)
volume {voU} csg (vo1+vo2)
# intersection
volume {voI} csg (vo1*vo2)
# chained operations
volume {voR} csg ((vo1+vo2)-vo3)
Source files
TestData/new-Booleans/boxes.fcsTestData/new-Booleans/boxes2.fcs
3. Debug Mode
debug_boolean writes diagnostic output for the two operands and can be placed before the csg expression to investigate failures:
debug_boolean vo2 vo1
volume {vo3} csg (vo1-vo2)
This prints intersection metadata (face count, vertex count, tolerance used) to the FLI log.
4. Tolerance
The default CSG tolerance is 1e-8. Models with very small features may need vertices to be at least 1e-7 apart to avoid degenerate intersections.
5. 2-D Area Islands (Holes)
An area can contain holes. A curve listed with a minus sign in the boundary declaration is an inner boundary (hole), whose winding must be opposite to the outer boundary.
# outer rectangle boundary: c1 (counter-clockwise)
# inner hole: c2 (clockwise, i.e. the reverse wind)
area {a1} boundary +c1 -c2
A more complex example with two holes:
area {a1} boundary +c1 -c2 -c3
Orientation convention
- Outer boundary: add curves with
+(or just the bare name) in counter-clockwise order. - Inner boundaries (islands/holes): prefix with
-and traverse clockwise.
Source files
TestData/new-UnionIslands/—00805f8f-07978.fcsand similar
6. Multi-Body Interaction
When volumes share a face (e.g., adjacent rooms), ensure face vertices are identical — do not rely on tolerance-based snapping. Reuse the same vertex references when possible.
7. Known Pitfalls
| Pitfall | Remedy |
|---|---|
| CSG result vanishes | Check that the volumes actually overlap; run debug_boolean |
| Island area fails mesh | Verify inner-boundary curve is exactly opposite winding |
| Tolerance errors | Ensure shared edges share the same vertex objects |
| Very thin slivers after subtract | Add a small extend margin before subtracting, or merge near-coincident vertices |
8. Relevant Test Folders
| Folder pattern | Coverage |
|---|---|
new-Booleans/ |
Basic CSG subtract + debug |
new-UnionIslands/ |
2-D island areas |
test-Boolean3D-*/ (≈14 folders) |
3-D CSG corner cases |
bug-Boolean3D-*/ (≈10 folders) |
Historical CSG bug reproductions |
9. Feature-Based Boolean Operations (Preferred Approach)
The low-level csg keyword (sections 1–3) works on raw volumes inside a single file. In practice, multi-file parametric models use a higher-level mechanism instead: Features metadata on gblock.
Features are declared on a gblock and the engine applies the boolean operations automatically when the block geometry is resolved. This approach is far more common in real HiStruct/FCC configurators.
9.1 Core Feature Types
| Feature Type | Effect | Key Fields |
|---|---|---|
BooleanOpeningsFeature |
Subtracts one or more opening volumes from the gblock's geometry | Openings, FilterByLayerNames, AsBpt, MergeResultingSurfaces, StrictAssertCheckVolume, WithVolumeTreshold |
BooleanIntersectionFeature |
Clips (intersects) the gblock's geometry to a boundary volume | Boundaries, FilterByLayerNames |
AddCompositeFeature |
Merges (unions) a list of component gblocks into one solid | Components |
9.2 BooleanOpeningsFeature — Cutting Openings
# Minimal: subtract a cutting-volume gblock from all geometry in gbWall
gblock {gbWall} gclass {gWall} lcs {...} parameters {...} metadata {
Features := [
BooleanOpeningsFeature {
Openings = [gbCuttingVolume]
}
]
}
Full field example (from test-Boolean3DrooferIntersectionMetadata/MainTest.fcs):
gblock {gbSolids} gclass mainBlocks[0] metadata {
Features := [
BooleanOpeningsFeature {
AsBpt = True, # boundary-point representation (panels)
MergeResultingSurfaces = True,
StrictAssertCheckVolume = True,
WithVolumeTreshold = 1.0e-8,
Openings = openingBlocks
}
]
}
FilterByLayerNames — target specific layers
When a gblock contains many sub-solids in different layers (wall panels, flashings, plinth, etc.), you can restrict which layers are cut using FilterByLayerNames. This is the standard pattern in building facades:
gblock {gbBuilding} gclass {gBuilding} lcs {...} parameters {...} metadata {
Features := [
BooleanOpeningsFeature {
Openings = [
dOpeningFront.GetGeometryFromLayerName("LayerCuttingVolume"),
dOpeningBack.GetGeometryFromLayerName("LayerCuttingVolume"),
dOpeningLeft.GetGeometryFromLayerName("LayerCuttingVolume"),
dOpeningRight.GetGeometryFromLayerName("LayerCuttingVolume")
],
FilterByLayerNames = [ "LayerWallPanel", "LayerWallPanel1",
"LayerWallPanel2", "LayerFlashings", "LayerPlinth" ]
}
]
}
GetGeometryFromLayerName("LayerCuttingVolume") extracts only the solids tagged in that layer from a distribution. This separates the cutting-volume geometry from the visual geometry of the opening component.
Multiple independent features per gblock
Each feature in the array is applied independently, so you can target different layer sets with different cutting volumes in the same gblock:
gblock {gbWallBack} gclass {gWall} lcs {...} parameters {...} metadata {
Features := [
FeatureCuttingRoof(BackWall.isAttic), # pre-computed lambda: cuts roof slope
FeatureCuttingCorner, # pre-computed variable: cuts corner flashing
FeatureCuttingOpeningBack # pre-computed variable: cuts door/window openings
]
}
9.3 Reusable Feature Variables
It is idiomatic to define feature objects as named variables and reuse them across multiple gblocks:
# Defined once near the top of the configurator file
FeatureCuttingSkyLightRidge := BooleanOpeningsFeature {
Openings = [gbCuttingVolumeSkyLight],
FilterByLayerNames = ["LayerRoofPanel", "LayerFlashings"]
}
FeatureCuttingCorner := BooleanOpeningsFeature {
Openings = [gbCuttingVolumeRoof],
FilterByLayerNames = ["LayerFlashingPanelCorner"]
}
# Applied to multiple gblocks
gblock {gbRoofLeft} gclass {gRoof} lcs {...} metadata { Features := [FeatureCuttingSkyLightRidge] }
gblock {gbRoofRight} gclass {gRoof} lcs {...} metadata { Features := [FeatureCuttingSkyLightRidge] }
gblock {gbRidgeFlash} gclass {gFlashRidge} lcs {...} metadata { Features := [FeatureCuttingSkyLightRidge] }
9.4 Conditional Features
Features can be empty-list conditional so no cutting happens when not needed:
# Ternary: apply cutting feature only when skylight is configured
gblock {gbRoofPanelsLeft} gclass {gRoofPanels} lcs {...} metadata {
Features := (Building.isSkyLight) ? ([BooleanOpeningsFeature{ Openings := [gbCuttingVolumeSkyLight] }]) : ([])
}
Or using a lambda to select between two pre-built feature objects:
FeatureCuttingRoof := isAttic => (
(isAttic) ? (FeatureCuttingAttic)
: (FeatureCuttingSlope)
)
gblock {gbWallRight} gclass {gWall} lcs {...} metadata {
Features := [(LeftWall.isAttic) ? (FeatureCuttingAttic) : (FeatureCuttingSlope), FeatureCuttingOpeningLeft]
}
9.5 BooleanIntersectionFeature — Clipping to a Boundary
BooleanIntersectionFeature retains only the part of the gblock geometry that is inside the boundary volumes. Used for clipping a structural segment to the extent of a building zone:
FeatureIntersectionAll := BooleanIntersectionFeature {
Boundaries := boundaryBlocks
}
# With layer filter (only clip specific sub-solids)
FeatureIntersectionLayers := BooleanIntersectionFeature {
Boundaries := boundaryBlocks,
FilterByLayerNames = cuttedLayers
}
# Conditionally include
FeaturesIntersection := (boundaryBlocks.Count > 0) ? ([FeatureIntersectionAll]) : ([])
9.6 Reusable Worker-Class Pattern
For large configurators, the features + gblock wiring is factored into a dedicated helper class (PlaneSegmentBooleanWorker):
# PlaneSegmentBooleanWorker.fcs (parameterized inputs)
segment := {} # the gclass to cut
AllFeatures := [] # array of features to apply
segmentLcs := GCS
gblock {gbSegment} gclass (segment) lcs (segmentLcs) metadata {
Features := AllFeatures
}
Calling side:
AllFeatures := FeaturesIntersection + FeaturesOpenings
cuttedSegment := PlaneSegmentBooleanWorker {
segment,
AllFeatures,
segmentLcs
}
gblock {gbSegment} gclass (cuttedSegment)
The + operator concatenates feature arrays, enabling composable feature pipelines.
9.7 Low-Level FeatureSolver API
Rarely used directly; useful for debug-dump files or procedural scripts:
features = [
BooleanIntersectionFeature { Boundaries := [...] },
BooleanOpeningsFeature { Openings := [...] }
]
result = Fcs.Geometry.Features.FeatureSolver(cmp1, features)
Volume metadata can also declare features (seen in dump files):
meta00 := { Features = [FCS.Library.Geometry.BooleanIntersectionFeature, FCS.Library.Geometry.BooleanOpeningsFeature] }
9.8 When to Use Each Approach
| Scenario | Approach |
|---|---|
| Simple script, single file, raw geometry testing | volume csg (a-b) |
| Parametric gblock component, subtract a door/window opening from a wall panel | BooleanOpeningsFeature on the wall gblock |
| Clip a building segment to a zone boundary | BooleanIntersectionFeature on the segment gblock |
| Different opening types (panels vs flashings) need separate cuts | Multiple BooleanOpeningsFeature entries with different FilterByLayerNames |
| Opening volume lives alongside visual geometry in a distribution | dOpening.GetGeometryFromLayerName("LayerCuttingVolume") |
| Cut is conditional on a parameter (skylight on/off) | Ternary Features := (cond)?([feat]):([]) |
9.9 Relevant Source Files
| File | Pattern demonstrated |
|---|---|
TestData/test-Boolean3DrooferIntersectionMetadata/MainTest.fcs |
BooleanOpeningsFeature with all boolean control fields |
TestData/bug-Boolean3D_20240913/BooleanSolid.fcs |
BooleanOpeningsFeature minimal |
TestData/bug-NoCut/Configurator_SheatedBuildings_Geometry/BuildingDuoPitchClearSpan.fcs |
Named feature variables, multiple features per gblock, conditional lambda |
TestData/benchmark-HBC-2/.../PlaneSegmentBoolean.fcs |
BooleanIntersectionFeature + BooleanOpeningsFeature combined, conditional arrays |
TestData/bug-HbcCycRef/_Common/BuildingTools/PlaneSegmentBooleanWorker.fcs |
Worker-class pattern |
LabComponents/.../Sheated_Building_DuoPitch.fcs |
GetGeometryFromLayerName, FilterByLayerNames on distribution openings |
LabComponents/.../Unihal_Buildings_Geometry_SimpleHall.fcs |
Conditional ternary feature on each gblock |
TestData/bug-Boolean3D_20230119/Demo_sub.fcs |
AddCompositeFeature{Components=[...]} |
10. Quick Reference
# --- CSG (single-file, raw volumes) ---
volume {result} csg (body1 - body2) # subtraction
volume {result} csg (body1 + body2) # union
volume {result} csg (body1 * body2) # intersection
debug_boolean body2 body1
# --- Island area ---
area {aResult} boundary +cOuter -cHole1 -cHole2
# --- Feature: subtract opening from gblock (all layers) ---
gblock {gb} gclass {gc} lcs {...} metadata {
Features := [BooleanOpeningsFeature{ Openings = [gbCuttingVolume] }]
}
# --- Feature: subtract opening, specific layers only ---
gblock {gb} gclass {gc} lcs {...} metadata {
Features := [
BooleanOpeningsFeature {
Openings = [dOpening.GetGeometryFromLayerName("LayerCuttingVolume")],
FilterByLayerNames = ["LayerWallPanel", "LayerFlashings"]
}
]
}
# --- Feature: clip to boundary ---
gblock {gb} gclass {gc} lcs {...} metadata {
Features := [BooleanIntersectionFeature{ Boundaries := [gbBoundary] }]
}
# --- Feature: conditional ---
gblock {gb} gclass {gc} lcs {...} metadata {
Features := (isOn) ? ([BooleanOpeningsFeature{Openings=[gbCut]}]) : ([])
}
# --- Feature: multiple features ---
gblock {gb} gclass {gc} lcs {...} metadata {
Features := [featureA, featureB, featureC]
}
# --- Combined array via + operator ---
AllFeatures := FeaturesIntersection + FeaturesOpenings