Skip to content

Commit a68bd7b

Browse files
committed
Fix text extrusion with proper vertex deduplication, edge detection, and normal calculation
1 parent 4c91d6e commit a68bd7b

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:
@@ -541,129 +542,145 @@ export class Font {
541542
textToModel(str, x, y, width, height, options) {
542543
({ width, height, options } = this._parseArgs(width, height, options));
543544
const extrude = options?.extrude || 0;
544-
// Step 1: generate glyph contours
545+
545546
let contours = this.textToContours(str, x, y, width, height, options);
546547
if (!Array.isArray(contours[0][0])) {
547548
contours = [contours];
548549
}
549550

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

555557
contours.forEach(glyphContours => {
556558
this._pInst.beginShape();
557-
const outer = glyphContours[0];
558-
outer.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
559-
560-
for (let i = 1; i < glyphContours.length; i++) {
559+
for (const contour of glyphContours) {
561560
this._pInst.beginContour();
562-
glyphContours[i].forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
561+
contour.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
563562
this._pInst.endContour(this._pInst.CLOSE);
564563
}
565-
566564
this._pInst.endShape(this._pInst.CLOSE);
567565
});
568-
566+
this._pInst.pop();
569567
this._pInst._renderer._validateFaces = prevValidateFaces;
570568
});
571569

572570
if (extrude === 0) {
573-
console.log('No extrusion');
574571
return geom;
575572
}
576573

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

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

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

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

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

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

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

0 commit comments

Comments
 (0)