Skip to main content

Finding and repairing broken shapes

Most shapes you model are sound. Trouble tends to arrive with imported files, whose faces another program wrote to its own tolerances, and with faces you put together by hand. bitbybit.occt.shapeFix shows you what is wrong and puts it right.

Inside-out solids​

A solid keeps its material on one side of its faces. If every face points the wrong way the material is on the outside: the volume comes out negative, and the viewer, which draws the back of a face in backFaceColour when the draw options ask for both sides with drawTwoSided, shows the whole solid blue.

Blue on its own proves nothing. A see-through solid shows the inside of its far walls the same way, and an open face shows its back from behind. A negative shapes.solid.getSolidVolume is the test, and it is worth knowing that isValid will not tell you: an inside-out solid is well formed, so it passes.

Here a drum is taken apart, its three faces are reversed and they are sewn back into a solid of volume -169.6. orientClosedSolid turns it into the drum beside it, at +169.6:

Bitbybit Platform

An inside-out solid, turned the right way in Rete

rete logoRete
Script Source (rete)
{
"id": "rete-v2-json",
"nodes": {
"drum": {
"id": "drum",
"name": "bitbybit.occt.shapes.solid.createCylinder",
"customName": "cylinder",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"radius": 3,
"height": 6,
"center": [
0,
0,
0
],
"direction": [
0,
1,
0
],
"angle": 360,
"originOnCenter": false
},
"inputs": {},
"position": [
40,
40
]
},
"face0": {
"id": "face0",
"name": "bitbybit.occt.shapes.face.getFace",
"customName": "get face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"index": 0
},
"inputs": {
"shape": {
"connections": [
{
"node": "drum",
"output": "result",
"data": {}
}
]
}
},
"position": [
460,
40
]
},
"reversed0": {
"id": "reversed0",
"name": "bitbybit.occt.shapes.face.reversedFace",
"customName": "reversed face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"shape": {
"connections": [
{
"node": "face0",
"output": "result",
"data": {}
}
]
}
},
"position": [
880,
40
]
},
"face1": {
"id": "face1",
"name": "bitbybit.occt.shapes.face.getFace",
"customName": "get face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"index": 1
},
"inputs": {
"shape": {
"connections": [
{
"node": "drum",
"output": "result",
"data": {}
}
]
}
},
"position": [
460,
288
]
},
"reversed1": {
"id": "reversed1",
"name": "bitbybit.occt.shapes.face.reversedFace",
"customName": "reversed face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"shape": {
"connections": [
{
"node": "face1",
"output": "result",
"data": {}
}
]
}
},
"position": [
880,
280
]
},
"face2": {
"id": "face2",
"name": "bitbybit.occt.shapes.face.getFace",
"customName": "get face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"index": 2
},
"inputs": {
"shape": {
"connections": [
{
"node": "drum",
"output": "result",
"data": {}
}
]
}
},
"position": [
460,
536
]
},
"reversed2": {
"id": "reversed2",
"name": "bitbybit.occt.shapes.face.reversedFace",
"customName": "reversed face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"shape": {
"connections": [
{
"node": "face2",
"output": "result",
"data": {}
}
]
}
},
"position": [
880,
520
]
},
"reversed": {
"id": "reversed",
"name": "bitbybit.lists.createList",
"customName": "create list",
"async": false,
"drawable": false,
"data": {},
"inputs": {
"listElements": {
"connections": [
{
"node": "reversed0",
"output": "result",
"data": {}
},
{
"node": "reversed1",
"output": "result",
"data": {}
},
{
"node": "reversed2",
"output": "result",
"data": {}
}
]
}
},
"position": [
1300,
40
]
},
"shell": {
"id": "shell",
"name": "bitbybit.occt.shapes.shell.sewFaces",
"customName": "sew faces",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"tolerance": 1e-7
},
"inputs": {
"shapes": {
"connections": [
{
"node": "reversed",
"output": "list",
"data": {}
}
]
}
},
"position": [
1720,
40
]
},
"insideOut": {
"id": "insideOut",
"name": "bitbybit.occt.shapes.solid.fromClosedShell",
"customName": "from closed shell",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"shape": {
"connections": [
{
"node": "shell",
"output": "result",
"data": {}
}
]
}
},
"position": [
2140,
40
]
},
"turned": {
"id": "turned",
"name": "bitbybit.occt.shapeFix.orientClosedSolid",
"customName": "orient closed solid",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"shape": {
"connections": [
{
"node": "insideOut",
"output": "result",
"data": {}
}
]
}
},
"position": [
2560,
40
]
},
"beside": {
"id": "beside",
"name": "bitbybit.occt.transforms.translate",
"customName": "translate",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"translation": [
8,
0,
0
]
},
"inputs": {
"shape": {
"connections": [
{
"node": "turned",
"output": "result",
"data": {}
}
]
}
},
"position": [
2980,
40
]
},
"bright": {
"id": "bright",
"name": "bitbybit.draw.optionsOcctShape",
"customName": "options occt shape",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"faceOpacity": 1,
"edgeOpacity": 1,
"edgeColour": "#5f6773",
"faceColour": "#ff4400",
"vertexColour": "#ff00ff",
"edgeWidth": 2,
"edgeWidthBySize": false,
"vertexSize": 0.03,
"drawEdges": true,
"drawFaces": true,
"drawVertices": false,
"precision": 0.01,
"angularDeflection": 0.5,
"relativeDeflection": false,
"precisionShare": 0,
"drawEdgeIndexes": false,
"edgeIndexHeight": 0.06,
"edgeIndexColour": "#ff00ff",
"drawFaceIndexes": false,
"faceIndexHeight": 0.06,
"faceIndexColour": "#0000ff",
"drawTwoSided": true,
"backFaceColour": "#0000ff",
"backFaceOpacity": 1,
"edgeArrowSize": 0,
"edgeArrowAngle": 15,
"keepMeshData": false,
"allowQualityDecrease": true,
"forceFaceDeflection": false,
"drawIsoCurves": false,
"isoCurvesU": 5,
"isoCurvesV": 5,
"isoCurvesColour": "#808080",
"surfaceAnalysis": "none",
"draftDirection": [
0,
1,
0
]
},
"inputs": {},
"position": [
40,
472
]
},
"drawInsideOut": {
"id": "drawInsideOut",
"name": "bitbybit.draw.drawAnyAsync",
"customName": "draw any async",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"entity": {
"connections": [
{
"node": "insideOut",
"output": "result",
"data": {}
}
]
},
"options": {
"connections": [
{
"node": "bright",
"output": "result",
"data": {}
}
]
}
},
"position": [
3400,
40
]
},
"drawBeside": {
"id": "drawBeside",
"name": "bitbybit.draw.drawAnyAsync",
"customName": "draw any async",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"entity": {
"connections": [
{
"node": "beside",
"output": "result",
"data": {}
}
]
},
"options": {
"connections": [
{
"node": "bright",
"output": "result",
"data": {}
}
]
}
},
"position": [
3400,
334
]
},
"view": {
"id": "view",
"name": "bitbybit.babylon.scene.adjustActiveArcRotateCamera",
"customName": "adjust active arc rotate camera",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"position": [
8,
15,
-25
],
"lookAt": [
4,
3,
0
],
"lowerBetaLimit": 1,
"upperBetaLimit": 179,
"angularSensibilityX": 1000,
"angularSensibilityY": 1000,
"maxZ": 1000,
"panningSensibility": 1000,
"wheelPrecision": 3
},
"inputs": {},
"position": [
3400,
628
]
}
},
"modules": {}
}

To build a solid from faces you do not trust, reach for shapes.solid.fromFaces. It sews them and turns the result so its material is inside, which fromClosedShell does not do.

Both of these turn a solid by setting a flag on it, and tubes from pipeWireCylindrical and gears with a bore come out the same way. Such a solid measures and draws correctly and works in booleans, but filletEdges and chamferEdges lose the flag, and the result comes out broken with a negative volume. To round or chamfer one, sew its faces into a new solid first: shapes.face.getFaces, shapes.shell.sewFaces, then fromClosedShell.

Faces that do not quite meet​

Sewing joins faces along edges that lie within a tolerance of each other. sewWithReport does the same job as shapes.shell.sewFaces and also says how it went: freeEdgeCount counts the edges left bounding a single face, contiguousEdgeCount the edges that were sewn together, and multipleEdgeCount the edges now shared by more than two faces.

freeBoundaries joins the free edges of a shape into wires, closed for those that close on themselves and open for the rest, so you can draw exactly where a shape is open.

These two sheets lie 0.02 apart. At a tolerance of 0.0000001 they stay two sheets with 8 free edges, and each keeps its own red loop. At 0.05 the middle edges are sewn, 6 free edges are left and one loop runs around the pair:

Bitbybit Platform

Two sheets sewn across a gap in Rete

rete logoRete
Script Source (rete)
{
"id": "rete-v2-json",
"nodes": {
"left": {
"id": "left",
"name": "bitbybit.occt.shapes.face.createSquareFace",
"customName": "square face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"size": 5,
"center": [
-2.51,
0,
0
],
"direction": [
0,
1,
0
]
},
"inputs": {},
"position": [
40,
40
]
},
"right": {
"id": "right",
"name": "bitbybit.occt.shapes.face.createSquareFace",
"customName": "square face",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"size": 5,
"center": [
2.51,
0,
0
],
"direction": [
0,
1,
0
]
},
"inputs": {},
"position": [
40,
334
]
},
"sheets": {
"id": "sheets",
"name": "bitbybit.lists.createList",
"customName": "create list",
"async": false,
"drawable": false,
"data": {},
"inputs": {
"listElements": {
"connections": [
{
"node": "left",
"output": "result",
"data": {}
},
{
"node": "right",
"output": "result",
"data": {}
}
]
}
},
"position": [
460,
40
]
},
"tight": {
"id": "tight",
"name": "bitbybit.occt.shapeFix.sewWithReport",
"customName": "sew with report",
"async": true,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"tolerance": 1e-7,
"nonManifold": false
},
"inputs": {
"shapes": {
"connections": [
{
"node": "sheets",
"output": "list",
"data": {}
}
]
}
},
"position": [
880,
40
]
},
"tightSheet": {
"id": "tightSheet",
"name": "bitbybit.json.getValueOnProp",
"customName": "get value on prop",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"property": "shape"
},
"inputs": {
"json": {
"connections": [
{
"node": "tight",
"output": "result",
"data": {}
}
]
}
},
"position": [
1300,
40
]
},
"tightRims": {
"id": "tightRims",
"name": "bitbybit.occt.shapeFix.freeBoundaries",
"customName": "free boundaries",
"async": true,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"tolerance": 1e-7
},
"inputs": {
"shape": {
"connections": [
{
"node": "tightSheet",
"output": "result",
"data": {}
}
]
}
},
"position": [
1720,
40
]
},
"tightLoops": {
"id": "tightLoops",
"name": "bitbybit.json.getValueOnProp",
"customName": "get value on prop",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"property": "closed"
},
"inputs": {
"json": {
"connections": [
{
"node": "tightRims",
"output": "result",
"data": {}
}
]
}
},
"position": [
2140,
40
]
},
"loose": {
"id": "loose",
"name": "bitbybit.occt.shapeFix.sewWithReport",
"customName": "sew with report",
"async": true,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"tolerance": 0.05,
"nonManifold": false
},
"inputs": {
"shapes": {
"connections": [
{
"node": "sheets",
"output": "list",
"data": {}
}
]
}
},
"position": [
880,
334
]
},
"looseSheet": {
"id": "looseSheet",
"name": "bitbybit.json.getValueOnProp",
"customName": "get value on prop",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"property": "shape"
},
"inputs": {
"json": {
"connections": [
{
"node": "loose",
"output": "result",
"data": {}
}
]
}
},
"position": [
1300,
288
]
},
"moved": {
"id": "moved",
"name": "bitbybit.occt.transforms.translate",
"customName": "translate",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": true,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"translation": [
0,
0,
7
]
},
"inputs": {
"shape": {
"connections": [
{
"node": "looseSheet",
"output": "result",
"data": {}
}
]
}
},
"position": [
1720,
288
]
},
"looseRims": {
"id": "looseRims",
"name": "bitbybit.occt.shapeFix.freeBoundaries",
"customName": "free boundaries",
"async": true,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"tolerance": 1e-7
},
"inputs": {
"shape": {
"connections": [
{
"node": "moved",
"output": "result",
"data": {}
}
]
}
},
"position": [
2140,
288
]
},
"looseLoops": {
"id": "looseLoops",
"name": "bitbybit.json.getValueOnProp",
"customName": "get value on prop",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"property": "closed"
},
"inputs": {
"json": {
"connections": [
{
"node": "looseRims",
"output": "result",
"data": {}
}
]
}
},
"position": [
2560,
40
]
},
"sheet": {
"id": "sheet",
"name": "bitbybit.draw.optionsOcctShape",
"customName": "options occt shape",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"faceOpacity": 1,
"edgeOpacity": 1,
"edgeColour": "#5f6773",
"faceColour": "#ffffff",
"vertexColour": "#ff00ff",
"edgeWidth": 2,
"edgeWidthBySize": false,
"vertexSize": 0.03,
"drawEdges": false,
"drawFaces": true,
"drawVertices": false,
"precision": 0.01,
"angularDeflection": 0.5,
"relativeDeflection": false,
"precisionShare": 0,
"drawEdgeIndexes": false,
"edgeIndexHeight": 0.06,
"edgeIndexColour": "#ff00ff",
"drawFaceIndexes": false,
"faceIndexHeight": 0.06,
"faceIndexColour": "#0000ff",
"drawTwoSided": true,
"backFaceColour": "#ffffff",
"backFaceOpacity": 1,
"edgeArrowSize": 0,
"edgeArrowAngle": 15,
"keepMeshData": false,
"allowQualityDecrease": true,
"forceFaceDeflection": false,
"drawIsoCurves": false,
"isoCurvesU": 5,
"isoCurvesV": 5,
"isoCurvesColour": "#808080",
"surfaceAnalysis": "none",
"draftDirection": [
0,
1,
0
]
},
"inputs": {},
"position": [
40,
628
]
},
"rim": {
"id": "rim",
"name": "bitbybit.draw.optionsOcctShape",
"customName": "options occt shape",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"faceOpacity": 1,
"edgeOpacity": 1,
"edgeColour": "#ff2020",
"faceColour": "#ff0000",
"vertexColour": "#ff00ff",
"edgeWidth": 4,
"edgeWidthBySize": false,
"vertexSize": 0.03,
"drawEdges": true,
"drawFaces": true,
"drawVertices": false,
"precision": 0.01,
"angularDeflection": 0.5,
"relativeDeflection": false,
"precisionShare": 0,
"drawEdgeIndexes": false,
"edgeIndexHeight": 0.06,
"edgeIndexColour": "#ff00ff",
"drawFaceIndexes": false,
"faceIndexHeight": 0.06,
"faceIndexColour": "#0000ff",
"drawTwoSided": true,
"backFaceColour": "#0000ff",
"backFaceOpacity": 1,
"edgeArrowSize": 0,
"edgeArrowAngle": 15,
"keepMeshData": false,
"allowQualityDecrease": true,
"forceFaceDeflection": false,
"drawIsoCurves": false,
"isoCurvesU": 5,
"isoCurvesV": 5,
"isoCurvesColour": "#808080",
"surfaceAnalysis": "none",
"draftDirection": [
0,
1,
0
]
},
"inputs": {},
"position": [
40,
2578
]
},
"drawTightSheet": {
"id": "drawTightSheet",
"name": "bitbybit.draw.drawAnyAsync",
"customName": "draw any async",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"entity": {
"connections": [
{
"node": "tightSheet",
"output": "result",
"data": {}
}
]
},
"options": {
"connections": [
{
"node": "sheet",
"output": "result",
"data": {}
}
]
}
},
"position": [
2980,
40
]
},
"drawTightLoops": {
"id": "drawTightLoops",
"name": "bitbybit.draw.drawAnyAsync",
"customName": "draw any async",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"entity": {
"connections": [
{
"node": "tightLoops",
"output": "result",
"data": {}
}
]
},
"options": {
"connections": [
{
"node": "rim",
"output": "result",
"data": {}
}
]
}
},
"position": [
2980,
334
]
},
"drawMoved": {
"id": "drawMoved",
"name": "bitbybit.draw.drawAnyAsync",
"customName": "draw any async",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"entity": {
"connections": [
{
"node": "moved",
"output": "result",
"data": {}
}
]
},
"options": {
"connections": [
{
"node": "sheet",
"output": "result",
"data": {}
}
]
}
},
"position": [
2980,
628
]
},
"drawLooseLoops": {
"id": "drawLooseLoops",
"name": "bitbybit.draw.drawAnyAsync",
"customName": "draw any async",
"async": true,
"drawable": true,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
}
},
"inputs": {
"entity": {
"connections": [
{
"node": "looseLoops",
"output": "result",
"data": {}
}
]
},
"options": {
"connections": [
{
"node": "rim",
"output": "result",
"data": {}
}
]
}
},
"position": [
2980,
922
]
},
"view": {
"id": "view",
"name": "bitbybit.babylon.scene.adjustActiveArcRotateCamera",
"customName": "adjust active arc rotate camera",
"async": false,
"drawable": false,
"data": {
"genericNodeData": {
"hide": false,
"oneOnOne": false,
"flatten": 0,
"forceExecution": false
},
"position": [
0,
17,
-11
],
"lookAt": [
0,
0,
3.5
],
"lowerBetaLimit": 1,
"upperBetaLimit": 179,
"angularSensibilityX": 1000,
"angularSensibilityY": 1000,
"maxZ": 1000,
"panningSensibility": 1000,
"wheelPrecision": 3
},
"inputs": {},
"position": [
2980,
1216
]
}
},
"modules": {}
}

Use a tolerance a little larger than the widest gap. One much larger can join edges that were never meant to meet.

Checking a shape​

isValid answers yes or no. validityReport gives the detail: isValid, a list of faults naming each faulty sub-shape and the checks it fails, and the smallest, largest and average tolerance found in the shape.

A valid shape is well formed, which is not the same as being the shape you meant. After a risky operation, compare its volume with what you expect as well.

The repair functions​

FunctionWhat it does
orientClosedSolidTurns a closed solid so its material is inside
fixSolidRepairs a solid, or makes one from a closed shell, with its shells fixed and turned the right way
fixShellTurns the faces of a shell so they all face the same way, and fixes its faces and edges
basicShapeRepairRuns the kernel's general repair over a shape: small gaps, wire and face defects
fixSmallEdgeOnWireRemoves edges shorter than a size from a wire and closes the gaps they leave
fixEdgeOrientationsAlongWireRebuilds a wire so its edges run head to tail in one direction