-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Fix textToModel face normals for extruded text #8091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev-2.0
Are you sure you want to change the base?
Changes from all commits
5e557e5
9a7e956
3e567bf
4c91d6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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]; | ||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can probably just |
||
// 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 = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was there a scenario when There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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() { | ||
|
There was a problem hiding this comment.
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:There was a problem hiding this comment.
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.
i add this adding stroke 0 always and also add push