// 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;
}