Print at Apr 22, 2026, 12:11:33 AM

Posted by dorin at Apr 11, 2026, 7:39:50 PM
Re: Modeller 3D plugin
My final dialog with one of the AI after few days of trying and fail.

Q: With TRIANGLE_ARRAY shapes work as expected but with QUAD's don't.
Could QUAD_ARRAY to not be proper solution for modeling shapes?


A: Yes, this is a fundamental representation problem. QUAD_ARRAY is the wrong primitive for general 3D modeling, and here's why:
QUAD_ARRAY limitations
• Quads are not guaranteed to be planar. After extrusion, scaling, or any non-uniform deformation, the 4 vertices of a face will generally not lie in the same plane. The renderer then silently splits each quad into 2 triangles — but it picks the split diagonal arbitrarily, producing inconsistent shading, incorrect normals, and visual artifacts.
• Shared vertex topology is not expressible. QUAD_ARRAY (like TRIANGLE_ARRAY) is an unindexed flat list — each face owns its own 4 vertices, so adjacent faces don't actually share vertices. This means:
◦ Welding/merging operations require explicit de-duplication
◦ Normal smoothing across edges is unreliable
◦ Operations like your extrude must manually track edge adjacency (your edgeUsage map is a workaround for this missing structure)
• Winding order ambiguity is worse with quads. With triangles, the cross product always gives an unambiguous normal. A non-planar quad has no single well-defined normal — different renderers resolve this differently.

What professional modelers use
The industry-standard internal representation is a half-edge mesh (also called a winged-edge or doubly-connected edge list):
Vertex → outgoing HalfEdge
HalfEdge → next, prev, twin, face, vertex
Face → one HalfEdge
This gives you O(1) access to:
• All faces around a vertex
• Both faces sharing an edge
• All edges around a face
• Boundary detection (twin == null)
Operations like extrude, bevel, loop cut, and subdivision become clean algorithms rather than index-hunting workarounds. Blender, Maya, and Wings3D all use half-edge internally, rendering to triangles only at display time.

Practical recommendation for your codebase
A full half-edge refactor is large, but a meaningful intermediate step is switching to an indexed triangle mesh with explicit adjacency:
class Mesh {
List<Point3f> vertices; // shared, indexed
List<int[]> triangles; // 3 vertex indices per face
List<String> faceNames;
// Derived on demand, not stored redundantly:
Map<String, int[]> edgeToFaces; // "vA-vB" → [faceLeft, faceRight]
}

• Triangles are always planar — no ambiguity
• Shared vertices are explicit — smoothing and welding are trivial
• Boundary edges are simply edges where edgeToFaces has only one entry — your edgeUsage map, but built into the structure
• Extrude, bevel, and subdivision algorithms are well-documented for triangle meshes
The rendering side doesn't change at all — you're already outputting triangles to the GPU anyway. QUAD_ARRAY was just an illusion of quads on top of triangle rendering.

Q: Good. Where to put and use this class? Or I need to start all from scratch?

A: You don't need to start from scratch. The cleanest approach is to use Mesh as an internal working representation inside SolidShape, converting to and from it only at the boundaries — load and display.

Current architecture (guessed from your code):
SolidShape
List<Point3f> vertices ← flat, unindexed
List<int[]> faces ← QUAD_ARRAY indices
List<String> faceNames

ShapeNode → SolidShape → renders via QUAD_ARRAY

Target architecture — minimal disruption:
SolidShape
Mesh mesh ← new internal representation

// Converters (called at load/save/render boundaries only)
void fromQuadArray(...) ← import legacy data
int[] toTriangleArray() ← export to renderer

// All editing operations now delegate to mesh
addVertex(), addFace(), etc.
The rest of your codebase — ShapeNode, the editor, undo/redo via takeFullSnapshot() — touches SolidShape only, so they don't need to change.

Where each piece lives:
Mesh.java ← new file, pure geometry, no rendering
List<Point3f> vertices
List<int[]> triangles ← always tris, always planar
List<String> faceNames
Map<String,int[]> edgeToFaces ← built on demand

SolidShape.java ← owns a Mesh, exposes editing API
Mesh mesh
fromQuadArray() ← called once on load/import
toTriangleArray() ← called by ShapeNode at render time
takeFullSnapshot() ← serializes mesh state for undo
restoreSnapshot()

ShapeNode.java ← no changes needed
update() ← calls ss.toTriangleArray() as before

Editor / directionalExtrudeSelected() ← operates on SolidShape API
← no longer touches faces/vertices directly

Migration path — incremental, not a rewrite:
Step 1 — Add Mesh.java, keep everything else unchanged. No breakage.
Step 2 — Add SolidShape.fromQuadArray() and SolidShape.toTriangleArray(). Wire toTriangleArray() into the existing render path. Validate visually — shapes should look identical.
Step 3 — Rewrite one operation (e.g. directionalExtrudeSelected) to work on mesh internally. Everything else still uses the old path. Test that one operation.
Step 4 — Migrate remaining operations one by one. Each is independently testable.
Step 5 — Once all operations use mesh, remove the old vertices/faces lists from SolidShape.

The snapshot/undo system is the only tricky part. Your current takeFullSnapshot() presumably serializes the flat arrays. You'll need to update it to serialize Mesh instead — but the interface (float[] before = ss.takeFullSnapshot()) doesn't need to change, just the implementation.

So in short: create Mesh.java as a new standalone class, embed it in SolidShape, and migrate operations one at a time. The editor, renderer, and undo system stay structurally the same. The total surface area that needs changing is just SolidShape internals.

(I've implemented the recommendations.)

Q: Thanks! It is worse now. I'll return to triangles. Probably at the end (when finally exported) could be converted to Quad.

A: That's the right call. Keep triangles internally, convert to quads only at export if needed. Let's go back to the clean triangle-based version without the Mesh layer — simpler, working, and honest about what the geometry actually is.
To make sure I give you exactly the right code:
which part is broken — is it directionalExtrudeSelected specifically, or did the Mesh.fromSolidShape / toSolidShape conversion also corrupt other operations like selection highlighting or undo?

Q: Doesn't mater. I have to make few steps back to a specific point and continue on another direction.

A: Understood — sometimes the cleanest move is to roll back to a known good state and branch from there.
Good luck with the new direction, and feel free to come back when you need another pair of eyes on the code!

Here is my last version on this direction.
----------------------------------------
A computer program does what you tell it to do, not what you want it to do. Murphy's Law
When all else fails, read the instructions. Murphy's Law
If you don't like "AS IS", DIY. Dorin's law