Script: TypeScript Code for Parametric Tower Facade

TypeScript Code for Parametric Tower Facade picture
Type
Typescript logo indicatortypescript
Date Created
Jan 3, 2023, 4:04:42 PM
Last Edit Date
Oct 2, 2025, 6:19:31 PM

Project Information

This 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.

View Full Project

Script Code

type Point3 = Bit.Inputs.Base.Point3;
type Vector3 = Bit.Inputs.Base.Point3;
type ShapePointer = Bit.Inputs.OCCT.TopoDSShapePointer;

const approxParametrizationTypeEnum = Bit.Inputs.OCCT.approxParametrizationTypeEnum;
const LoftAdvancedDto = Bit.Inputs.OCCT.LoftAdvancedDto;
const SkyboxDto = Bit.Inputs.BabylonScene.SkyboxDto;
const DirectionalLightDto = Bit.Inputs.BabylonScene.DirectionalLightDto;
const DrawOcctShapeOptions = Bit.Inputs.Draw.DrawOcctShapeOptions;
const PBRMetallicRoughnessDto = Bit.Inputs.BabylonMaterial.PBRMetallicRoughnessDto;

const skyboxEnum = Bit.Inputs.Base.skyboxEnum;

type UV = [
    number,
    number,
]

type CellUV = {
    ptUV1: UV,
    ptUV2: UV,
    ptUV3: UV,
    ptUV4: UV,
    center: UV,
    uIndex: number,
    vIndex: number,
}

type CellPt = {
    pt1: Point3,
    pt2: Point3,
    pt3: Point3,
    pt4: Point3,
    center: Point3,
    pt1Normal: Vector3,
    pt2Normal: Vector3,
    pt3Normal: Vector3,
    pt4Normal: Vector3,
    centerNormal: Vector3,
    uIndex: number,
    vIndex: number,
}

const exportGlb = false;
const meshingPrecision = 0.01;
const chunksize = 30;
const drawEdges = true;

const start = async () => {
    const asset = await bitbybit.asset.getFile({ fileName: 'tower-base.stp' });
    const shape = await bitbybit.occt.io.loadSTEPorIGES({ assetFile: asset as File, adjustZtoY: true });
    const scaledShape = await bitbybit.occt.transforms.scale({ shape, factor: 0.1 });
    const face1 = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 4 });
    const face1Of = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 2 });

    const face2 = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 1 });
    const face2Of = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 3 });

    let faceTop = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 0 });
    let faceSide1 = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 5 });
    let faceSide2 = await bitbybit.occt.shapes.face.getFace({ shape: scaledShape, index: 6 });

    faceSide1 = await bitbybit.occt.shapes.face.reversedFace({ shape: faceSide1 });
    faceSide2 = await bitbybit.occt.shapes.face.reversedFace({ shape: faceSide2 });

    if (!exportGlb) {
        const opt = new SkyboxDto();
        opt.blur = 0.4;
        opt.skybox = skyboxEnum.clearSky;
        opt.size = 1000;
        opt.environmentIntensity = 0.7;
        bitbybit.babylon.scene.enableSkybox(opt);

        const lightOpt = new DirectionalLightDto();
        lightOpt.direction = [-50, -10, -30];
        lightOpt.intensity = 1;
        lightOpt.shadowGeneratorMapSize = 4000;
        bitbybit.babylon.scene.drawDirectionalLight(lightOpt)
    }

    const getUVCells = async (face, nrCellsU: number, nrCellsV: number) => {
        const cellsUV: CellUV[] = [];

        const stepU = 1 / nrCellsU;
        const stepV = 1 / nrCellsV;

        for (let u = 0; u < nrCellsU; u++) {
            for (let v = 0; v < nrCellsV; v++) {
                const uPar1 = u * stepU;
                const vPar1 = v * stepV;
                const uPar2 = (u + 1) * stepU;
                const vPar2 = v * stepV;
                const uPar3 = u * stepU;
                const vPar3 = (v + 1) * stepV;
                const uPar4 = (u + 1) * stepU;
                const vPar4 = (v + 1) * stepV;

                const uCenter = u * stepU + (stepU / 2);
                const vCenter = v * stepV + (stepV / 2);

                const cornerUV: CellUV = {
                    ptUV1: [uPar1, vPar1],
                    ptUV2: [uPar2, vPar2],
                    ptUV3: [uPar3, vPar3],
                    ptUV4: [uPar4, vPar4],
                    center: [uCenter, vCenter],
                    uIndex: u,
                    vIndex: v,
                }
                cellsUV.push(cornerUV);
            }
        }

        const promises = cellsUV.map(c => {
            const allUVSPoints = [c.ptUV1, c.ptUV2, c.ptUV3, c.ptUV4, c.center];
            return bitbybit.occt.shapes.face.pointsOnUVs({ shape: face, paramsUV: allUVSPoints });
        });

        let pts = await Promise.all(promises);

        const normalPromises = cellsUV.map(c => {
            const allUVSPoints = [c.ptUV1, c.ptUV2, c.ptUV3, c.ptUV4, c.center];
            return bitbybit.occt.shapes.face.normalsOnUVs({ shape: face, paramsUV: allUVSPoints });
        });

        let normals = await Promise.all(normalPromises);

        const cellsPt: CellPt[] = pts.map((p, index) => {
            let cellPt: CellPt = {
                pt1: p[0],
                pt2: p[1],
                pt3: p[2],
                pt4: p[3],
                center: p[4],
                pt1Normal: normals[index][0],
                pt2Normal: normals[index][1],
                pt3Normal: normals[index][2],
                pt4Normal: normals[index][3],
                centerNormal: normals[index][4],
                uIndex: cellsUV[index].uIndex,
                vIndex: cellsUV[index].vIndex,
            }
            return cellPt;
        })

        return cellsPt;
    }

    type TrianglesEntrance = { entranceSideFrames: ShapePointer[], entranceSideWindows: ShapePointer[] };
    const createTrianglesEntrance = async (pt1: Point3, pt2: Point3, pt3: Point3): Promise<TrianglesEntrance> => {

        const entranceSideFrames = [];
        const entranceSideWindows = [];

        const ln1 = await bitbybit.occt.shapes.edge.line({ start: pt1, end: pt2 });
        const ln2 = await bitbybit.occt.shapes.edge.line({ start: pt2, end: pt3 });
        const ln3 = await bitbybit.occt.shapes.edge.line({ start: pt3, end: pt1 });
        let triangleWire = await bitbybit.occt.shapes.wire.combineEdgesAndWiresIntoAWire({ shapes: [ln1, ln2, ln3] });

        const plane = await bitbybit.occt.shapes.face.createFaceFromWire({ shape: triangleWire, planar: true });
        const normal = await bitbybit.occt.shapes.face.normalOnUV({ shape: plane, paramU: 0.5, paramV: 0.5 });
        const normalized = bitbybit.vector.normalized({ vector: normal });
        const offsetVec = bitbybit.vector.mul({ vector: normalized, scalar: 0.02 }) as Vector3;
        const offsetVec2 = bitbybit.vector.mul({ vector: normalized, scalar: 0.01 }) as Vector3;
        const offsetVec3 = bitbybit.vector.mul({ vector: normalized, scalar: -0.02 }) as Vector3;

        const triangleWire2 = await bitbybit.occt.fillets.fillet2d({ shape: triangleWire, radius: 0.08 });

        const triangleWire3 = await bitbybit.occt.fillets.fillet2d({ shape: triangleWire, radius: 0.0001 });

        const triangleWireMiddle1 = await bitbybit.occt.operations.offset({ shape: triangleWire2, distance: -0.03, tolerance: 1e-7 });
        const triangleWireMiddle2 = await bitbybit.occt.operations.offset({ shape: triangleWire2, distance: -0.04, tolerance: 1e-7 });
        const triangleWireMiddle3 = await bitbybit.occt.operations.offset({ shape: triangleWire2, distance: -0.05, tolerance: 1e-7 });
        const triangleWireCenter = await bitbybit.occt.operations.offset({ shape: triangleWire2, distance: -0.07, tolerance: 1e-7 });
        const triangleWireMiddleDown = await bitbybit.occt.operations.offset({ shape: triangleWire2, distance: -0.03, tolerance: 1e-7 });

        const triangleWireMiddleUp1 = await bitbybit.occt.transforms.translate({ shape: triangleWireMiddle1, translation: offsetVec });
        const triangleWireMiddleUp2 = await bitbybit.occt.transforms.translate({ shape: triangleWireMiddle2, translation: offsetVec2 });
        const triangleWireMiddleUp3 = await bitbybit.occt.transforms.translate({ shape: triangleWireMiddle3, translation: offsetVec });

        const triangleWireMiddleDownMoved = await bitbybit.occt.transforms.translate({ shape: triangleWireMiddleDown, translation: offsetVec3 });


        const opt = new Bit.Inputs.OCCT.LoftAdvancedDto([triangleWire3, triangleWireMiddleUp1, triangleWireMiddleUp2, triangleWireMiddleUp3, triangleWireCenter, triangleWireMiddleDownMoved]);
        opt.closed = true;
        opt.straight = true;
        const loft = await bitbybit.occt.operations.loftAdvanced(opt);
        entranceSideFrames.push(loft);

        const window = await bitbybit.occt.shapes.face.createFaceFromWire({ shape: triangleWireCenter, planar: true });
        entranceSideWindows.push(window);
        return { entranceSideFrames, entranceSideWindows };
    }

    type TrianglesSide = { sideFrames: ShapePointer[], sideWindows: ShapePointer[] };

    const createTrianglesSide = async (pt1: Point3, pt2: Point3, pt3: Point3, pt3Normal: Vector3, centerOffsetFactor: number, inBetweenScaling: number): Promise<TrianglesSide> => {
        const sideFrames = [];
        const sideWindows = [];

        const centerNormal = bitbybit.vector.mul({ vector: pt3Normal, scalar: centerOffsetFactor }) as Bit.Inputs.Base.Vector3;
        const translation = bitbybit.babylon.transforms.translationXYZ({ translation: centerNormal })

        const movedPt = await bitbybit.point.transformPoint({ point: pt3, transformation: translation });

        const ln1 = await bitbybit.occt.shapes.edge.line({ start: pt1, end: pt2 });
        const ln2 = await bitbybit.occt.shapes.edge.line({ start: pt2, end: movedPt });
        const ln3 = await bitbybit.occt.shapes.edge.line({ start: movedPt, end: pt1 });

        let triangleWire = await bitbybit.occt.shapes.wire.combineEdgesAndWiresIntoAWire({ shapes: [ln1, ln2, ln3] });

        const plane = await bitbybit.occt.shapes.face.createFaceFromWire({ shape: triangleWire, planar: true });

        const normal = await bitbybit.occt.shapes.face.normalOnUV({ shape: plane, paramU: 0.5, paramV: 0.5 });
        const center = await bitbybit.occt.shapes.face.getFaceCenterOfMass({ shape: plane });
        const normalized = bitbybit.vector.normalized({ vector: normal });

        const offsetVec = bitbybit.vector.mul({ vector: normalized, scalar: inBetweenScaling }) as Bit.Inputs.Base.Vector3;
        const offsetVec2 = bitbybit.vector.mul({ vector: normalized, scalar: -0.01 }) as Bit.Inputs.Base.Vector3;

        const triangleWireMiddle1 = await bitbybit.occt.operations.offset({ shape: triangleWire, distance: -0.03, tolerance: 1e-7 });
        const triangleWireMiddle2 = await bitbybit.occt.operations.offset({ shape: triangleWire, distance: -0.04, tolerance: 1e-7 });
        const triangleWireMiddle3 = await bitbybit.occt.transforms.scale3d({ shape: triangleWire, scale: [0.001, 0.001, 0.001], center });

        const triangleWireMiddleUp2 = await bitbybit.occt.transforms.translate({ shape: triangleWireMiddle2, translation: offsetVec2 });
        const triangleWireMiddleUp3 = await bitbybit.occt.transforms.translate({ shape: triangleWireMiddle3, translation: offsetVec });

        const opt = new LoftAdvancedDto([triangleWire, triangleWireMiddle1, triangleWireMiddleUp2]);

        opt.closed = true;
        opt.straight = true;
        opt.parType = approxParametrizationTypeEnum.approxCentripetal;
        opt.maxUDegree = 1;
        opt.periodic = false;
        opt.tolerance = 1e-7;
        const loft = await bitbybit.occt.operations.loftAdvanced(opt);

        sideFrames.push(loft);
        opt.shapes = [triangleWireMiddleUp2, triangleWireMiddleUp3];
        opt.closed = false;
        const loft2 = await bitbybit.occt.operations.loftAdvanced(opt);
        sideWindows.push(loft2)

        return { sideFrames, sideWindows };
    }

    const cells1 = await getUVCells(face1, 6, 25);
    const cells2 = await getUVCells(face1Of, 6, 25);

    const cells3 = await getUVCells(face2, 25, 6);
    const cells4 = await getUVCells(face2Of, 25, 6);

    const cells5 = await getUVCells(faceSide1, 25, 6);
    const cells6 = await getUVCells(faceSide2, 25, 6);
    const cells7 = await getUVCells(faceTop, 6, 6);

    const computeTriangleFramesForCells = async (cells: CellPt[], cellsOffset: CellPt[]): Promise<TrianglesEntrance[]> => {
        let index = 0;
        let triEnt: TrianglesEntrance[] = [];
        for (const cell of cells) {
            const cellTop = cellsOffset[index];
            const triEnt1 = await createTrianglesEntrance(cell.pt1, cell.pt2, cellTop.center);
            const triEnt2 = await createTrianglesEntrance(cell.pt2, cell.pt4, cellTop.center);
            const triEnt3 = await createTrianglesEntrance(cell.pt4, cell.pt3, cellTop.center);
            const triEnt4 = await createTrianglesEntrance(cell.pt3, cell.pt1, cellTop.center);
            triEnt.push(...[triEnt1, triEnt2, triEnt3, triEnt4]);
            index++;
        }
        return triEnt;
    }

    const computeTriangleSidesForCells = async (cells: CellPt[], centerOffsetFactor: number, inBetweenScaling: number): Promise<TrianglesSide[]> => {
        let triSides: TrianglesSide[] = [];
        for (const cell of cells) {
            const triSide1 = await createTrianglesSide(cell.pt1, cell.pt2, cell.center, cell.centerNormal, centerOffsetFactor, inBetweenScaling);
            const triSide2 = await createTrianglesSide(cell.pt2, cell.pt4, cell.center, cell.centerNormal, centerOffsetFactor, inBetweenScaling);
            const triSide3 = await createTrianglesSide(cell.pt4, cell.pt3, cell.center, cell.centerNormal, centerOffsetFactor, inBetweenScaling);
            const triSide4 = await createTrianglesSide(cell.pt3, cell.pt1, cell.center, cell.centerNormal, centerOffsetFactor, inBetweenScaling);
            triSides.push(...[triSide1, triSide2, triSide3, triSide4]);
        }
        return triSides;
    }

    const tEntrance1 = await computeTriangleFramesForCells(cells1, cells2);
    const tEntrance2 = await computeTriangleFramesForCells(cells3, cells4);

    const tSide1 = await computeTriangleSidesForCells(cells5, 0.3, 0.04);
    const tSide2 = await computeTriangleSidesForCells(cells6, 0.3, 0.04);
    const tsTop3 = await computeTriangleSidesForCells(cells7, 0.2, 0.02);

    const chunkize = (shapes: ShapePointer[], chunkSize: number): ShapePointer[][] => {
        const chunks = [];
        for (let i = 0; i < shapes.length; i += chunkSize) {
            const chunk = shapes.slice(i, i + chunkSize);
            chunks.push(chunk);
        }
        return chunks;
    }

    const drawEntrances = async (t: TrianglesEntrance[]) => {
        const drawOptionsEntranceFrames = new DrawOcctShapeOptions();
        drawOptionsEntranceFrames.edgeWidth = 0.2;
        drawOptionsEntranceFrames.precision = meshingPrecision;
        drawOptionsEntranceFrames.faceColour = '#8888ff';
        drawOptionsEntranceFrames.edgeColour = '#000000';
        drawOptionsEntranceFrames.drawEdges = drawEdges;

        const frames = t.map(s => s.entranceSideFrames).flat();
        for (const chunk of chunkize(frames, chunksize)) {
            const compoundFrames = await bitbybit.occt.shapes.compound.makeCompound({ shapes: chunk });
            await bitbybit.draw.drawAnyAsync({ entity: compoundFrames, options: drawOptionsEntranceFrames });
        }

        const drawOptionsEntranceWindows = new DrawOcctShapeOptions();
        drawOptionsEntranceWindows.edgeWidth = 0.2;
        drawOptionsEntranceWindows.precision = meshingPrecision;
        drawOptionsEntranceWindows.drawEdges = drawEdges;
        const windowMaterial = new PBRMetallicRoughnessDto();
        windowMaterial.baseColor = '#0000ff';
        windowMaterial.backFaceCulling = false;
        windowMaterial.metallic = 0.9;
        windowMaterial.roughness = 0.3;
        windowMaterial.alpha = 0.6;
        windowMaterial.zOffset = 2;

        drawOptionsEntranceWindows.faceMaterial = bitbybit.babylon.material.pbrMetallicRoughness.create(windowMaterial)

        const windows = t.map(s => s.entranceSideWindows).flat();
        for (const chunk of chunkize(windows, chunksize)) {
            const compoundFrames = await bitbybit.occt.shapes.compound.makeCompound({ shapes: chunk });
            await bitbybit.draw.drawAnyAsync({ entity: compoundFrames, options: drawOptionsEntranceWindows });
        }
    }

    const drawSides = async (t: TrianglesSide[]) => {
        const drawOptionsSideFrames = new DrawOcctShapeOptions();
        drawOptionsSideFrames.edgeWidth = 0.2;
        drawOptionsSideFrames.precision = meshingPrecision;
        drawOptionsSideFrames.faceColour = '#111155';
        drawOptionsSideFrames.edgeColour = '#000000';
        drawOptionsSideFrames.drawEdges = drawEdges;

        const frames = t.map(s => s.sideFrames).flat();
        for (const chunk of chunkize(frames, chunksize)) {
            const compoundFrames = await bitbybit.occt.shapes.compound.makeCompound({ shapes: chunk });
            await bitbybit.draw.drawAnyAsync({ entity: compoundFrames, options: drawOptionsSideFrames });
        }

        const drawOptionsSidePanels = new DrawOcctShapeOptions();
        drawOptionsSidePanels.edgeWidth = 0.2;
        drawOptionsSidePanels.precision = meshingPrecision;
        drawOptionsSidePanels.drawEdges = drawEdges;
        const panelMaterial = new PBRMetallicRoughnessDto();
        panelMaterial.baseColor = '#111155';
        panelMaterial.backFaceCulling = false;
        panelMaterial.metallic = 0.8;
        panelMaterial.roughness = 0.3;

        drawOptionsSidePanels.faceMaterial = bitbybit.babylon.material.pbrMetallicRoughness.create(panelMaterial)

        const windows = t.map(s => s.sideWindows).flat();
        for (const chunk of chunkize(windows, chunksize)) {
            const compoundFrames = await bitbybit.occt.shapes.compound.makeCompound({ shapes: chunk });
            await bitbybit.draw.drawAnyAsync({ entity: compoundFrames, options: drawOptionsSidePanels });
        }
    }

    await drawEntrances(tEntrance1);
    await drawEntrances(tEntrance2);

    await drawSides(tSide1);
    await drawSides(tSide2);
    await drawSides(tsTop3);

    if (!exportGlb) {
        const groundOpt = new DrawOcctShapeOptions();
        groundOpt.faceColour = '#5555ff';
        groundOpt.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: groundOpt });
    }
    if (exportGlb) {
        bitbybit.babylon.io.exportGLB({
            fileName: 'parametric-structural-tower-facade.glb'
        })
    }
}

start();