Skip to content

Commit 60ef55e

Browse files
authored
Merge pull request #8091 from VANSH3104/fix-textToModel
Fix textToModel face normals for extruded text
2 parents a0fe46f + 903fd2b commit 60ef55e

File tree

1 file changed

+136
-50
lines changed

1 file changed

+136
-50
lines changed

src/type/p5.Font.js

Lines changed: 136 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { textCoreConstants } from './textCore';
66
import * as constants from '../core/constants';
77
import { UnicodeRange } from '@japont/unicode-range';
88
import { unicodeRanges } from './unicodeRanges';
9+
import { Vector } from '../math/p5.Vector';
910

1011
/*
1112
API:
@@ -542,63 +543,148 @@ export class Font {
542543
textToModel(str, x, y, width, height, options) {
543544
({ width, height, options } = this._parseArgs(width, height, options));
544545
const extrude = options?.extrude || 0;
545-
const contours = this.textToContours(str, x, y, width, height, options);
546546

547+
let contours = this.textToContours(str, x, y, width, height, options);
548+
// Step 2: build base flat geometry - single shape
547549
const geom = this._pInst.buildGeometry(() => {
548-
if (extrude === 0) {
549-
const prevValidateFaces = this._pInst._renderer._validateFaces;
550-
this._pInst._renderer._validateFaces = true;
551-
this._pInst.beginShape();
552-
this._pInst.normal(0, 0, 1);
553-
for (const contour of contours) {
554-
this._pInst.beginContour();
555-
for (const { x, y } of contour) {
556-
this._pInst.vertex(x, y);
557-
}
558-
this._pInst.endContour(this._pInst.CLOSE);
559-
}
560-
this._pInst.endShape();
561-
this._pInst._renderer._validateFaces = prevValidateFaces;
562-
} else {
563-
const prevValidateFaces = this._pInst._renderer._validateFaces;
564-
this._pInst._renderer._validateFaces = true;
565-
566-
// Draw front faces
567-
for (const side of [1, -1]) {
568-
this._pInst.beginShape();
569-
for (const contour of contours) {
570-
this._pInst.beginContour();
571-
for (const { x, y } of contour) {
572-
this._pInst.vertex(x, y, side * extrude * 0.5);
573-
}
574-
this._pInst.endContour(this._pInst.CLOSE);
575-
}
576-
this._pInst.endShape();
577-
}
578-
this._pInst._renderer._validateFaces = prevValidateFaces;
579-
580-
// Draw sides
581-
for (const contour of contours) {
582-
this._pInst.beginShape(this._pInst.QUAD_STRIP);
583-
for (const v of contour) {
584-
for (const side of [-1, 1]) {
585-
this._pInst.vertex(v.x, v.y, side * extrude * 0.5);
586-
}
587-
}
588-
this._pInst.endShape();
550+
const prevValidateFaces = this._pInst._renderer._validateFaces;
551+
this._pInst._renderer._validateFaces = true;
552+
553+
this._pInst.beginShape();
554+
for (const contour of contours) {
555+
this._pInst.beginContour();
556+
for (const pt of contour) {
557+
this._pInst.vertex(pt.x, pt.y, 0);
589558
}
559+
this._pInst.endContour(this._pInst.CLOSE);
590560
}
561+
562+
this._pInst.endShape(this._pInst.CLOSE);
563+
564+
this._pInst._renderer._validateFaces = prevValidateFaces;
591565
});
592-
if (extrude !== 0) {
593-
geom.computeNormals();
594-
for (const face of geom.faces) {
595-
if (face.every(idx => geom.vertices[idx].z <= -extrude * 0.5 + 0.1)) {
596-
for (const idx of face) geom.vertexNormals[idx].set(0, 0, -1);
597-
face.reverse();
598-
}
566+
567+
if (extrude === 0) {
568+
return geom;
569+
}
570+
571+
// The tessellation process creates separate vertices for each triangle,
572+
// even when they share the same position. We need to deduplicate them
573+
// to find which faces are actually connected, so we can identify the
574+
// outer edges for extrusion.
575+
576+
const vertexIndices = {};
577+
const vertexId = v => `${v.x.toFixed(6)}-${v.y.toFixed(6)}-${v.z.toFixed(6)}`;
578+
const newVertices = [];
579+
const newVertexIndex = [];
580+
581+
for (const v of geom.vertices) {
582+
const id = vertexId(v);
583+
if (!(id in vertexIndices)) {
584+
const index = newVertices.length;
585+
vertexIndices[id] = index;
586+
newVertices.push(v.copy());
587+
}
588+
newVertexIndex.push(vertexIndices[id]);
589+
}
590+
591+
// Remap faces to use deduplicated vertices
592+
const newFaces = geom.faces.map(f => f.map(i => newVertexIndex[i]));
593+
594+
//Find outer edges (edges that appear in only one face)
595+
const seen = {};
596+
for (const face of newFaces) {
597+
for (let off = 0; off < face.length; off++) {
598+
const a = face[off];
599+
const b = face[(off + 1) % face.length];
600+
const id = `${Math.min(a, b)}-${Math.max(a, b)}`;
601+
if (!seen[id]) seen[id] = [];
602+
seen[id].push([a, b]);
599603
}
600604
}
601-
return geom;
605+
const validEdges = [];
606+
for (const key in seen) {
607+
if (seen[key].length === 1) {
608+
validEdges.push(seen[key][0]);
609+
}
610+
}
611+
612+
// Step 5: Create extruded geometry
613+
const extruded = this._pInst.buildGeometry(() => {});
614+
const half = extrude * 0.5;
615+
extruded.vertices = [];
616+
extruded.faces = [];
617+
extruded.edges = []; // INITIALIZE EDGES ARRAY
618+
619+
// Add side face vertices (separate for each edge for flat shading)
620+
for (const [a, b] of validEdges) {
621+
const vA = newVertices[a];
622+
const vB = newVertices[b];
623+
624+
// Skip if vertices are too close (degenerate edge)
625+
// We only need to check the perimeter edge length since the other edge
626+
// is the extrude direction, which is always > 0 for extruded geometry
627+
628+
const edgeVector = new Vector(vB.x - vA.x, vB.y - vA.y, vB.z - vA.z);
629+
const extrudeVector = new Vector(0, 0, extrude);
630+
const crossProduct = Vector.cross(edgeVector, extrudeVector);
631+
const dist = edgeVector.mag();
632+
if (crossProduct.mag() < 0.0001 || dist < 0.0001) continue;
633+
// Front face vertices
634+
const frontA = extruded.vertices.length;
635+
extruded.vertices.push(new Vector(vA.x, vA.y, vA.z + half));
636+
const frontB = extruded.vertices.length;
637+
extruded.vertices.push(new Vector(vB.x, vB.y, vB.z + half));
638+
const backA = extruded.vertices.length;
639+
extruded.vertices.push(new Vector(vA.x, vA.y, vA.z - half));
640+
const backB = extruded.vertices.length;
641+
extruded.vertices.push(new Vector(vB.x, vB.y, vB.z - half));
642+
643+
extruded.faces.push([frontA, backA, backB]);
644+
extruded.faces.push([frontA, backB, frontB]);
645+
extruded.edges.push([frontA, frontB]);
646+
extruded.edges.push([backA, backB]);
647+
extruded.edges.push([frontA, backA]);
648+
extruded.edges.push([frontB, backB]);
649+
}
650+
651+
// Add front face (with unshared vertices for flat shading)
652+
const frontVertexOffset = extruded.vertices.length;
653+
for (const v of newVertices) {
654+
extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
655+
}
656+
for (const face of newFaces) {
657+
if (face.length < 3) continue;
658+
const mappedFace = face.map(i => i + frontVertexOffset);
659+
extruded.faces.push(mappedFace);
660+
661+
// ADD EDGES FOR FRONT FACE
662+
for (let i = 0; i < mappedFace.length; i++) {
663+
const nextIndex = (i + 1) % mappedFace.length;
664+
extruded.edges.push([mappedFace[i], mappedFace[nextIndex]]);
665+
}
666+
}
667+
668+
// Add back face (reversed winding order)
669+
const backVertexOffset = extruded.vertices.length;
670+
for (const v of newVertices) {
671+
extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
672+
}
673+
674+
for (const face of newFaces) {
675+
if (face.length < 3) continue;
676+
const mappedFace = [...face].reverse().map(i => i + backVertexOffset);
677+
extruded.faces.push(mappedFace);
678+
679+
// ADD EDGES FOR BACK FACE
680+
for (let i = 0; i < mappedFace.length; i++) {
681+
const nextIndex = (i + 1) % mappedFace.length;
682+
extruded.edges.push([mappedFace[i], mappedFace[nextIndex]]);
683+
}
684+
}
685+
686+
extruded.computeNormals();
687+
return extruded;
602688
}
603689

604690
variations() {

0 commit comments

Comments
 (0)