
typescriptThis conceptual design was made by using cell like subdivisions of surfaces. To rationalise the freeform surface geometry we used triangulation. We have written a lot of new algorithms based on the OpenCascade technology kernel and integrated into our API. This project is open-source and shared under MIT license. You can use the script as an inspiration for your own work or learning.
type Point3 = Bit.Inputs.Base.Point3;
type Vector3 = Bit.Inputs.Base.Vector3;
type UV = [number, number];
type ShapePointer = Bit.Inputs.OCCT.TopoDSShapePointer;
type FacePointer = Bit.Inputs.OCCT.TopoDSFacePointer;
type WirePointer = Bit.Inputs.OCCT.TopoDSWirePointer;
const { LoftAdvancedDto, approxParametrizationTypeEnum } = Bit.Inputs.OCCT;
const { SkyboxDto, DirectionalLightDto } = Bit.Inputs.BabylonScene;
const { DrawOcctShapeOptions } = Bit.Inputs.Draw;
const { PBRMetallicRoughnessDto } = Bit.Inputs.BabylonMaterial;
const exportGlb = false;
const meshingPrecision = 0.01;
const shapesPerDrawCall = 30;
const drawEdges = true;
// Which faces of tower-base.stp get which pattern. Entrance faces take the apex of each
// triangle from the matching cell of a second, offset face; side faces push the apex out
// along the face normal by apexHeight.
type EntranceFacade = { kind: "entrance"; face: number; apexFace: number; cellsU: number; cellsV: number };
type SideFacade = { kind: "side"; face: number; reversed: boolean; cellsU: number; cellsV: number; apexHeight: number; panelHeight: number };
type Facade = EntranceFacade | SideFacade;
const facades: Facade[] = [
{ kind: "entrance", face: 4, apexFace: 2, cellsU: 6, cellsV: 25 },
{ kind: "entrance", face: 1, apexFace: 3, cellsU: 25, cellsV: 6 },
{ kind: "side", face: 5, reversed: true, cellsU: 25, cellsV: 6, apexHeight: 0.3, panelHeight: 0.04 },
{ kind: "side", face: 6, reversed: true, cellsU: 25, cellsV: 6, apexHeight: 0.3, panelHeight: 0.04 },
{ kind: "side", face: 0, reversed: false, cellsU: 6, cellsV: 6, apexHeight: 0.2, panelHeight: 0.02 },
];
// Entrance frame sections, inset from the rounded triangle and lifted along its normal.
// Every inset stays below the fillet radius, so each section keeps its three arcs and
// all sections have the same six edges, which the loft needs.
const entranceFillet = 0.08;
const entranceSections = [
{ inset: 0.03, lift: 0.02 },
{ inset: 0.04, lift: 0.01 },
{ inset: 0.05, lift: 0.02 },
{ inset: 0.07, lift: 0 },
{ inset: 0.03, lift: -0.02 },
];
const entranceWindowSection = 3;
const sideInsets = [0.03, 0.04];
const sideLift = -0.01;
// Triangles smaller than this (inradius) are too small to frame and are skipped.
const minInradius = 0.002;
const add = (a: number[], b: number[]): Point3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
const sub = (a: number[], b: number[]): Vector3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const scale = (a: number[], s: number): Vector3 => [a[0] * s, a[1] * s, a[2] * s];
const cross = (a: number[], b: number[]): Vector3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
const magnitude = (a: number[]): number => Math.hypot(a[0], a[1], a[2]);
const normalize = (a: number[]): Vector3 => scale(a, 1 / magnitude(a));
const triangleNormal = (a: Point3, b: Point3, c: Point3): Vector3 => normalize(cross(sub(b, a), sub(c, a)));
const centroid = (a: Point3, b: Point3, c: Point3): Point3 => scale(add(add(a, b), c), 1 / 3) as Point3;
// The radius of the largest circle inside the triangle. A 2D fillet fits only below it,
// and an inward offset of more than it collapses the outline.
const inradius = (a: Point3, b: Point3, c: Point3): number => {
const perimeter = magnitude(sub(b, a)) + magnitude(sub(c, b)) + magnitude(sub(a, c));
const area = magnitude(cross(sub(b, a), sub(c, a))) / 2;
return perimeter > 0 ? (2 * area) / perimeter : 0;
};
const describeError = (error: unknown): string => {
const e = error as { code?: string; message?: string };
return e?.code ?? e?.message ?? String(error);
};
// A cell's corners in loop order, so consecutive corners make its four triangle bases.
type Cell = { ring: [Point3, Point3, Point3, Point3]; center: Point3; centerNormal: Vector3 };
const sampleCells = async (face: FacePointer, cellsU: number, cellsV: number): Promise<Cell[]> => {
const ringUVs: UV[] = [];
const centerUVs: UV[] = [];
for (let u = 0; u < cellsU; u++) {
for (let v = 0; v < cellsV; v++) {
const u0 = u / cellsU;
const u1 = (u + 1) / cellsU;
const v0 = v / cellsV;
const v1 = (v + 1) / cellsV;
ringUVs.push([u0, v0], [u1, v0], [u1, v1], [u0, v1]);
centerUVs.push([(u0 + u1) / 2, (v0 + v1) / 2]);
}
}
const [ringPoints, centers, centerNormals] = await Promise.all([
bitbybit.occt.shapes.face.pointsOnUVs({ shape: face, paramsUV: ringUVs }),
bitbybit.occt.shapes.face.pointsOnUVs({ shape: face, paramsUV: centerUVs }),
bitbybit.occt.shapes.face.normalsOnUVs({ shape: face, paramsUV: centerUVs }),
]);
return centers.map((center, i) => ({
ring: ringPoints.slice(i * 4, i * 4 + 4) as Cell["ring"],
center,
centerNormal: centerNormals[i],
}));
};
type Frame = { frame: ShapePointer; window: ShapePointer };
const sideLoft = (shapes: WirePointer[], closed: boolean) => {
const options = new LoftAdvancedDto(shapes);
options.closed = closed;
options.straight = true;
options.parType = approxParametrizationTypeEnum.approxCentripetal;
options.maxUDegree = 1;
options.periodic = false;
options.tolerance = 1e-7;
return bitbybit.occt.operations.loftAdvanced(options);
};
const createEntranceTriangle = async (a: Point3, b: Point3, apex: Point3): Promise<Frame | undefined> => {
const room = inradius(a, b, apex);
if (room < minInradius) {
return undefined;
}
// Shrink the whole profile on triangles too small for it, instead of failing.
const fit = Math.min(1, (0.9 * room) / entranceFillet);
const normal = triangleNormal(a, b, apex);
const outline = await bitbybit.occt.shapes.wire.createPolygonWire({ points: [a, b, apex] });
const [rounded, nearlySharp] = await Promise.all([
bitbybit.occt.fillets.fillet2d({ shape: outline, radius: entranceFillet * fit }),
bitbybit.occt.fillets.fillet2d({ shape: outline, radius: 0.0001 * fit }),
]);
const insets = await Promise.all(entranceSections.map(s =>
bitbybit.occt.operations.offset({ shape: rounded, distance: -s.inset * fit, tolerance: 1e-7 })));
const sections = await bitbybit.occt.transforms.translateShapes({
shapes: insets,
translations: entranceSections.map(s => scale(normal, s.lift * fit)),
});
const loftOptions = new LoftAdvancedDto([nearlySharp, ...sections]);
loftOptions.closed = true;
loftOptions.straight = true;
const [frame, window] = await Promise.all([
bitbybit.occt.operations.loftAdvanced(loftOptions),
bitbybit.occt.shapes.face.createFaceFromWire({ shape: insets[entranceWindowSection], planar: true }),
]);
return { frame, window };
};
const createSideTriangle = async (a: Point3, b: Point3, center: Point3, centerNormal: Vector3, apexHeight: number, panelHeight: number): Promise<Frame | undefined> => {
const apex = add(center, scale(centerNormal, apexHeight));
const room = inradius(a, b, apex);
if (room < minInradius) {
return undefined;
}
const fit = Math.min(1, (0.8 * room) / Math.max(...sideInsets));
const normal = triangleNormal(a, b, apex);
const outline = await bitbybit.occt.shapes.wire.createPolygonWire({ points: [a, b, apex] });
const [inset1, inset2, tip] = await Promise.all([
bitbybit.occt.operations.offset({ shape: outline, distance: -sideInsets[0] * fit, tolerance: 1e-7 }),
bitbybit.occt.operations.offset({ shape: outline, distance: -sideInsets[1] * fit, tolerance: 1e-7 }),
bitbybit.occt.transforms.scale3d({ shape: outline, scale: [0.001, 0.001, 0.001], center: centroid(a, b, apex) }),
]);
const [inset2Lifted, tipLifted] = await bitbybit.occt.transforms.translateShapes({
shapes: [inset2, tip],
translations: [scale(normal, sideLift * fit), scale(normal, panelHeight * fit)],
});
const [frame, window] = await Promise.all([
sideLoft([outline, inset1, inset2Lifted], true),
sideLoft([inset2Lifted, tipLifted], false),
]);
return { frame, window };
};
// Builds every triangle at once so the worker never waits for the script, and keeps going
// past a triangle the kernel cannot build: 1.4 reports such a failure as an error instead
// of an empty shape, which used to stop the whole script.
const buildAll = async (label: string, jobs: (() => Promise<Frame | undefined>)[]): Promise<Frame[]> => {
const results = await Promise.allSettled(jobs.map(job => job()));
const built: Frame[] = [];
const failures = new Map<string, number>();
let skipped = 0;
for (const result of results) {
if (result.status === "rejected") {
const reason = describeError(result.reason);
failures.set(reason, (failures.get(reason) ?? 0) + 1);
} else if (result.value) {
built.push(result.value);
} else {
skipped++;
}
}
const failed = [...failures.values()].reduce((sum, n) => sum + n, 0);
console.log(`${label}: ${built.length} of ${jobs.length} triangles built, ${skipped} too small, ${failed} failed`);
failures.forEach((count, reason) => console.warn(`${label}: ${count} x ${reason}`));
return built;
};
const drawInChunks = async (shapes: ShapePointer[], options: Bit.Inputs.Draw.DrawOcctShapeOptions) => {
for (let i = 0; i < shapes.length; i += shapesPerDrawCall) {
const compound = await bitbybit.occt.shapes.compound.makeCompound({ shapes: shapes.slice(i, i + shapesPerDrawCall) });
await bitbybit.draw.drawAnyAsync({ entity: compound, options });
}
};
const frameOptions = (faceColour: string) => {
const options = new DrawOcctShapeOptions();
options.edgeWidth = 0.2;
options.precision = meshingPrecision;
options.drawEdges = drawEdges;
options.faceColour = faceColour;
options.edgeColour = "#000000";
return options;
};
const materialOptions = (material: Bit.Inputs.BabylonMaterial.PBRMetallicRoughnessDto) => {
const options = new DrawOcctShapeOptions();
options.edgeWidth = 0.2;
options.precision = meshingPrecision;
options.drawEdges = drawEdges;
options.faceMaterial = bitbybit.babylon.material.pbrMetallicRoughness.create(material);
return options;
};
const glassMaterial = new PBRMetallicRoughnessDto();
glassMaterial.baseColor = "#0000ff";
glassMaterial.backFaceCulling = false;
glassMaterial.metallic = 0.9;
glassMaterial.roughness = 0.3;
glassMaterial.alpha = 0.6;
glassMaterial.zOffset = 2;
const panelMaterial = new PBRMetallicRoughnessDto();
panelMaterial.baseColor = "#111155";
panelMaterial.backFaceCulling = false;
panelMaterial.metallic = 0.8;
panelMaterial.roughness = 0.3;
const setUpScene = () => {
const skybox = new SkyboxDto();
skybox.skybox = Bit.Inputs.Base.skyboxEnum.clearSky;
skybox.blur = 0.4;
skybox.size = 1000;
skybox.environmentIntensity = 0.7;
bitbybit.babylon.scene.enableSkybox(skybox);
const light = new DirectionalLightDto();
light.direction = [-50, -10, -30];
light.intensity = 1;
light.shadowGeneratorMapSize = 4000;
bitbybit.babylon.scene.drawDirectionalLight(light);
const setStartingCamera = () => {
const camera = bitbybit.babylon.scene.getActiveCamera() as BABYLON.ArcRotateCamera;
camera.setTarget(new BABYLON.Vector3(1.479, 4.817, -3.04));
camera.setPosition(new BABYLON.Vector3(-4.807, 0.327, 4.33));
camera.fov = 1.1;
};
setStartingCamera();
};
const start = async () => {
if (!exportGlb) {
setUpScene();
}
const asset = await bitbybit.asset.getFile({ fileName: "tower-base.stp" });
const imported = await bitbybit.occt.io.loadSTEPorIGES({ assetFile: asset as File, adjustZtoY: true });
const tower = await bitbybit.occt.transforms.scale({ shape: imported, factor: 0.1 });
const faceAt = async (index: number, reversed = false): Promise<FacePointer> => {
const face = await bitbybit.occt.shapes.face.getFace({ shape: tower, index });
return reversed ? bitbybit.occt.shapes.face.reversedFace({ shape: face }) : face;
};
const entranceFrames: Frame[] = [];
const sideFrames: Frame[] = [];
for (const facade of facades) {
const label = `face ${facade.face}`;
if (facade.kind === "entrance") {
const [cells, apexCells] = await Promise.all([
faceAt(facade.face).then(face => sampleCells(face, facade.cellsU, facade.cellsV)),
faceAt(facade.apexFace).then(face => sampleCells(face, facade.cellsU, facade.cellsV)),
]);
const jobs = cells.flatMap((cell, i) => cell.ring.map((corner, k) =>
() => createEntranceTriangle(corner, cell.ring[(k + 1) % 4], apexCells[i].center)));
entranceFrames.push(...await buildAll(label, jobs));
} else {
const cells = await sampleCells(await faceAt(facade.face, facade.reversed), facade.cellsU, facade.cellsV);
const jobs = cells.flatMap(cell => cell.ring.map((corner, k) =>
() => createSideTriangle(corner, cell.ring[(k + 1) % 4], cell.center, cell.centerNormal, facade.apexHeight, facade.panelHeight)));
sideFrames.push(...await buildAll(label, jobs));
}
}
await drawInChunks(entranceFrames.map(t => t.frame), frameOptions("#8888ff"));
await drawInChunks(entranceFrames.map(t => t.window), materialOptions(glassMaterial));
await drawInChunks(sideFrames.map(t => t.frame), frameOptions("#111155"));
await drawInChunks(sideFrames.map(t => t.window), materialOptions(panelMaterial));
if (exportGlb) {
bitbybit.babylon.io.exportGLB({ fileName: "parametric-structural-tower-facade.glb" });
} else {
const groundOptions = new DrawOcctShapeOptions();
groundOptions.faceColour = "#5555ff";
groundOptions.drawEdges = false;
const ground = await bitbybit.occt.shapes.face.createCircleFace({ radius: 20, center: [0, 0, 0], direction: [0, 1, 0] });
await bitbybit.draw.drawAnyAsync({ entity: ground, options: groundOptions });
}
const logCameraOnMove = () => {
const camera = bitbybit.babylon.scene.getActiveCamera() as BABYLON.TargetCamera;
const round = (v: BABYLON.Vector3) => [v.x, v.y, v.z].map(n => Number(n.toFixed(3)));
let timer: ReturnType<typeof setTimeout> | undefined;
camera.onViewMatrixChangedObservable.add(() => {
clearTimeout(timer);
timer = setTimeout(() => {
console.log(`camera position: ${JSON.stringify(round(camera.position))}, target: ${JSON.stringify(round(camera.getTarget()))}`);
}, 300);
});
};
logCameraOnMove();
};
start();
Editor plans for 3D development, API keys for server-side CAD algorithms
Cloud storage, private projects & 3D tools for Bitbybit editors
* "Unlimited" means high limits that most users won't reach. Compare the plans
Run CAD algorithms on managed cloud servers from your own backend applications
Custom software development, dedicated servers & CAD automation at scale.