Skip to main content

Tutorial: Building a 3D Laptop Holder Configurator for Shopify (TypeScript)

What You Will Learn: Multi-Variant 3D Configuration

This tutorial demonstrates how to create a more complex 3D configurable product—a laptop holder—and drive its appearance and features using three distinct sets of Shopify product variants. The core logic for the laptop holder will be developed using TypeScript on the Bitbybit platform.

You will learn how to:

  • Structure a TypeScript script on Bitbybit to create a parametric 3D laptop holder.
  • Define multiple product variants in Shopify (e.g., Laptop Type, Number of Laptops, Color).
  • Export your TypeScript logic using the "Export to Runner" feature.
  • Integrate this script into your Shopify product page using the "3D Bits" app (via BITBYBIT RUNNER).
  • Ensure your 3D model dynamically updates based on the customer's selection of these multiple variants.

View the Demo:

A screenshot of the 3D parametric laptop holder model as it appears in the Shopify store. 3D Laptop Holder Configurator Example

Video Tutorial: Building the Laptop Holder Configurator

Setting Up the Laptop Holder Product Variants in Shopify

For this configurator, you will need to define the following product variants in your Shopify admin:

  • Laptop Type
    • Values: MacBook Pro 16, MacBook Pro 14, MacBook Air
  • Number Laptops (or "Capacity")
    • Values: 1, 2, 3
  • Color
    • Values: Black, Blue (or any other colors you wish to offer)

Your Bitbybit TypeScript script will be designed to read these variant selections and adjust the 3D model accordingly.

The Bitbybit TypeScript Script

To save you time and provide a starting point, here is the embedded Bitbybit TypeScript script used in this tutorial for the parametric laptop holder. You can explore it here and then use the "Export to Runner" feature in the Bitbybit editor to get the JavaScript code for your Shopify BITBYBIT RUNNER block.

Bitbybit Platform

Bitbybit Rete Editor - 3D Laptop Holder

typescript logoTypescript
Script Source (typescript)
Bit.mockBitbybitRunnerInputs({
"Laptop Type": "MacBook Pro 16",
"Number Laptops": "3",
"Color": "Black",
});
const inputs = Bit.getBitbybitRunnerInputs();

const laptops: Laptop[] = []

let laptop: Laptop;

switch (inputs["Laptop Type"]) {
case "MacBook Pro 16":
laptop = {
length: 1.63,
width: 35.8,
height: 24.6
};
break;
case "MacBook Pro 14":
laptop = {
length: 1.57,
width: 31.3,
height: 22.2
}
break;
case "MacBook Air":
laptop = {
length: 1.2,
width: 30.5,
height: 21.6
}
break;
default:
break;
}

let flipColor = false;
switch (inputs["Color"]) {
case "Blue":
flipColor = true;
break;
default:
break;
}

console.log("laptop ", laptop);

const nrLaptops = +inputs["Number Laptops"];

for (let i = 0; i < nrLaptops; i++) {
laptops.push({ ...laptop });
}

const whiteColor = "#ffffff";
const holderColor = "#333333";

const laptopLiftedHeight = 3;
const distanceBetweenLaptops = 1.7;
const exportSTEP = false;

bitbybit.babylon.scene.backgroundColour({ colour: "#bbbbbb" });

const pointLightConf = new Bit.Inputs.BabylonScene.PointLightDto();
pointLightConf.position = [-15, 20, -5];
pointLightConf.intensity = 8000;
pointLightConf.diffuse = "#3333ff";
pointLightConf.radius = 0;
bitbybit.babylon.scene.drawPointLight(pointLightConf);

const controlPoints = [
[-12.5, 0, 0],
[-8, 13, 0],
[-4, 11, 0],
[-2, 6, 0],
[2, 6, 0],
[4, 14, 0],
[8, 17, 0],
[12.5, 0, 0]
] as Bit.Inputs.Base.Point3[];

let laptopStand;
let laptopStandMesh;

const laptopsFilletsMesh = [];

async function start() {
const ground = await bitbybit.occt.shapes.face.createCircleFace({ center: [0, 0, 0], direction: [0, 1, 0], radius: 75, });
const groundOptions = new Bit.Inputs.Draw.DrawOcctShapeOptions();
groundOptions.faceColour = whiteColor;
groundOptions.drawEdges = false;
await bitbybit.draw.drawAnyAsync({ entity: ground, options: groundOptions });

const renderLaptops = async (laptops) => {

laptops.forEach(laptop => {
laptop.center = [0, laptop.height / 2 + laptopLiftedHeight, 0] as Bit.Inputs.Base.Point3;
});

let laptopFillets = [];
let totalDistance = 0;
let previousLaptopLength = 0;

laptops.forEach(async (laptop, index) => {
totalDistance += distanceBetweenLaptops + laptop.length / 2 + previousLaptopLength / 2;
previousLaptopLength = laptop.length;
laptop.center[2] = totalDistance;
const laptopBaseModel = await bitbybit.occt.shapes.solid.createBox({
width: laptop.width,
length: laptop.length,
height: laptop.height,
center: laptop.center
});
const laptopFillet = await bitbybit.occt.fillets.filletEdges({ shape: laptopBaseModel, indexes: undefined, radius: 0.2 });
laptopFillets.push(laptopFillet);

const laptopVisModel = await bitbybit.occt.shapes.solid.createBox({
width: laptop.width,
length: laptop.length - 0.01,
height: laptop.height,
center: laptop.center
});
const laptopVisFillet = await bitbybit.occt.fillets.filletEdges({ shape: laptopVisModel, indexes: undefined, radius: 0.2 });
laptopFillets.push(laptopFillet);

const di = new Bit.Inputs.Draw.DrawOcctShapeOptions();

di.faceOpacity = 0.2;
di.edgeWidth = 5;
di.edgeOpacity = 0.6;
di.edgeColour = whiteColor;
di.faceColour = whiteColor;
const laptopFilletMesh = await bitbybit.draw.drawAnyAsync({ entity: laptopVisFillet, options: di });
laptopsFilletsMesh.push(laptopFilletMesh);
})

const polygonWire = await bitbybit.occt.shapes.wire.createPolygonWire({
points: controlPoints
});
const extrusion = await bitbybit.occt.operations.extrude({
shape: polygonWire, direction: [0, 0, totalDistance += distanceBetweenLaptops + previousLaptopLength / 2]
});
const laptopStandFillet = await bitbybit.occt.fillets.filletEdges({ shape: extrusion, indexes: undefined, radius: 1 });
const laptopStandThick = await bitbybit.occt.operations.makeThickSolidSimple({ shape: laptopStandFillet, offset: -0.5 });

laptopStand = await bitbybit.occt.booleans.difference({ shape: laptopStandThick, shapes: laptopFillets, keepEdges: false });
const li = new Bit.Inputs.OCCT.DrawShapeDto(laptopStand);
li.faceOpacity = 1;
if (flipColor) {
li.faceColour = "#0000ff";
li.edgeColour = whiteColor;
} else {
li.faceColour = holderColor;
li.edgeColour = whiteColor;
}
li.edgeWidth = 5;
laptopStandMesh = await bitbybit.draw.drawAnyAsync({ entity: laptopStand, options: li });
const laptopsMeshes = await Promise.all(laptopsFilletsMesh);
return [laptopStandMesh, ...laptopsMeshes];
}

const meshes = await renderLaptops(laptops);
return { meshes };
}

class Laptop {
width: number;
length: number;
height: number;
center?: Bit.Inputs.Base.Point3;
}

Bit.setBitbybitRunnerResult(start());

Why TypeScript for This Example?

You might notice this example opts for TypeScript rather than one of our visual editors like Rete or Blockly. TypeScript offers the most robust and flexible environment for creating complex configurators because:

  • Full Language Power: You can leverage the full capabilities of the TypeScript programming language, including complex logic, data structures, and software design patterns.
  • Direct BabylonJS Access: It provides direct access to all features of the underlying BabylonJS game engine core, allowing for highly customized rendering, animations, and interactions.
  • Scalability: For intricate products with many parameters and conditional behaviors, TypeScript often provides a more scalable and maintainable solution.

This example showcases how TypeScript can be effectively integrated into Shopify as a configurable 3D product using the Bitbybit Runner.


By following this tutorial, you'll gain insight into building sophisticated 3D product configurators driven by multiple Shopify variants, utilizing the power of TypeScript and the Bitbybit platform. Remember to adapt the provided index.html and script.js (from previous tutorials) by replacing the placeholder exported script function with the actual JavaScript code generated from your TypeScript laptop holder script.