Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 112 additions & 45 deletions src/type/p5.Font.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,63 +541,130 @@ export class Font {
textToModel(str, x, y, width, height, options) {
({ width, height, options } = this._parseArgs(width, height, options));
const extrude = options?.extrude || 0;
const contours = this.textToContours(str, x, y, width, height, options);
// Step 1: generate glyph contours
let contours = this.textToContours(str, x, y, width, height, options);
if (!Array.isArray(contours[0][0])) {
contours = [contours];
}

// Step 2: build base flat geometry
const geom = this._pInst.buildGeometry(() => {
if (extrude === 0) {
const prevValidateFaces = this._pInst._renderer._validateFaces;
this._pInst._renderer._validateFaces = true;
const prevValidateFaces = this._pInst._renderer._validateFaces;
this._pInst._renderer._validateFaces = true;

contours.forEach(glyphContours => {
this._pInst.beginShape();
this._pInst.normal(0, 0, 1);
for (const contour of contours) {
const outer = glyphContours[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 2.0, you can surround every contour with begin/endContour including the first one! so you dont have to special case the first contour. How the code used to work will suffice here:

for (const contour of contours) {
  this._pInst.beginContour();
  for (const { x, y } of contour) {
    this._pInst.vertex(x, y);
  }
  this._pInst.endContour(this._pInst.CLOSE);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! I updated the code so every contour uses beginContour/endContour, including the first one.and I originally added stroke(0) with a push/pop to make sure edges are generated consistently.
Following your suggestion, I’ll keep only what’s needed and avoid forcing stroke unless required.

const geom = this._pInst.buildGeometry(() => {
      const prevValidateFaces = this._pInst._renderer._validateFaces;
      this._pInst._renderer._validateFaces = true;
      this._pInst.push();
      this._pInst.stroke(0);

      contours.forEach(glyphContours => {
        this._pInst.beginShape();
        for (const contour of glyphContours) {
          this._pInst.beginContour();
          contour.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
          this._pInst.endContour(this._pInst.CLOSE);
        }
        this._pInst.endShape(this._pInst.CLOSE);
      });
      this._pInst.pop();
      this._pInst._renderer._validateFaces = prevValidateFaces;
    });

i add this adding stroke 0 always and also add push

outer.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));

for (let i = 1; i < glyphContours.length; i++) {
this._pInst.beginContour();
for (const { x, y } of contour) {
this._pInst.vertex(x, y);
}
glyphContours[i].forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
this._pInst.endContour(this._pInst.CLOSE);
}
this._pInst.endShape();
this._pInst._renderer._validateFaces = prevValidateFaces;

this._pInst.endShape(this._pInst.CLOSE);
});

this._pInst._renderer._validateFaces = prevValidateFaces;
});

if (extrude === 0) {
console.log('No extrusion');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should take this out before merging so we don't clutter users' console.

return geom;
}

// Step 3: Create extruded geometry with UNSHARED vertices for flat shading
const extruded = this._pInst.buildGeometry(() => {});
const half = extrude * 0.5;

extruded.vertices = [];
extruded.vertexNormals = [];
extruded.faces = [];

let vertexIndex = 0;
const Vector = this._pInst.constructor.Vector;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably just import { Vector } from '../math/p5.Vector'; at the top so that we don't need to import it through here

// Helper to add a triangle with flat normal
const addTriangle = (v0, v1, v2) => {
const edge1 = Vector.sub(v1, v0);
const edge2 = Vector.sub(v2, v0);
const normal = Vector.cross(edge1, edge2);
if (normal.magSq() > 0.0001) {
normal.normalize();
} else {
const prevValidateFaces = this._pInst._renderer._validateFaces;
this._pInst._renderer._validateFaces = true;

// Draw front faces
for (const side of [1, -1]) {
this._pInst.beginShape();
for (const contour of contours) {
this._pInst.beginContour();
for (const { x, y } of contour) {
this._pInst.vertex(x, y, side * extrude * 0.5);
}
this._pInst.endContour(this._pInst.CLOSE);
}
this._pInst.endShape();
}
this._pInst._renderer._validateFaces = prevValidateFaces;

// Draw sides
for (const contour of contours) {
this._pInst.beginShape(this._pInst.QUAD_STRIP);
for (const v of contour) {
for (const side of [-1, 1]) {
this._pInst.vertex(v.x, v.y, side * extrude * 0.5);
}
}
this._pInst.endShape();
}
normal.set(0, 0, 1);
}
});
if (extrude !== 0) {
geom.computeNormals();

// Add vertices (unshared - each triangle gets its own copies)
extruded.vertices.push(v0.copy(), v1.copy(), v2.copy());
extruded.vertexNormals.push(normal.copy(), normal.copy(), normal.copy());
extruded.faces.push([vertexIndex, vertexIndex + 1, vertexIndex + 2]);
vertexIndex += 3;
};

for (const face of geom.faces) {
if (face.length < 3) continue;
const v0 = geom.vertices[face[0]];
for (let i = 1; i < face.length - 1; i++) {
const v1 = geom.vertices[face[i]];
const v2 = geom.vertices[face[i + 1]];
addTriangle(
new Vector(v0.x, v0.y, v0.z + half),
new Vector(v1.x, v1.y, v1.z + half),
new Vector(v2.x, v2.y, v2.z + half)
);
}
}

for (const face of geom.faces) {
if (face.length < 3) continue;
const v0 = geom.vertices[face[0]];
for (let i = 1; i < face.length - 1; i++) {
const v1 = geom.vertices[face[i]];
const v2 = geom.vertices[face[i + 1]];
addTriangle(
new Vector(v0.x, v0.y, v0.z - half),
new Vector(v2.x, v2.y, v2.z - half),
new Vector(v1.x, v1.y, v1.z - half)
);
}
}

// Side faces from edges
let edges = geom.edges;
if (!edges || !Array.isArray(edges)) {
edges = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a scenario when edges was empty? We can try temporarily setting stroke(0) (within a push/pop) when generating the initial geometry to ensure that edges are present if we need. I think if we can do that and guarantee the edges exist, we can take out this block of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a fallback in case edges was empty, but since you mentioned we can guarantee edges by enabling stroke inside a push/pop, I simplified this block and removed the redundant fallback.

const validEdges = geom.edges.filter(([a, b]) => a !== b);

for (const [a, b] of validEdges) {
  const v0 = geom.vertices[a];
  const v1 = geom.vertices[b];

  const vFront0 = new Vector(v0.x, v0.y, v0.z + half);
  const vFront1 = new Vector(v1.x, v1.y, v1.z + half);
  const vBack0 = new Vector(v0.x, v0.y, v0.z - half);
  const vBack1 = new Vector(v1.x, v1.y, v1.z - half);

  extruded.faces.push([vFront0, vBack0, vBack1]);
  extruded.faces.push([vFront0, vBack1, vFront1]);
}

const edgeSet = new Set();
for (const face of geom.faces) {
if (face.every(idx => geom.vertices[idx].z <= -extrude * 0.5 + 0.1)) {
for (const idx of face) geom.vertexNormals[idx].set(0, 0, -1);
face.reverse();
for (let i = 0; i < face.length; i++) {
const a = face[i];
const b = face[(i + 1) % face.length];
if (a === b) continue;
const key = a < b ? `${a},${b}` : `${b},${a}`;
if (!edgeSet.has(key)) {
edgeSet.add(key);
edges.push([a, b]);
}
}
}
}
return geom;

const validEdges = edges.filter(([a, b]) => a !== b);

for (const [a, b] of validEdges) {
const v0 = geom.vertices[a];
const v1 = geom.vertices[b];

const vFront0 = new Vector(v0.x, v0.y, v0.z + half);
const vFront1 = new Vector(v1.x, v1.y, v1.z + half);
const vBack0 = new Vector(v0.x, v0.y, v0.z - half);
const vBack1 = new Vector(v1.x, v1.y, v1.z - half);

// Two triangles forming the side quad
addTriangle(vFront0, vBack0, vBack1);
addTriangle(vFront0, vBack1, vFront1);
}
return extruded;
}

variations() {
Expand Down
Loading