Skip to content

Commit 38fef3a

Browse files
committed
Fix text extrusion with proper vertex deduplication, edge detection, and normal calculation
1 parent 546ec24 commit 38fef3a

File tree

1 file changed

+102
-85
lines changed

1 file changed

+102
-85
lines changed

src/type/p5.Font.js

Lines changed: 102 additions & 85 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,129 +543,145 @@ 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-
// Step 1: generate glyph contours
546+
546547
let contours = this.textToContours(str, x, y, width, height, options);
547548
if (!Array.isArray(contours[0][0])) {
548549
contours = [contours];
549550
}
550551

551-
// Step 2: build base flat geometry
552552
const geom = this._pInst.buildGeometry(() => {
553553
const prevValidateFaces = this._pInst._renderer._validateFaces;
554554
this._pInst._renderer._validateFaces = true;
555+
this._pInst.push();
556+
this._pInst.stroke(0);
555557

556558
contours.forEach(glyphContours => {
557559
this._pInst.beginShape();
558-
const outer = glyphContours[0];
559-
outer.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
560-
561-
for (let i = 1; i < glyphContours.length; i++) {
560+
for (const contour of glyphContours) {
562561
this._pInst.beginContour();
563-
glyphContours[i].forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
562+
contour.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
564563
this._pInst.endContour(this._pInst.CLOSE);
565564
}
566-
567565
this._pInst.endShape(this._pInst.CLOSE);
568566
});
569-
567+
this._pInst.pop();
570568
this._pInst._renderer._validateFaces = prevValidateFaces;
571569
});
572570

573571
if (extrude === 0) {
574-
console.log('No extrusion');
575572
return geom;
576573
}
577574

578-
// Step 3: Create extruded geometry with UNSHARED vertices for flat shading
575+
const vertexIndices = {};
576+
const vertexId = v => `${v.x.toFixed(6)}-${v.y.toFixed(6)}-${v.z.toFixed(6)}`;
577+
const newVertices = [];
578+
const newVertexIndex = [];
579+
580+
for (const v of geom.vertices) {
581+
const id = vertexId(v);
582+
if (!(id in vertexIndices)) {
583+
const index = newVertices.length;
584+
vertexIndices[id] = index;
585+
newVertices.push(v.copy());
586+
}
587+
newVertexIndex.push(vertexIndices[id]);
588+
}
589+
590+
// Remap faces to use deduplicated vertices
591+
const newFaces = geom.faces.map(f => f.map(i => newVertexIndex[i]));
592+
593+
//Find outer edges (edges that appear in only one face)
594+
const seen = {};
595+
for (const face of newFaces) {
596+
for (let off = 0; off < face.length; off++) {
597+
const a = face[off];
598+
const b = face[(off + 1) % face.length];
599+
const id = `${Math.min(a, b)}-${Math.max(a, b)}`;
600+
if (!seen[id]) seen[id] = [];
601+
seen[id].push([a, b]);
602+
}
603+
}
604+
const validEdges = [];
605+
for (const key in seen) {
606+
if (seen[key].length === 1) {
607+
validEdges.push(seen[key][0]);
608+
}
609+
}
610+
611+
console.log(`Found ${validEdges.length} outer edges from ${Object.keys(seen).length} total edges`);
612+
613+
// Step 5: Create extruded geometry
579614
const extruded = this._pInst.buildGeometry(() => {});
580615
const half = extrude * 0.5;
581-
582616
extruded.vertices = [];
583-
extruded.vertexNormals = [];
584617
extruded.faces = [];
618+
extruded.edges = []; // INITIALIZE EDGES ARRAY
585619

586-
let vertexIndex = 0;
587-
const Vector = this._pInst.constructor.Vector;
588-
// Helper to add a triangle with flat normal
589-
const addTriangle = (v0, v1, v2) => {
590-
const edge1 = Vector.sub(v1, v0);
591-
const edge2 = Vector.sub(v2, v0);
592-
const normal = Vector.cross(edge1, edge2);
593-
if (normal.magSq() > 0.0001) {
594-
normal.normalize();
595-
} else {
596-
normal.set(0, 0, 1);
597-
}
598-
599-
// Add vertices (unshared - each triangle gets its own copies)
600-
extruded.vertices.push(v0.copy(), v1.copy(), v2.copy());
601-
extruded.vertexNormals.push(normal.copy(), normal.copy(), normal.copy());
602-
extruded.faces.push([vertexIndex, vertexIndex + 1, vertexIndex + 2]);
603-
vertexIndex += 3;
604-
};
605-
606-
for (const face of geom.faces) {
607-
if (face.length < 3) continue;
608-
const v0 = geom.vertices[face[0]];
609-
for (let i = 1; i < face.length - 1; i++) {
610-
const v1 = geom.vertices[face[i]];
611-
const v2 = geom.vertices[face[i + 1]];
612-
addTriangle(
613-
new Vector(v0.x, v0.y, v0.z + half),
614-
new Vector(v1.x, v1.y, v1.z + half),
615-
new Vector(v2.x, v2.y, v2.z + half)
616-
);
617-
}
620+
// Add side face vertices (separate for each edge for flat shading)
621+
for (const [a, b] of validEdges) {
622+
const vA = newVertices[a];
623+
const vB = newVertices[b];
624+
// Skip if vertices are too close (degenerate edge)
625+
const dist = Math.sqrt(
626+
Math.pow(vB.x - vA.x, 2) +
627+
Math.pow(vB.y - vA.y, 2) +
628+
Math.pow(vB.z - vA.z, 2)
629+
);
630+
if (dist < 0.0001) continue;
631+
// Front face vertices
632+
const frontA = extruded.vertices.length;
633+
extruded.vertices.push(new Vector(vA.x, vA.y, vA.z + half));
634+
const frontB = extruded.vertices.length;
635+
extruded.vertices.push(new Vector(vB.x, vB.y, vB.z + half));
636+
const backA = extruded.vertices.length;
637+
extruded.vertices.push(new Vector(vA.x, vA.y, vA.z - half));
638+
const backB = extruded.vertices.length;
639+
extruded.vertices.push(new Vector(vB.x, vB.y, vB.z - half));
640+
641+
extruded.faces.push([frontA, backA, backB]);
642+
extruded.faces.push([frontA, backB, frontB]);
643+
extruded.edges.push([frontA, frontB]);
644+
extruded.edges.push([backA, backB]);
645+
extruded.edges.push([frontA, backA]);
646+
extruded.edges.push([frontB, backB]);
618647
}
619648

620-
for (const face of geom.faces) {
621-
if (face.length < 3) continue;
622-
const v0 = geom.vertices[face[0]];
623-
for (let i = 1; i < face.length - 1; i++) {
624-
const v1 = geom.vertices[face[i]];
625-
const v2 = geom.vertices[face[i + 1]];
626-
addTriangle(
627-
new Vector(v0.x, v0.y, v0.z - half),
628-
new Vector(v2.x, v2.y, v2.z - half),
629-
new Vector(v1.x, v1.y, v1.z - half)
630-
);
631-
}
649+
// Add front face (with unshared vertices for flat shading)
650+
const frontVertexOffset = extruded.vertices.length;
651+
for (const v of newVertices) {
652+
extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
632653
}
654+
for (const face of newFaces) {
655+
if (face.length < 3) continue;
656+
const mappedFace = face.map(i => i + frontVertexOffset);
657+
extruded.faces.push(mappedFace);
633658

634-
// Side faces from edges
635-
let edges = geom.edges;
636-
if (!edges || !Array.isArray(edges)) {
637-
edges = [];
638-
const edgeSet = new Set();
639-
for (const face of geom.faces) {
640-
for (let i = 0; i < face.length; i++) {
641-
const a = face[i];
642-
const b = face[(i + 1) % face.length];
643-
if (a === b) continue;
644-
const key = a < b ? `${a},${b}` : `${b},${a}`;
645-
if (!edgeSet.has(key)) {
646-
edgeSet.add(key);
647-
edges.push([a, b]);
648-
}
649-
}
659+
// ADD EDGES FOR FRONT FACE
660+
for (let i = 0; i < mappedFace.length; i++) {
661+
const nextIndex = (i + 1) % mappedFace.length;
662+
extruded.edges.push([mappedFace[i], mappedFace[nextIndex]]);
650663
}
651664
}
652665

653-
const validEdges = edges.filter(([a, b]) => a !== b);
654-
655-
for (const [a, b] of validEdges) {
656-
const v0 = geom.vertices[a];
657-
const v1 = geom.vertices[b];
666+
// Add back face (reversed winding order)
667+
const backVertexOffset = extruded.vertices.length;
668+
for (const v of newVertices) {
669+
extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
670+
}
658671

659-
const vFront0 = new Vector(v0.x, v0.y, v0.z + half);
660-
const vFront1 = new Vector(v1.x, v1.y, v1.z + half);
661-
const vBack0 = new Vector(v0.x, v0.y, v0.z - half);
662-
const vBack1 = new Vector(v1.x, v1.y, v1.z - half);
672+
for (const face of newFaces) {
673+
if (face.length < 3) continue;
674+
const mappedFace = [...face].reverse().map(i => i + backVertexOffset);
675+
extruded.faces.push(mappedFace);
663676

664-
// Two triangles forming the side quad
665-
addTriangle(vFront0, vBack0, vBack1);
666-
addTriangle(vFront0, vBack1, vFront1);
677+
// ADD EDGES FOR BACK FACE
678+
for (let i = 0; i < mappedFace.length; i++) {
679+
const nextIndex = (i + 1) % mappedFace.length;
680+
extruded.edges.push([mappedFace[i], mappedFace[nextIndex]]);
681+
}
667682
}
683+
684+
extruded.computeNormals();
668685
return extruded;
669686
}
670687

0 commit comments

Comments
 (0)