Skip to main content

OCCT Bottle Example

Complete Example - Parametric OCCT Bottle

Below is a complete example that creates a parametric bottle with threading using OCCT, with lil-gui controls for adjusting parameters:

Live Example

Bitbybit Platform

StackBlitz - PlayCanvas Full Runner Example

<!DOCTYPE html>
<html lang="en">
<head>
<title>Bitbybit Runner PlayCanvas - Parametric Bottle</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Load the full PlayCanvas runner (includes PlayCanvas) -->
<script src="https://cdn.jsdelivr.net/gh/bitbybit-dev/bitbybit-assets@latest/runner/bitbybit-runner-playcanvas.js"></script>
<!-- Load lil-gui for parameter controls -->
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.19/dist/lil-gui.umd.min.js"></script>
<script type="module">
const runnerOptions = {
canvasId: 'myCanvas',
canvasZoneClass: 'myCanvasZone',
enableOCCT: true,
enableJSCAD: false,
enableManifold: false,
cameraPosition: [10, 10, 10],
cameraTarget: [0, 4, 0],
backgroundColor: '#1a1c1f',
loadFonts: ['Roboto'],
};

const runner = window.bitbybitRunner.getRunnerInstance();
const { bitbybit, Bit, camera, scene, app, pc } = await runner.run(
runnerOptions
);
app.graphicsDevice.maxPixelRatio = window.devicePixelRatio;
app.resizeCanvas();

// Model parameters
const model = {
width: 5,
height: 8,
thickness: 3,
color: '#ff00ff',
};

// Store current shape for downloading
let currentShape = null;

// Create point lights
function createLights() {
// First point light
const light1 = new pc.Entity('pointLight1');
light1.addComponent('light', {
type: 'point',
color: new pc.Color(1, 1, 1),
intensity: 2,
range: 50,
castShadows: true,
shadowBias: 0.2,
normalOffsetBias: 0.05,
shadowResolution: 2048,
});
light1.setPosition(
model.thickness * 2,
model.height * 0.5,
model.width
);
scene.addChild(light1);

return [light1];
}

const lights = createLights();

// Track current model group for cleanup
let currentGroup = null;
let currentDimensionsGroup = null;

// Download STEP file
async function downloadSTEP() {
if (!currentShape) return;
await bitbybit.occt.io.saveShapeSTEP({
shape: currentShape,
fileName: 'bottle.step',
adjustYtoZ: true,
tryDownload: true,
});
}

// Download STL file
async function downloadSTL() {
if (!currentShape) return;
await bitbybit.occt.io.saveShapeStl({
shape: currentShape,
fileName: 'bottle.stl',
precision: 0.01,
adjustYtoZ: true,
tryDownload: true,
binary: true,
});
}

// Wire up download buttons
document
.getElementById('downloadStep')
.addEventListener('click', downloadSTEP);
document
.getElementById('downloadStl')
.addEventListener('click', downloadSTL);

async function createBottle(width, height, thickness, color) {
// Clean up previous model
if (currentGroup) {
currentGroup.destroy();
currentGroup = null;
}
if (currentDimensionsGroup) {
currentDimensionsGroup.destroy();
currentDimensionsGroup = null;
}

const aPnt1 = [-width / 2, 0, 0];
const aPnt2 = [-width / 2, 0, -thickness / 4];
const aPnt3 = [0, 0, -thickness / 2];
const aPnt4 = [width / 2, 0, -thickness / 4];
const aPnt5 = [width / 2, 0, 0];

const anArc = await bitbybit.occt.shapes.edge.arcThroughThreePoints({
start: aPnt2,
middle: aPnt3,
end: aPnt4,
});
const edge1 = await bitbybit.occt.shapes.edge.line({
start: aPnt1,
end: aPnt2,
});
const edge2 = await bitbybit.occt.shapes.edge.line({
start: aPnt4,
end: aPnt5,
});

const firstHalfWire =
await bitbybit.occt.shapes.wire.combineEdgesAndWiresIntoAWire({
shapes: [edge1, anArc, edge2],
});

const direction = [1, 0, 0];
const origin = [0, 0, 0];
const secondHalfWire = await bitbybit.occt.transforms.mirror({
direction,
origin,
shape: firstHalfWire,
});

const wire =
await bitbybit.occt.shapes.wire.combineEdgesAndWiresIntoAWire({
shapes: [firstHalfWire, secondHalfWire],
});

const aPrismVec = [0, height, 0];
const face = await bitbybit.occt.shapes.face.createFaceFromWire({
shape: wire,
planar: true,
});
const extruded = await bitbybit.occt.operations.extrude({
shape: face,
direction: aPrismVec,
});
const appliedFillets = await bitbybit.occt.fillets.filletEdges({
shape: extruded,
radius: thickness / 12,
});

const neckLocation = [0, height, 0];
const neckRadius = thickness / 4;
const neckHeight = height / 10;
const neckAxis = [0, 1, 0];

const neck = await bitbybit.occt.shapes.solid.createCylinder({
radius: neckRadius,
height: neckHeight,
center: neckLocation,
direction: neckAxis,
});

const unioned = await bitbybit.occt.booleans.union({
shapes: [appliedFillets, neck],
keepEdges: false,
});

const faceToRemove = await bitbybit.occt.shapes.face.getFace({
shape: unioned,
index: 27,
});

const thickOptions = new Bit.Inputs.OCCT.ThickSolidByJoinDto(
unioned,
[faceToRemove],
-thickness / 50
);
const thick = await bitbybit.occt.operations.makeThickSolidByJoin(
thickOptions
);

const geom = bitbybit.occt.geom;

// Threading: Create Surfaces
const aCyl1 = await geom.surfaces.cylindricalSurface({
direction: neckAxis,
radius: neckRadius * 0.99,
center: neckLocation,
});
const aCyl2 = await geom.surfaces.cylindricalSurface({
direction: neckAxis,
radius: neckRadius * 1.05,
center: neckLocation,
});

const aPnt = [2 * Math.PI, neckHeight / 2];
const aDir = [2 * Math.PI, neckHeight / 4];
const aMajor = 2 * Math.PI;
const aMinor = neckHeight / 10;

const anEllipse1 = await geom.curves.geom2dEllipse({
center: aPnt,
direction: aDir,
radiusMinor: aMinor,
radiusMajor: aMajor,
sense: true,
});
const anEllipse2 = await geom.curves.geom2dEllipse({
center: aPnt,
direction: aDir,
radiusMinor: aMinor / 4,
radiusMajor: aMajor,
sense: true,
});

const anArc1 = await geom.curves.geom2dTrimmedCurve({
shape: anEllipse1,
u1: 0,
u2: Math.PI,
sense: true,
adjustPeriodic: true,
});
const anArc2 = await geom.curves.geom2dTrimmedCurve({
shape: anEllipse2,
u1: 0,
u2: Math.PI,
sense: true,
adjustPeriodic: true,
});

const anEllipsePnt1 = await geom.curves.get2dPointFrom2dCurveOnParam({
shape: anEllipse1,
param: 0,
});
const anEllipsePnt2 = await geom.curves.get2dPointFrom2dCurveOnParam({
shape: anEllipse1,
param: Math.PI,
});

const aSegment = await geom.curves.geom2dSegment({
start: anEllipsePnt1,
end: anEllipsePnt2,
});

const anEdge1OnSurf1 =
await bitbybit.occt.shapes.edge.makeEdgeFromGeom2dCurveAndSurface({
curve: anArc1,
surface: aCyl1,
});
const anEdge2OnSurf1 =
await bitbybit.occt.shapes.edge.makeEdgeFromGeom2dCurveAndSurface({
curve: aSegment,
surface: aCyl1,
});
const anEdge1OnSurf2 =
await bitbybit.occt.shapes.edge.makeEdgeFromGeom2dCurveAndSurface({
curve: anArc2,
surface: aCyl2,
});
const anEdge2OnSurf2 =
await bitbybit.occt.shapes.edge.makeEdgeFromGeom2dCurveAndSurface({
curve: aSegment,
surface: aCyl2,
});

const threadingWire1 =
await bitbybit.occt.shapes.wire.combineEdgesAndWiresIntoAWire({
shapes: [anEdge1OnSurf1, anEdge2OnSurf1],
});
const threadingWire2 =
await bitbybit.occt.shapes.wire.combineEdgesAndWiresIntoAWire({
shapes: [anEdge1OnSurf2, anEdge2OnSurf2],
});

const loft = await bitbybit.occt.operations.loft({
shapes: [threadingWire1, threadingWire2],
makeSolid: true,
});

const union = await bitbybit.occt.booleans.union({
shapes: [loft, thick],
keepEdges: false,
});

// Store shape for downloading
currentShape = union;

// Create dimensions
// Width dimension (bottom of bottle)
const widthDimOpt =
new Bit.Inputs.OCCT.SimpleLinearLengthDimensionDto();
widthDimOpt.end = [-width / 2, 0, thickness / 2];
widthDimOpt.start = [width / 2, 0, thickness / 2];
widthDimOpt.direction = [0, 0, 1];
widthDimOpt.offsetFromPoints = 0;
widthDimOpt.labelSize = 0.3;
widthDimOpt.labelSuffix = 'cm';
widthDimOpt.labelRotation = 180;
widthDimOpt.endType = 'arrow';
widthDimOpt.arrowSize = 0.3;
widthDimOpt.crossingSize = 0.2;
widthDimOpt.decimalPlaces = 1;
const widthDimension =
await bitbybit.occt.dimensions.simpleLinearLengthDimension(
widthDimOpt
);

// Height dimension (side of bottle)
const totalHeight = height + neckHeight;
const heightDimOpt =
new Bit.Inputs.OCCT.SimpleLinearLengthDimensionDto();
heightDimOpt.start = [-width / 2, 0, 0];
heightDimOpt.end = [-width / 2, totalHeight, 0];
heightDimOpt.direction = [-1, 0, 0];
heightDimOpt.offsetFromPoints = 0;
heightDimOpt.labelSize = 0.3;
heightDimOpt.endType = 'arrow';
heightDimOpt.labelSuffix = 'cm';
heightDimOpt.labelRotation = 0;
heightDimOpt.arrowSize = 0.3;
heightDimOpt.crossingSize = 0.2;
heightDimOpt.decimalPlaces = 1;
const heightDimension =
await bitbybit.occt.dimensions.simpleLinearLengthDimension(
heightDimOpt
);

// Thickness dimension (depth of bottle)
const thicknessDimOpt =
new Bit.Inputs.OCCT.SimpleLinearLengthDimensionDto();
thicknessDimOpt.start = [width / 2, 0, -thickness / 2];
thicknessDimOpt.end = [width / 2, 0, thickness / 2];
thicknessDimOpt.direction = [1, 0, 0];
thicknessDimOpt.offsetFromPoints = 0;
thicknessDimOpt.labelSize = 0.3;
thicknessDimOpt.endType = 'arrow';
thicknessDimOpt.labelSuffix = 'cm';
thicknessDimOpt.arrowSize = 0.3;
thicknessDimOpt.labelRotation = 180;
thicknessDimOpt.crossingSize = 0.2;
thicknessDimOpt.decimalPlaces = 1;
const thicknessDimension =
await bitbybit.occt.dimensions.simpleLinearLengthDimension(
thicknessDimOpt
);

// Draw options for model
const di = new Bit.Inputs.Draw.DrawOcctShapeOptions();
di.edgeColour = '#000000';
di.edgeOpacity = 0.5;
di.faceColour = color;
di.precision = 0.001;
di.faceOpacity = 1;
di.edgeWidth = 1;

currentGroup = await bitbybit.draw.drawAnyAsync({
entity: union,
options: di,
});

// Draw options for dimensions
const dimOptions = new Bit.Inputs.Draw.DrawOcctShapeOptions();
dimOptions.edgeColour = '#ffffff';
dimOptions.edgeWidth = 2;
dimOptions.drawEdges = true;
dimOptions.drawFaces = false;

currentDimensionsGroup = await bitbybit.draw.drawAnyAsync({
entity: [widthDimension, heightDimension, thicknessDimension],
options: dimOptions,
});

// Update light positions based on new model size
lights[0].setPosition(thickness * 2, height * 0.5, width);
}

// Initial render
await createBottle(
model.width,
model.height,
model.thickness,
model.color
);

// Setup lil-gui
const gui = new lil.GUI();
gui.title('Bottle Parameters');

gui
.add(model, 'width', 2, 10, 0.5)
.name('Width')
.onFinishChange(() => {
createBottle(model.width, model.height, model.thickness, model.color);
});

gui
.add(model, 'height', 4, 15, 0.5)
.name('Base Height')
.onFinishChange(() => {
createBottle(model.width, model.height, model.thickness, model.color);
});

gui
.add(model, 'thickness', 1, 6, 0.25)
.name('Thickness')
.onFinishChange(() => {
createBottle(model.width, model.height, model.thickness, model.color);
});

gui
.addColor(model, 'color')
.name('Color')
.onFinishChange(() => {
createBottle(model.width, model.height, model.thickness, model.color);
});
</script>
<style>
body {
margin: 0;
background-color: #1a1c1f;
}
#myCanvas {
display: block;
width: 100%;
height: 100vh;
}
.myCanvasZone {
width: 100%;
height: 100vh;
}
.download-buttons {
position: fixed;
bottom: 20px;
left: 20px;
display: flex;
gap: 10px;
z-index: 1000;
}
.download-btn {
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.download-step {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.download-stl {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
</style>
</head>

<body>
<div class="download-buttons">
<button id="downloadStep" class="download-btn download-step">
⬇ Download STEP
</button>
<button id="downloadStl" class="download-btn download-stl">
⬇ Download STL
</button>
</div>
<div class="myCanvasZone">
<canvas id="myCanvas"></canvas>
</div>
</body>
</html>

Key Points

Runner Script

<script src="https://cdn.jsdelivr.net/gh/bitbybit-dev/bitbybit-assets@latest/runner/bitbybit-runner-playcanvas.js"></script>

The full runner includes PlayCanvas, so you don't need to load it separately.

lil-gui Integration

<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.19/dist/lil-gui.umd.min.js"></script>

Use lil-gui to create interactive parameter controls. The onFinishChange callback regenerates the model when parameters are adjusted.

Initialization

const runner = window.bitbybitRunner.getRunnerInstance();
const { bitbybit, Bit, camera, scene, app, pc } = await runner.run(runnerOptions);

The run() method returns:

  • bitbybit - The main Bitbybit API object
  • Bit - Helper classes including input DTOs
  • camera - The PlayCanvas camera entity
  • scene - The PlayCanvas scene (root entity)
  • app - The PlayCanvas Application instance
  • pc - The PlayCanvas library itself

PlayCanvas Lights vs Babylon.js

In PlayCanvas, point lights are created as entities:

const light = new pc.Entity('pointLight');
light.addComponent('light', {
type: 'point',
color: new pc.Color(1, 1, 1),
intensity: 2,
range: 50,
castShadows: true,
shadowBias: 0.2,
normalOffsetBias: 0.05,
shadowResolution: 2048,
});
light.setPosition(x, y, z);
scene.addChild(light);

Runner Options

OptionTypeDescription
canvasIdstringThe ID of your canvas element
canvasZoneClassstringCSS class for the canvas container
enableOCCTbooleanEnable OpenCASCADE kernel
enableJSCADbooleanEnable JSCAD kernel
enableManifoldbooleanEnable Manifold kernel
cameraPositionnumber[]Initial camera position [x, y, z]
cameraTargetnumber[]Camera look-at target [x, y, z]
backgroundColorstringScene background color (hex)
loadFontsstring[]Fonts to load for text operations

Downloading STEP & STL Files

Use bitbybit.occt.io to export shapes in various formats:

// Download STEP file
await bitbybit.occt.io.saveShapeSTEP({
shape: myShape,
fileName: 'model.step',
adjustYtoZ: true, // Convert Y-up to Z-up coordinate system
tryDownload: true,
});

// Download STL file
await bitbybit.occt.io.saveShapeStl({
shape: myShape,
fileName: 'model.stl',
precision: 0.01, // Mesh precision (lower = higher resolution)
adjustYtoZ: true,
tryDownload: true,
binary: true, // Binary format for smaller file size
});

Adding Dimensions

Use bitbybit.occt.dimensions to create linear dimensions:

// Create dimension options DTO with default values
const dimOpt = new Bit.Inputs.OCCT.SimpleLinearLengthDimensionDto();
dimOpt.start = [0, 0, 0]; // Start point
dimOpt.end = [5, 0, 0]; // End point
dimOpt.direction = [0, 0, 1]; // Direction to offset the dimension line
dimOpt.offsetFromPoints = 0; // Distance from measurement points
dimOpt.labelSize = 0.3; // Size of the label text
dimOpt.labelSuffix = 'cm'; // Unit suffix (no space)
dimOpt.labelRotation = 180; // Rotate label for readability
dimOpt.endType = 'arrow'; // Arrow style endpoints
dimOpt.arrowSize = 0.3; // Arrow size
dimOpt.crossingSize = 0.2; // Size of endpoint markers
dimOpt.decimalPlaces = 1; // Number of decimal places

const dimension = await bitbybit.occt.dimensions.simpleLinearLengthDimension(dimOpt);

// Draw dimensions as wire geometry
const dimOptions = new Bit.Inputs.Draw.DrawOcctShapeOptions();
dimOptions.edgeColour = '#ffffff';
dimOptions.edgeWidth = 2;
dimOptions.drawEdges = true;
dimOptions.drawFaces = false;

await bitbybit.draw.drawAnyAsync({
entity: dimension,
options: dimOptions,
});

GitHub Source

View the full source code on GitHub: PlayCanvas Full Runner Examples