Source: utilities/profiledContourBuilder.js

// Original source: https://discourse.threejs.org/t/profiledcontourgeometry-multimaterial/5801


import {Group, BufferGeometry, BufferAttribute, Mesh, Box2, Box3, Vector2, Vector3, MathUtils } from '../../node_modules/three/build/three.module.js';
import * as BufferGeometryUtils from  '../../node_modules/three/examples/jsm/utils/BufferGeometryUtils.js';

import { applyBoxUV }  from './applyBoxMapping.js';

  
export function buildProfiledContour( { 
		contour, contourShape, contourDimensions, contourMaterials, contourClosed, contourCap,
		profile, profileMaterialType, profileMaterials, profileSmoothShading, profileCap, profileAlignment
	} ) {

	//Set the defaults for contour settings
	contourClosed = contourClosed !== undefined ? contourClosed : true;
	contourCap = contourCap !== undefined  ? contourCap : false;

	//Set the defaults for profile settings
	profileMaterialType = profileMaterialType !== undefined ? profileMaterialType : "group";
	
	
	if( contourShape && contourDimensions ){
		let newContour = createContour(contourShape, contourDimensions )
		contour = newContour
	}

	const perContourSegment = true;

	// Slightly adjust profile points with epsilon to prevent issues with uv box mappig on exactly 45 degrees planes
	const profileEpsilon = 0.0001;
	profile = addEpsilonToProfile( profile, profileEpsilon );

	// Get the profile bounding box
	const { minX, maxX } = computeProfileBounds(profile);

	const offsetProfile = offsetProfilePoints( profile, profileAlignment, minX, maxX );
	//console.log( offsetProfile );


	// Add the first point as extra last point in the contour to make it closed
	if( contourClosed ){
		contour.push( contour[ 0 ], contour[ 1 ] );
	} 
	
	// Points on contour and profile
	const contourPoints = contour.length / 2;
	const profilePoints = offsetProfile.length / 2;

	// Segments of contour and profile
	const contourSegments = contourPoints - 1; // contour segments - mainly horizontal
	const profileSegments = profilePoints - 1; // radius segments - mainly vertical
	
	// Arrays for storing all data
	let	vertices = [];    	// profilePoints many vertex colums
	let	positions = []; 	// contourSegments many geometries and meshes
	let	uvs = []; 			// contourSegments many uv's' 
	let geometries = []; 	// geometries of frame strips
	let meshes = [];  		// meshes of frame strips
	//const indexArray = []; // This will store the vertex indices for faces

	
	// Create a sub array for each contour or profile segment
	if (perContourSegment) {
		// For smooth shading per contour segment, we create arrays for each contour segment
		for (let i = 0; i < contourSegments; i++) {
			vertices.push([]);    // Each contour segment gets its own independent vertices
			positions.push([]);   // Each contour segment gets its own positions
			uvs.push([]);         // Each contour segment gets its own UVs
		}
		vertices.push([]); // Last column for non-smooth shading
	} 
	else {
		// For non-smooth shading, we create sub-arrays for each profile segment (current implementation)
		for (let j = 0; j < profileSegments; j++) {
			vertices.push([]);    // Each profile segment gets its own independent vertices
			positions.push([]);   // Each profile segment gets its own positions
			uvs.push([]);         // Each profile segment gets its own UVs
		}
		vertices.push([]); // Last column for non-smooth shading
	}
	
	// Create a group that will contain the meshes
	const group = new Group();

	// Calculate all vertices
	if(perContourSegment){
		calculateVerticesPerContourSegment( contour, offsetProfile, profilePoints, contourPoints, profileSegments, contourSegments, contourClosed, vertices );
	}
	else{
		calculateVertices( contour, offsetProfile, profilePoints, contourPoints, profileSegments, contourSegments, contourClosed, vertices );
	}

	// Calculate positions and uvs
	if(perContourSegment){
		calculatePositionsAndUVsPerContourSegment(profileSegments, contourSegments, vertices, positions, uvs)
	}
	else{
		calculatePositionsAndUVs( profileSegments, contourSegments, vertices, positions, uvs );
	}


	// Create the Meshes 
	if( perContourSegment ){
		createMeshesPerContourSegment( group, meshes, profileSegments, contourSegments, geometries, positions, uvs, profileMaterialType, profileMaterials, profileSmoothShading )
	}
	else{
		createMeshes( group, meshes, profileSegments, contourSegments, geometries, positions, uvs, profileMaterialType, profileMaterials, profileSmoothShading )
	}

	// Cap the start and end of the contour if cap is true
	if ( !contourClosed && profileCap ) {
		createCapProfile( group, profilePoints, vertices, contourMaterials, contourSegments )
	}

	// Cap the contour if contourClosed and cap are true
	if ( contourClosed && contourCap ) {
		createCapContour( profile, group, contourPoints, contour, profileMaterialType, contourMaterials, minX, maxX, profileAlignment)
	}

	// Center the group that will contain the meshes based on the contour
	const centeredGroup = setCenter( group )

	return centeredGroup; // group of frame strips
	
}




/**
 * Helper Functions for ProfiledContourUV
 */

function createContour( shape, dimensions ){

	console.log( "createContour()")

	let contour;

	function createRectangularContour(width, depth) {
		// Define the rectangular contour based on width and depth
		const contour = [
			0, 0,          // Bottom-left corner
			width, 0,      // Bottom-right corner
			width, depth,  // Top-right corner
			0, depth       // Top-left corner
		];
	
		return contour;
	}

	function createCircularContour(radius, segments) {
		const contour = [];
		const angleStep = (Math.PI * 2) / segments;
	
		for (let i = 0; i < segments; i++) {
			const angle = i * angleStep;
			const x = radius * Math.cos(angle);
			const y = radius * Math.sin(angle);
			contour.push(x, y);
		}
	
		return contour;
	}

	function createOvalContour(horizontalRadius, verticalRadius, segments) {
		const contour = [];
		const angleStep = (Math.PI * 2) / segments;
	
		for (let i = 0; i < segments; i++) {
			const angle = i * angleStep;
			const x = horizontalRadius * Math.cos(angle); // X scaled by horizontal radius
			const y = verticalRadius * Math.sin(angle);   // Y scaled by vertical radius
			contour.push(x, y);
		}
	
		return contour;
	}

	function createLineContour(){}

	function createArcContour(){}

	switch(shape){
		case "rectangle":
				contour = createRectangularContour( dimensions.x, dimensions.y ); //width and depth
			break;
		case "round":
				contour = createCircularContour( dimensions.x, dimensions.z ); //radius and segments
			break;
		case "oval":
				contour = createOvalContour( dimensions.x, dimensions.y, dimensions.z); //width and depth and segments
			break;
		case "haxagon":

			break;
		case "default":
			console.error("unknown shape for contour");
			break;
	}

	return contour;

}


function createProfile( shape, dimensions ){

	console.log( "createProfile()")

	let profile;

	function generateBeveledProfile(){

	}

	function generateFilletedProfile(){
		
	}

	function generateCirclularProfile(radius, segments, angleOffset = - Math.PI / 2) {

		const profileShape = [];

		const angleStep = Math.PI / (segments - 1);  // Divide the half circle into segments

		for (let i = 0; i < segments; i++) {
			const angle = angleOffset + i * angleStep;
			const x = radius * Math.cos(angle);  // X-coordinate
			const y = radius * Math.sin(angle);  // Y-coordinate
			profileShape.push(x, y);  // Add the point (x, y) to the profile shape
		}

		return profileShape;
	}

	switch(shape){
		case "bevel":
			profile = generateBeveledProfile(dimensions.x, dimensions.y, dimensions.z);
		break;
		case "fillet":
			profile = generateFilletedProfile( dimensions.x, dimensions.y, dimensions.z);
		break;
		case "cirlce":
			profile = generateCirclularProfile( dimensions.x, dimensions.y, dimensions.z);
		break;

		case "default":
				console.error("unknown shape for profile");
			break;
	}

	return profile;

}


function addEpsilonToProfile(profile, epsilon){
	let newProfile = []
	for( let coord of profile ){
		coord  += epsilon
		newProfile.push(coord)
	}
	return newProfile
}

// Function to compute the bounding box of the profileShape
function computeProfileBounds(profile) {
	let minX = Infinity;
	let maxX = -Infinity;

	for (let i = 0; i < profile.length; i += 2) {
		const x = profile[i];  // x-coordinate
		if (x < minX) minX = x;
		if (x > maxX) maxX = x;
	}

	return { minX, maxX };
}

function offsetProfilePoints(profile, profileAlignment, minX, maxX ){

	const profileWidth = maxX - minX;

	// Calculate the offset based on the alignment setting
	let offsetX = 0;
	if (profileAlignment === 'min') {
		offsetX = -minX;  // Align to min X
	} else if (profileAlignment === 'max') {
		offsetX = -maxX;  // Align to max X
	} else if (profileAlignment === 'center') {
		offsetX = -(minX + profileWidth / 2);  // Center the profile (default behavior)
	}

	// Apply the X offset to profile
	const offsetProfile = profile.map((val, idx) => {
		if (idx % 2 === 0) {
			return val + offsetX;  // Adjust the X value
		}
		return val;  // Leave Y value unchanged
	});

	return offsetProfile;
}


// Vertices
function calculateVertices( contour, offsetProfile, profilePoints, contourPoints, profileSegments, contourSegments, contourClosed, vertices ){

	// Functions to create group of frame strips, non indexed BufferGeometry
	const len = ( x, y, z ) => Math.sqrt( x * x + y * y + z * z );
	const dot = (x1, y1, z1, x2, y2, z2) => ( x1 * x2 + y1 * y2 + z1 * z2 );

	let i1, i2, i3, i6, j1, j3;
	let xc0, yc0, xc1, yc1, xc2, yc2, xSh, xDiv;
	let dx, dy, dx0, dy0, dx2, dy2;
	let e0x, e0y,e0Length, e2x, e2y, e2Length, ex, ey, eLength;
	let xd, phi, bend;
	let x, y, z, x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4;
	let a, u1, u2, u3, u4, d2, d3;
	const epsilon = 0.000001;


	// Calculate the vertices based on the profile and contour points

	for ( let j = 0; j < profilePoints; j ++ ) {

		for ( let i = 0; i < contourPoints; i ++ ) {

			i2 = 2 * i; 
			
			xc1 = contour[ i2 ];
			yc1 = contour[ i2 + 1 ];
			
			if ( i === 0 ) {
				
				xc0 = contour[ ( contourSegments - 1 ) * 2 ]; // penultimate point
				yc0 = contour[ ( contourSegments - 1 ) * 2 + 1 ];
				
			} else {
						
				xc0 = contour[ i2 - 2 ]; 	// previous point
				yc0 = contour[ i2 - 1 ];
				
			}
			
			if ( i === contourSegments ) {
				
				xc2 = contour[ 2 ];			// second point
				yc2 = contour[ 3 ];
				
			} else {
				
				xc2 = contour[ i2 + 2 ]; 	// next point
				yc2 = contour[ i2 + 3 ];
				
			}	
			
			if ( !contourClosed ) {
				
				if ( i === 0 ) {
					
					// direction
					dx2 = xc2 - xc1;
					dy2 = yc2 - yc1;
					
					// unit vector
					e2Length = Math.sqrt( dx2 * dx2 + dy2 * dy2 );
					
					e2x = dx2 / e2Length;
					e2y = dy2 / e2Length;
					
					// orthogonal
					ex = e2y;
					ey = -e2x;
					
				}
				
				if ( i === contourSegments ) {
					
					// direction
					
					dx0 = xc1 - xc0;
					dy0 = yc1 - yc0;
					
					// unit vector
					e0Length = Math.sqrt( dx0 * dx0 + dy0 * dy0 );
					
					e0x = dx0 / e0Length;
					e0y = dy0 / e0Length;
					
					// orthogonal
					ex = e0y;
					ey = -e0x;
					
				}
				
				xDiv = 1;
				bend = 1;
				
			}
			
			if ( ( i > 0 && i < contourSegments ) || contourClosed ) {
				
				// directions
				
				dx0 = xc0 - xc1;
				dy0 = yc0 - yc1;
				
				dx2 = xc2 - xc1;
				dy2 = yc2 - yc1;
				
				if( Math.abs( ( dy2 / dx2 ) - ( dy0 / dx0 ) ) < epsilon ) { // prevent 0
					
					dy0 += epsilon;
					
				}
				
				if( Math.abs( ( dx2 / dy2 ) - ( dx0 / dy0 ) ) < epsilon ) { // prevent 0
					
					dx0 += epsilon;
					
				}  
				
				// unit vectors
				
				e0Length = Math.sqrt( dx0 * dx0 + dy0 * dy0 );
				
				e0x = dx0 / e0Length;
				e0y = dy0 / e0Length;
				
				e2Length = Math.sqrt( dx2 * dx2 + dy2 * dy2 );
				
				e2x = dx2 / e2Length;
				e2y = dy2 / e2Length;
				
				// direction transformed 
				
				ex = e0x + e2x;
				ey = e0y + e2y;
				
				eLength = Math.sqrt( ex * ex + ey * ey );
				
				ex = ex / eLength;
				ey = ey / eLength;
				
				phi = Math.acos( e2x * e0x + e2y * e0y ) / 2;
				
				bend = Math.sign( dx0 * dy2 - dy0 * dx2 ); // z cross -> curve bending
				
				xDiv = Math.sin( phi );
				
			}

			
			xSh = offsetProfile[ j * 2 ];
			
			xd = xSh / xDiv;
			
			dx = xd * bend * ex;
			dy = xd * bend * ey;
			
			x = xc1 + dx; 
			y = yc1 + dy;
			z = offsetProfile[ j * 2 + 1 ];	// ySh
			
			// store vertex
			vertices[ j ].push( x, y, z );	

			//dApex = xd * Math.cos( phi );
		
		}
		
	}

}

function calculateVerticesPerContourSegment(contour, offsetProfile, profilePoints, contourPoints, profileSegments, contourSegments, contourClosed, vertices) {

    // Utility functions for vector operations
    const len = (x, y, z) => Math.sqrt(x * x + y * y + z * z);
    const dot = (x1, y1, z1, x2, y2, z2) => (x1 * x2 + y1 * y2 + z1 * z2);

    let j2;
    let xc0, yc0, xc1, yc1, xc2, yc2, xSh, xDiv;
    let dx, dy, dx0, dy0, dx2, dy2;
    let e0x, e0y, e0Length, e2x, e2y, e2Length, ex, ey, eLength;
    let xd, phi, bend;
    let x, y, z;
    const epsilon = 0.000001;


    // Iterate over contour points (outer loop)
    for (let j = 0; j < contourPoints; j++) {

        // Iterate over profile points (inner loop)
        for (let i = 0; i < profilePoints; i++) {

            // Index the contour points (x, y)
            j2 = 2 * j;  //for the y

            xc1 = contour[j2];
            yc1 = contour[j2 + 1];

            if (j === 0) {
                xc0 = contour[(contourSegments - 1) * 2]; // penultimate point
                yc0 = contour[(contourSegments - 1) * 2 + 1];
            } 
			else {
                xc0 = contour[j2 - 2]; // previous point
                yc0 = contour[j2 - 1];
            }

            if (j === contourSegments) {
                xc2 = contour[2]; // second point
                yc2 = contour[3];
            } 
			else {
                xc2 = contour[j2 + 2]; // next point
                yc2 = contour[j2 + 3];
            }


            if (!contourClosed) {

                if (j === 0) {
                    // Calculate direction and orthogonal vectors for the first point
                    dx2 = xc2 - xc1;
                    dy2 = yc2 - yc1;
                    e2Length = Math.sqrt(dx2 * dx2 + dy2 * dy2);
                    e2x = dx2 / e2Length;
                    e2y = dy2 / e2Length;
                    ex = e2y;
                    ey = -e2x;
                }
                if (j === contourSegments) {
                    dx0 = xc1 - xc0;
                    dy0 = yc1 - yc0;
                    e0Length = Math.sqrt(dx0 * dx0 + dy0 * dy0);
                    e0x = dx0 / e0Length;
                    e0y = dy0 / e0Length;
                    ex = e0y;
                    ey = -e0x;
                }
                xDiv = 1;
                bend = 1;
            }

            if ((j > 0 && j < contourSegments) || contourClosed) {
                // Directions for the contour segment
                dx0 = xc0 - xc1;
                dy0 = yc0 - yc1;
                dx2 = xc2 - xc1;
                dy2 = yc2 - yc1;

                if (Math.abs((dy2 / dx2) - (dy0 / dx0)) < epsilon) dy0 += epsilon;
                if (Math.abs((dx2 / dy2) - (dx0 / dy0)) < epsilon) dx0 += epsilon;

                e0Length = Math.sqrt(dx0 * dx0 + dy0 * dy0);
                e0x = dx0 / e0Length;
                e0y = dy0 / e0Length;

                e2Length = Math.sqrt(dx2 * dx2 + dy2 * dy2);
                e2x = dx2 / e2Length;
                e2y = dy2 / e2Length;

                // Calculate average direction
                ex = e0x + e2x;
                ey = e0y + e2y;
                eLength = Math.sqrt(ex * ex + ey * ey);
                ex /= eLength;
                ey /= eLength;

                // Calculate bend angle
                phi = Math.acos(e2x * e0x + e2y * e0y) / 2;
                bend = Math.sign(dx0 * dy2 - dy0 * dx2); // Cross product for bend direction

                xDiv = Math.sin(phi);
            }

            // Calculate vertex positions based on profile offset
            xSh = offsetProfile[i * 2];
            xd = xSh / xDiv;
            dx = xd * bend * ex;
            dy = xd * bend * ey;
            x = xc1 + dx;
            y = yc1 + dy;
            z = offsetProfile[i * 2 + 1]; // ySh (profile z position)

            // Store vertex for current profile point in the current contour segment
            vertices[j].push(x, y, z);
        }
    }
}


// Positions and UVs

function calculatePositionsAndUVs( profileSegments, contourSegments, vertices, positions, uvs ){

	// Functions to create group of frame strips, non indexed BufferGeometry
	const len = ( x, y, z ) => Math.sqrt( x * x + y * y + z * z );
	const dot = (x1, y1, z1, x2, y2, z2) => ( x1 * x2 + y1 * y2 + z1 * z2 );

	let i1, i2, i3, i6, j1, j3;
	let xc0, yc0, xc1, yc1, xc2, yc2, xSh, xDiv;
	let dx, dy, dx0, dy0, dx2, dy2;
	let e0x, e0y,e0Length, e2x, e2y, e2Length, ex, ey, eLength;
	let xd, phi, bend;
	let x, y, z, x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4;
	let a, u1, u2, u3, u4, d2, d3;
	const epsilon = 0.000001;


	// Calculate positions and uvs

	for ( let j = 0; j < profileSegments; j ++ ) {
		
		j1 = j + 1;
		j3 = 3 * j;
		
		for ( let i = 0; i < contourSegments; i ++ ) {
			
			i3 = 3 * i;
			i6 = i3 + 3;
			
			x1 = vertices[ j ][ i3 ];
			y1 = vertices[ j ][ i3 + 1 ];
			z1 = vertices[ j ][ i3 + 2 ] ;
			
			x2 = vertices[ j1 ][ i3 ];
			y2 = vertices[ j1 ][ i3 + 1 ];
			z2 = vertices[ j1 ][ i3 + 2 ];
			
			x3 = vertices[ j1 ][ i6 ];
			y3 = vertices[ j1 ][ i6 + 1 ];
			z3 = vertices[ j1 ][ i6 + 2 ];
			
			x4 = vertices[ j ][ i6 ];
			y4 = vertices[ j ][ i6 + 1 ];
			z4 = vertices[ j ][ i6 + 2 ];
			
			positions[ j ].push( x1, y1, z1, x2, y2, z2, x4, y4, z4, x2, y2, z2, x3, y3, z3, x4, y4, z4 );
			
			a = len( x4 - x1, y4 - y1, z4 - z1 );
			
			d2 = dot( x4 - x1, y4 - y1, z4 - z1, x2 - x1, y2 - y1, z2 - z1 ) / a;
			d3 = dot( x1 - x4, y1 - y4, z1 - z4, x3 - x4, y3 - y4, z3 - z4, ) / a;
			
			if ( d2 >= 0 && d3 >= 0 ) {
				
				u1 = 0;
				u2 = d2 / a;
				u3 = 1 - d3 / a;
				u4 = 1;
				
			}
			
			if ( d2 >= 0 && d3 < 0 ) {
				
				u1 = 0;
				u2 = d2 / ( a - d3 );
				u3 = 1;
				u4 = 1 + d3 / ( a - d3 ); 
				
			}
			
			if ( d2 < 0 && d3 < 0 ) {
				
				u1 = -d2 / ( a - d2 - d3 );
				u2 = 0;
				u3 = 1;
				u4 = 1 + d3 / ( a - d2 - d3  );
				
			}
			
			if ( d2 < 0 && d3 >= 0 ) {
				
				u1 = -d2 / ( a - d2  );
				u2 = 0;
				u3 = 1 - d3 / ( a - d2  );
				u4 = 1;
				
			}
			
			uvs[ j ].push( u1, 1, u2, 0, u4, 1, u2, 0, u3, 0, u4, 1 );
			
		}
		
	}

}

function calculatePositionsAndUVsPerContourSegment( profileSegments, contourSegments, vertices, positions, uvs ){

	// Functions to create group of frame strips, non indexed BufferGeometry
	const len = ( x, y, z ) => Math.sqrt( x * x + y * y + z * z );
	const dot = (x1, y1, z1, x2, y2, z2) => ( x1 * x2 + y1 * y2 + z1 * z2 );

	let i1, i2, i3, i6, j1, j3;
	let xc0, yc0, xc1, yc1, xc2, yc2, xSh, xDiv;
	let dx, dy, dx0, dy0, dx2, dy2;
	let e0x, e0y,e0Length, e2x, e2y, e2Length, ex, ey, eLength;
	let xd, phi, bend;
	let x, y, z, x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4;
	let a, u1, u2, u3, u4, d2, d3;
	const epsilon = 0.000001;


	// Calculate positions and uvs per contour segment

	for ( let j = 0; j < contourSegments; j ++ ) {
		
		j1 = j + 1;
		j3 = 3 * j;
		
		for ( let i = 0; i < profileSegments; i ++ ) {
			
			i3 = 3 * i;
			i6 = i3 + 3;
			
			x1 = vertices[ j ][ i3 ];
			y1 = vertices[ j ][ i3 + 1 ];
			z1 = vertices[ j ][ i3 + 2 ] ;
			
			x2 = vertices[ j1 ][ i3 ];
			y2 = vertices[ j1 ][ i3 + 1 ];
			z2 = vertices[ j1 ][ i3 + 2 ];
			
			x3 = vertices[ j1 ][ i6 ];
			y3 = vertices[ j1 ][ i6 + 1 ];
			z3 = vertices[ j1 ][ i6 + 2 ];
			
			x4 = vertices[ j ][ i6 ];
			y4 = vertices[ j ][ i6 + 1 ];
			z4 = vertices[ j ][ i6 + 2 ];
			
			positions[ j ].push( x1, y1, z1, x2, y2, z2, x4, y4, z4, x2, y2, z2, x3, y3, z3, x4, y4, z4 );
			
			a = len( x4 - x1, y4 - y1, z4 - z1 );
			
			d2 = dot( x4 - x1, y4 - y1, z4 - z1, x2 - x1, y2 - y1, z2 - z1 ) / a;
			d3 = dot( x1 - x4, y1 - y4, z1 - z4, x3 - x4, y3 - y4, z3 - z4, ) / a;
			
			if ( d2 >= 0 && d3 >= 0 ) {
				
				u1 = 0;
				u2 = d2 / a;
				u3 = 1 - d3 / a;
				u4 = 1;
				
			}
			
			if ( d2 >= 0 && d3 < 0 ) {
				
				u1 = 0;
				u2 = d2 / ( a - d3 );
				u3 = 1;
				u4 = 1 + d3 / ( a - d3 ); 
				
			}
			
			if ( d2 < 0 && d3 < 0 ) {
				
				u1 = -d2 / ( a - d2 - d3 );
				u2 = 0;
				u3 = 1;
				u4 = 1 + d3 / ( a - d2 - d3  );
				
			}
			
			if ( d2 < 0 && d3 >= 0 ) {
				
				u1 = -d2 / ( a - d2  );
				u2 = 0;
				u3 = 1 - d3 / ( a - d2  );
				u4 = 1;
				
			}
			
			uvs[ j ].push( u1, 1, u2, 0, u4, 1, u2, 0, u3, 0, u4, 1 );
			
		}
		
	}

}

// Meshes

function createMeshes( group, meshes, profileSegments, contourSegments, geometries, positions, uvs, profileMaterialType, profileMaterials ){

	for ( let j = 0; j < profileSegments; j ++ ) {
		
		// Create Buffer Geometry
		geometries[ j ] = new BufferGeometry( );
		geometries[ j ].setAttribute( 'position', new BufferAttribute( new Float32Array( positions[ j ] ), 3 ) );
		geometries[ j ].setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs[ j ] ), 2 ) );

		// Use smooth shading by computing vertex normals
		geometries[j].computeVertexNormals();
		
		// MultiMaterial support for each face
		if ( materialType === "face"  ) {  //matPerSquare
			
			for ( let i = 0; i < contourSegments; i ++ ) {
				
				geometries[ j ].addGroup( i * 6, 6, j * contourSegments + i ); 
				
			}
			
			meshes[ j ] = new Mesh( geometries[ j ], profileMaterials );
			
		} 
		// material per band
		else if( materialType === "band" ){
			meshes[ j ] = new Mesh( geometries[ j ], profileMaterials[ j ] );
		}
		// material per mesh
		else if( materialType === "segment" ){

		}
		else{

			const bufferGeometry = geometries[ j ]

			applyBoxUV( bufferGeometry, 1 )
			bufferGeometry.attributes.uv.needsUpdate = true;

			const mesh = new Mesh( bufferGeometry, profileMaterials[ 0 ] );

			meshes[ j ] = mesh
			
		}
		
		geometries[ j ].computeVertexNormals( );
		
		group.add( meshes[ j ] )
		
	}
}

function createMeshesPerContourSegment( group, meshes, profileSegments, contourSegments, geometries, positions, uvs, profileMaterialType, profileMaterials, profileSmoothShading ){

	for ( let j = 0; j < contourSegments; j ++ ) {
		
		// Create Buffer Geometry
		geometries[ j ] = new BufferGeometry( );
		geometries[ j ].setAttribute( 'position', new BufferAttribute( new Float32Array( positions[ j ] ), 3 ) );
		geometries[ j ].setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs[ j ] ), 2 ) );

		// BufferGeometryUtils.mergeVertices() can only perform the merge if vertex data are identical. 
		// To ensure this, it is necessary to remove the existing normal and uv attribute.
		if( profileSmoothShading ){
			geometries[j].deleteAttribute('normal');
			geometries[j].deleteAttribute('uv');
			geometries[j] = BufferGeometryUtils.mergeVertices(  geometries[j], 1e-4 );

			// Use smooth shading by computing vertex normals
			geometries[j].computeVertexNormals();
		}

		// Use smooth shading by computing vertex normals
		geometries[j].computeVertexNormals();

		
		// MultiMaterial support for each face
		if ( profileMaterialType === "face"  ) {  
			
			for ( let i = 0; i < profileSegments; i ++ ) {
				geometries[ j ].addGroup( i * 6, 6, j * profileSegments + i ); 
			}
			
			meshes[ j ] = new Mesh( geometries[ j ], profileMaterials );
			
		} 
		// material per band
		else if( profileMaterialType === "band" ){
			meshes[ j ] = new Mesh( geometries[ j ], profileMaterials[ j ] );
		}
		// material per mesh
		else if( profileMaterialType === "mesh" ){ 

			console.error("no setup per mesh segment")
		}
		else{ 

			let bufferGeometry = geometries[ j ]

			applyBoxUV( bufferGeometry, 1 );

			bufferGeometry.attributes.uv.needsUpdate = true;
			bufferGeometry.attributes.normal.needsUpdate = true;

			const mesh = new Mesh( bufferGeometry, profileMaterials[0] );

			meshes[ j ] = mesh

		}
		
		group.add( meshes[ j ] )
		
	}
}


// Capping

function createCapProfile( group, profilePoints, vertices, materials, contourSegments ){

	console.log( "createCapProfile" );

	// Cap the start of the contour
	const capStartGeometry = new BufferGeometry();
	const capStartVertices = [];
	
	for (let j = 0; j < profilePoints; j++) {
		const i3 = 3 * 0; // First row
		capStartVertices.push(vertices[j][i3], vertices[j][i3 + 1], vertices[j][i3 + 2]);
	}
	
	capStartGeometry.setAttribute('position', new BufferAttribute(new Float32Array(capStartVertices), 3));
	const capStartMesh = new Mesh( capStartGeometry, materials[4] );
	group.add(capStartMesh);
	
	// Cap the end of the contour
	const capEndGeometry = new BufferGeometry();
	const capEndVertices = [];
	
	for (let j = 0; j < profilePoints; j++) {
		const i3 = 3 * contourSegments; // Last row
		capEndVertices.push(vertices[j][i3], vertices[j][i3 + 1], vertices[j][i3 + 2]);
	}
	
	capEndGeometry.setAttribute('position', new BufferAttribute(new Float32Array(capEndVertices), 3));
	const capEndMesh = new Mesh( capEndGeometry, materials[4] );
	group.add(capEndMesh);

}

// This function still neds to be correctly finished!
function offsetContour(contour, distance) {

    //console.log("contour:", contour);
    //console.log("distance:", distance);

    const newContour = [];
    const numPoints = contour.length / 2;

    // Helper function to normalize a vector
    function normalize(vx, vy) {
        const length = Math.sqrt(vx * vx + vy * vy);
        if (length === 0) {
            // Return [0, 0] to prevent division by zero
            return [0, 0];
        }
        return [vx / length, vy / length];
    }

    // Step 1: Calculate the bounding box of the contour
    let minX = Infinity, maxX = -Infinity;
    let minY = Infinity, maxY = -Infinity;

    for (let i = 0; i < numPoints; i++) {
        const x = contour[i * 2];
        const y = contour[i * 2 + 1];
        if (x < minX) minX = x;
        if (x > maxX) maxX = x;
        if (y < minY) minY = y;
        if (y > maxY) maxY = y;
    }

    // Step 2: For each point, compute the inset/outset direction
    for (let i = 0; i < numPoints; i++) {
        // Get the current point and neighboring points
        const i2 = i * 2;
        const next = (i + 1) % numPoints;
        const prev = (i - 1 + numPoints) % numPoints;

        // Current point
        const x1 = contour[i2];
        const y1 = contour[i2 + 1];

        // Previous point
        const x0 = contour[prev * 2];
        const y0 = contour[prev * 2 + 1];

        // Next point
        const x2 = contour[next * 2];
        const y2 = contour[next * 2 + 1];

        // Compute edge vectors
        const dx0 = x1 - x0;
        const dy0 = y1 - y0;
        const dx1 = x2 - x1;
        const dy1 = y2 - y1;

        // Step 3: Calculate normals for the current point
        const [nx0, ny0] = normalize(-dy0, dx0); // Normal to the previous edge
        const [nx1, ny1] = normalize(-dy1, dx1); // Normal to the next edge

        // Average the two normals to get the bisector direction
        const bisectX = nx0 + nx1;
        const bisectY = ny0 + ny1;

        // Normalize the bisector direction
        const [nbx, nby] = normalize(bisectX, bisectY);

        // Step 4: Move the point by the distance along the bisector (inset or outset)
        const insetX = x1 + nbx * distance;
        const insetY = y1 + nby * distance;

        // Add the new point to the new contour
        newContour.push(insetX, insetY);
    }

    //console.log("offset contour:", newContour);
    return newContour;
}


function createCapContour(profileShape, group, contourPoints, contour, materialType, materials, minX, maxX, profileAlignment) {

    //console.log("alignX:", alignX);
    //console.log("minX:", minX, "maxX:", maxX);

    // Z values for the bottom and top of the profileShape
    const bottomZ = profileShape[1]; // Start of profileShape (Z)
    const topZ = profileShape[profileShape.length - 1]; // End of profileShape (Z)

    // Compute the inset/outset amount based on the profile min/max values
    let offsetAmount;

    switch (profileAlignment) {
        case "min":
            offsetAmount = 0 //minX
            break;
        case "center":
            offsetAmount = (maxX - minX) / 2;
            break;
        case "max":
            offsetAmount = maxX;
            break;
    }

    //console.log("offsetAmount:", offsetAmount);

    // Calculate the inset/outset for the contour
    const insetContour = offsetContour(contour, -offsetAmount);  // Inset contour for the inner cap
   // console.log("insetContour:", insetContour);

    // Get the bounding box of the contour
    const boundingBox = new Box2();
    for (let i = 0; i < contourPoints; i++) {
        boundingBox.expandByPoint(new Vector2(insetContour[2 * i], insetContour[2 * i + 1]));
    }

    const size = boundingBox.getSize(new Vector2());  // Width and height of the contour
	//console.log( "size", size)

    // Create the bottom cap using the inset contour (projected to bottomZ)
    const capBottomGeometry = new BufferGeometry();
    const capBottomVertices = [];
    const capBottomUVs = [];
    const capBottomIndices = [];

    for (let i = 0; i < contourPoints; i++) {
        const i2 = 2 * i;

        // Get each point from the inset contour and project to bottomZ
        const contourX = insetContour[i2];
        const contourY = insetContour[i2 + 1];

        // Add the adjusted vertex to the cap bottom
        capBottomVertices.push(contourX, contourY, bottomZ);

        // Calculate UV coordinates based on 1x1 meter scale
		// Recalculate UVs to ensure 1x1 meter mapping by multiplying by the size in meters
        const u =  ( (contourX - boundingBox.min.x) / size.x ) * size.x ;
        const v = ( (contourY - boundingBox.min.y) / size.y ) * size.y ;
        capBottomUVs.push(u, v);
    }

    // Triangulate the bottom cap
    for (let i = 2; i < contourPoints; i++) {
        capBottomIndices.push(0, i - 1, i);
    }

    capBottomGeometry.setAttribute('position', new BufferAttribute(new Float32Array(capBottomVertices), 3));
    capBottomGeometry.setAttribute('uv', new BufferAttribute(new Float32Array(capBottomUVs), 2)); // Add UVs

	applyBoxUV( capBottomGeometry, 1 )
	capBottomGeometry.attributes.uv.needsUpdate = true;

    capBottomGeometry.setIndex(capBottomIndices); // Set the index for the faces
    capBottomGeometry.computeVertexNormals();

	
	let capBottomMesh;
	if( materialType === "border" ){
		capBottomMesh = new Mesh(capBottomGeometry, materials[1]); // Use appropriate material
	}
	else{
		capBottomMesh = new Mesh(capBottomGeometry, materials[0]); // Use appropriate material
	}

    //const capBottomMesh = new Mesh(capBottomGeometry, materials[0]); // Use appropriate material
    group.add(capBottomMesh);

    // Create the top cap using the inset contour (projected to topZ)
    const capTopGeometry = new BufferGeometry();
    const capTopVertices = [];
    const capTopUVs = [];
    const capTopIndices = [];

    for (let i = 0; i < contourPoints; i++) {
        const i2 = 2 * i;

        // Get each point from the inset contour and project to topZ
        const contourX = insetContour[i2];
        const contourY = insetContour[i2 + 1];

        // Add the adjusted vertex to the cap top
        capTopVertices.push(contourX, contourY, topZ);

        // Calculate UV coordinates based on 1x1 meter scale
		const u =  ( (contourX - boundingBox.min.x) / size.x ) * size.x ;
        const v = ( (contourY - boundingBox.min.y) / size.y ) * size.y ;
        capTopUVs.push(u, v);
    }

    // Triangulate the top cap
    for (let i = 2; i < contourPoints; i++) {
        capTopIndices.push(0, i - 1, i);
    }

    capTopGeometry.setAttribute('position', new BufferAttribute(new Float32Array(capTopVertices), 3));
    capTopGeometry.setAttribute('uv', new BufferAttribute(new Float32Array(capTopUVs), 2)); // Add UVs

	applyBoxUV( capTopGeometry, 1 )
	capTopGeometry.attributes.uv.needsUpdate = true;

    capTopGeometry.setIndex(capTopIndices); // Set the index for the faces
    capTopGeometry.computeVertexNormals();

	let capTopMesh;
	if( materialType === "border" ){
		capTopMesh = new Mesh(capTopGeometry, materials[1]); // Use appropriate material
	}
	else{
		capTopMesh = new Mesh(capTopGeometry, materials[0]); // Use appropriate material
	}

    group.add(capTopMesh);

}


function setCenter( group ){

	group.updateMatrixWorld(true); 

	const boundingBox = new Box3().setFromObject(group);

	const center = boundingBox.getCenter(new Vector3());
	//console.log( center.x, center.y, center.z )

	const zTranslation = ( boundingBox.max.z - boundingBox.min.z ) / 2;

	// Center the group by subtracting the center from its position
	group.position.set( -center.x, -center.y, zTranslation );  //-center.z

	// Set the group to the Z base 
	const yMin = boundingBox.min.z

	// Create a pivot at the center point
	const pivot = new Group();

	// Add the group to the pivot
	pivot.add(group);

	// Now apply the rotation to the pivot instead of the group
	pivot.rotateX(-Math.PI / 2);

	return pivot;

}