Module: Mesh Generators
Table Of Contents
create-ground.ts)create-torus.ts)create-sphere.ts)create-box.ts)create-cylinder.ts)create-plane.ts)create-disc.ts)create-polyhedron.ts)create-ribbon.ts)create-tube.ts)create-extrude.ts)path3d.ts)> Package path: packages/babylon-lite/src/mesh/
Purpose
Procedural mesh generation for the core BabylonJS MeshBuilder shape set: ground (with heightmap support), torus, UV sphere, box, cylinder/cone, plane, disc/ring, polyhedron, ribbon, tube, and extruded shape. Each generator produces CPU-side vertex data (positions, normals, UVs, indices) and — where applicable — a companion upload helper. All generators match Babylon.js MeshBuilder output exactly; parity is enforced pixel-perfect by scene 38 (MAD = 0.000).
Public API Surface
Ground (create-ground.ts)
export interface GroundData { positions: Float32Array; // vertexCount × 3 normals: Float32Array; // vertexCount × 3 uvs: Float32Array; // vertexCount × 2 indices: Uint32Array; // indexCount}
export interface GroundOptions { width?: number; // Default: 1 height?: number; // Default: 1 subdivisions?: number; // Default: 1 minHeight?: number; // Default: 0 maxHeight?: number; // Default: 1}
export interface GroundGPU { positionBuffer: GPUBuffer; normalBuffer: GPUBuffer; uvBuffer: GPUBuffer; indexBuffer: GPUBuffer; indexCount: number;}
export function createFlatGroundData(opts?: GroundOptions): GroundData;
export function applyHeightmap( ground: GroundData, heightmapData: Uint8ClampedArray, hmWidth: number, hmHeight: number, subdivisions: number, minHeight: number, maxHeight: number): void;
export async function createGroundFromHeightMap(heightmapUrl: string, opts?: GroundOptions): Promise<GroundData>;Torus (create-torus.ts)
export interface TorusData { positions: Float32Array; // vertexCount × 3 normals: Float32Array; // vertexCount × 3 uvs: Float32Array; // vertexCount × 2 indices: Uint32Array; // indexCount}
export interface TorusOptions { diameter?: number; // Default: 1 thickness?: number; // Default: 0.5 tessellation?: number; // Default: 16}
export interface TorusGPU { positionBuffer: GPUBuffer; normalBuffer: GPUBuffer; uvBuffer: GPUBuffer; indexBuffer: GPUBuffer; indexCount: number;}
export function createTorusData(opts?: TorusOptions): TorusData;export function uploadTorusToGPU(device: GPUDevice, data: TorusData): TorusGPU;Sphere (create-sphere.ts)
export interface SphereMeshData { positions: Float32Array; // vertexCount × 3 normals: Float32Array; // vertexCount × 3 indices: Uint32Array; // indexCount vertexCount: number; indexCount: number;}
export interface SphereOptions { segments?: number; // Default: 32 (minimum: 3) diameter?: number; // Default: 1 diameterX?: number; // Default: diameter diameterY?: number; // Default: diameter diameterZ?: number; // Default: diameter}
export function createSphereData(options?: SphereOptions): SphereMeshData;
export function uploadSphereToGPU(device: GPUDevice, data: SphereMeshData): { posBuffer: GPUBuffer; normBuffer: GPUBuffer; idxBuffer: GPUBuffer; idxCount: number };Box (create-box.ts)
export interface BoxData { positions: Float32Array; // 24 × 3 = 72 floats normals: Float32Array; // 24 × 3 = 72 floats indices: Uint32Array; // 36 indices vertexCount: number; // always 24 indexCount: number; // always 36}
export interface BoxGPU { posBuffer: GPUBuffer; normBuffer: GPUBuffer; idxBuffer: GPUBuffer; idxCount: number;}
export function createBoxData(size?: number): BoxData; // Default: size = 1export function uploadBoxToGPU(device: GPUDevice, data: BoxData): BoxGPU;Cylinder (create-cylinder.ts)
export interface CylinderOptions { height?: number; // Default: 2 diameter?: number; // Default: 1 (overrides diameterTop / diameterBottom) diameterTop?: number; // Default: 1 (0 → cone) diameterBottom?: number; // Default: 1 tessellation?: number; // Default: 24 subdivisions?: number; // Default: 1 arc?: number; // Default: 1 (partial wedge)}
export function createCylinderData(opts?: CylinderOptions): MeshData;Plane (create-plane.ts)
export interface PlaneOptions { size?: number; // Default: 1 (shorthand for width + height) width?: number; height?: number;}
export function createPlaneData(opts?: PlaneOptions): MeshData;Disc / Ring (create-disc.ts)
export interface DiscOptions { radius?: number; // Default: 0.5 tessellation?: number; // Default: 64 arc?: number; // Default: 1 (<1 → pie slice / ring)}
export function createDiscData(opts?: DiscOptions): MeshData;Polyhedron (create-polyhedron.ts)
export interface PolyhedronOptions { type?: number; // 0-14 (0=tetra, 3=icosahedron, …) size?: number; // Default: 1 (uniform scale) sizeX?: number; sizeY?: number; sizeZ?: number; flat?: boolean; // Default: true (duplicate verts per face)}
export function createPolyhedronData(opts?: PolyhedronOptions): MeshData;Ribbon (create-ribbon.ts)
export interface RibbonOptions { pathArray: Vec3[][]; closeArray?: boolean; // Default: false closePath?: boolean; // Default: false offset?: number; // Default: pathArray[0].length / 2 sideOrientation?: number;}
export function createRibbonData(opts: RibbonOptions): MeshData;Tube (create-tube.ts)
export const CAP_NONE = 0;export const CAP_START = 1;export const CAP_END = 2;export const CAP_ALL = 3;
export interface TubeOptions { path: Vec3[]; radius?: number; // Default: 1 tessellation?: number; // Default: 64 radiusFunction?: (i: number, distance: number) => number; cap?: number; // Default: CAP_NONE arc?: number; // Default: 1}
export function createTubeData(opts: TubeOptions): MeshData;Extrude Shape (create-extrude.ts)
export interface ExtrudeShapeOptions { shape: Vec3[]; path: Vec3[]; scale?: number; // Default: 1 rotation?: number; // Default: 0 (radians accumulated per path step) cap?: number; // Default: CAP_NONE closeShape?: boolean; closePath?: boolean;}
export function createExtrudeShapeData(opts: ExtrudeShapeOptions): MeshData;Shared types
// All new builders return this common shape.export interface MeshData { positions: Float32Array; normals: Float32Array; uvs: Float32Array; indices: Uint32Array;}Internal Architecture
Ground
Grid layout: (subdivisions + 1) × (subdivisions + 1) vertices.
Vertex counts:
- Vertices:
(subdivisions + 1)² - Indices:
subdivisions² × 6
Vertex position formula:
x = -width/2 + (col / subdivisions) * widthy = 0 (flat; displaced by heightmap)z = -height/2 + (1 - row / subdivisions) * heightUV formula:
u = col / subdivisionsv = 1 - row / subdivisionsInitial normals: All (0, 1, 0) (up).
Index generation (per quad):
topLeft = row * cols + coltopRight = topLeft + 1bottomLeft = (row + 1) * cols + colbottomRight = bottomLeft + 1
Triangle 1: topLeft, bottomLeft, bottomRightTriangle 2: topLeft, bottomRight, topRightHeightmap displacement (applyHeightmap):
-
For each vertex at
(row, col):- Sample heightmap at pixel
(px, py):u = col / subdivisionsv = row / subdivisions (row 0 = top of image)px = floor(u * (hmWidth - 1))py = floor(v * (hmHeight - 1)) - Compute weighted luminance:
gradient = r * 0.3 + g * 0.59 + b * 0.11 - Displace Y:
position.y = minHeight + gradient * (maxHeight - minHeight)
- Sample heightmap at pixel
-
Recompute normals:
- Reset all normals to zero
- For each triangle: compute face normal via cross product of edge vectors, negate the result (due to Z-flip from
1 - row/subdivisions), normalize to unit length, then accumulate onto each triangle's 3 vertices - Normalize all vertex normals
Cross product formula for face normals:
e1 = p1 - p0e2 = p2 - p0fn = -(e1 × e2) // negatedfn = fn / |fn| // normalize before accumulationAsync heightmap loading (createGroundFromHeightMap):
- Create flat ground data
- Load image via
new Image()withcrossOrigin = 'anonymous' - Draw to canvas, extract
ImageData - Call
applyHeightmap
Torus
Parameterization (matches Babylon.js Mesh.CreateTorus):
R = diameter / 2 // major radius (default: 0.5)r = thickness / 2 // tube radius (default: 0.25)stride = tessellation + 1
outerAngle = i * 2π / tessellation - π/2 // around major ringinnerAngle = j * 2π / tessellation + π // around tube cross-sectionVertex counts:
- Vertices:
(tessellation + 1)² - Indices:
(tessellation + 1)² × 6(includes wrapping seam geometry)
Position formula:
dx = cos(innerAngle)dy = sin(innerAngle)
x = (dx * r + R) * cos(outerAngle)y = dy * rz = -(dx * r + R) * sin(outerAngle)Normal formula (rotate tube normal by Y-axis rotation):
nx = dx * cos(outerAngle)ny = dynz = -dx * sin(outerAngle)UV formula:
u = i / tessellationv = 1 - j / tessellationIndex generation (per quad with wrapping):
nextI = (i + 1) % stridenextJ = (j + 1) % stride
Triangle 1: (i*stride + j), (i*stride + nextJ), (nextI*stride + j)Triangle 2: (i*stride + nextJ), (nextI*stride + nextJ), (nextI*stride + j)Sphere
Parameterization (matches Babylon.js MeshBuilder.CreateSphere):
totalZRotationSteps = 2 + segments // vertical rowstotalYRotationSteps = 2 * totalZRotationSteps // horizontal columns
rx = (diameterX ?? diameter) / 2 // default: 0.5ry = (diameterY ?? diameter) / 2rz = (diameterZ ?? diameter) / 2Default tessellation: segments = 32 → totalZ = 34, totalY = 68 → 35 × 69 = 2415 vertices.
Vertex counts:
- Vertices:
(totalZRotationSteps + 1) × (totalYRotationSteps + 1) - Indices:
totalZRotationSteps × totalYRotationSteps × 6
Position formula:
angleZ = (zStep / totalZRotationSteps) * π // polar angle [0, π]angleY = (yStep / totalYRotationSteps) * 2π // azimuthal angle [0, 2π]
nx = sin(angleZ) * cos(angleY)ny = cos(angleZ)nz = sin(angleZ) * sin(angleY)
position = (rx * nx, ry * ny, rz * nz)normal = (nx, ny, nz)Index generation (per quad):
a = zStep * (totalYRotationSteps + 1) + yStepb = a + totalYRotationSteps + 1
Triangle 1: a, b, a+1Triangle 2: a+1, b, b+1Note: No UV coordinates are generated. The sphere only has positions, normals, and indices.
Box
Static geometry — uses pre-computed constant arrays.
Vertex count: 24 (4 per face × 6 faces) Index count: 36 (2 triangles × 3 indices × 6 faces)
Face order: +Z, -Z, +X, -X, +Y, -Y
Vertex positions (at size = 1, half-extent = 0.5):
| Face | V0 | V1 | V2 | V3 | Normal |
|---|---|---|---|---|---|
Face +Z | V0 (0.5,-0.5,0.5) | V1 (-0.5,-0.5,0.5) | V2 (-0.5,0.5,0.5) | V3 (0.5,0.5,0.5) | Normal (0,0,1) |
Face -Z | V0 (0.5,0.5,-0.5) | V1 (-0.5,0.5,-0.5) | V2 (-0.5,-0.5,-0.5) | V3 (0.5,-0.5,-0.5) | Normal (0,0,-1) |
Face +X | V0 (0.5,0.5,-0.5) | V1 (0.5,-0.5,-0.5) | V2 (0.5,-0.5,0.5) | V3 (0.5,0.5,0.5) | Normal (1,0,0) |
Face -X | V0 (-0.5,0.5,0.5) | V1 (-0.5,-0.5,0.5) | V2 (-0.5,-0.5,-0.5) | V3 (-0.5,0.5,-0.5) | Normal (-1,0,0) |
Face +Y | V0 (-0.5,0.5,0.5) | V1 (-0.5,0.5,-0.5) | V2 (0.5,0.5,-0.5) | V3 (0.5,0.5,0.5) | Normal (0,1,0) |
Face -Y | V0 (0.5,-0.5,0.5) | V1 (0.5,-0.5,-0.5) | V2 (-0.5,-0.5,-0.5) | V3 (-0.5,-0.5,0.5) | Normal (0,-1,0) |
Index pattern per face:
[base+0, base+1, base+2], [base+0, base+2, base+3]Complete indices:
[0,1,2], [0,2,3], [4,5,6], [4,6,7], [8,9,10], [8,10,11],[12,13,14], [12,14,15], [16,17,18], [16,18,19], [20,21,22], [20,22,23]Scaling: When size ≠ 1, all position coordinates are multiplied by size. Normals remain unchanged. When size = 1, the pre-computed BOX_POSITIONS constant is returned directly (no allocation).
Cylinder
Ported from BJS VertexData.CreateCylinder. Builds subdivisions + 1 radial rings interpolated between diameterBottom and diameterTop, plus CAP_ALL triangle fans at both ends. Normals are computed from the slant angle so cones and prisms shade correctly. arc < 1 produces a partial wedge with a seam.
Plane
Single quad in the XY plane; normal (0, 0, -1); UVs [0,1]; index order (0,1,2),(0,2,3) → winding matches frontFace: "ccw" with the -Z normal.
Disc / Ring
Triangle fan from the center, tessellation outer vertices around 2π · arc radians. Normal is hard-coded (0, 0, -1). With arc < 1, the fan forms a pie slice; mesh thickness-of-zero rings are produced by sampling the same shape at two radii (extrude path).
Polyhedron
BJS preset tables (polyhedron-data.ts) contain vertex positions and face index lists for 15 polyhedra. The builder scales by sizeX/Y/Z then tessellates each face:
flat = true(default): each face contributes independent vertices with a single face normal (cross product of two edge vectors). No vertex sharing across faces.flat = false: face vertices are shared; normals are averaged viacompute-normals.tsfor smooth shading.
Ribbon
The base primitive that powers tube and extrude. Given a pathArray (rows of equal-length 3D paths), it:
- Concatenates all rows into one positions buffer (row-major).
- Emits two triangles per quad stitched between consecutive paths.
- Normalizes UVs via cumulative edge distances along each row and column (matches BJS).
- Computes per-vertex normals via
compute-normals.ts. - If
closePath, duplicates each row's first vertex at the end and averages the seam normal. - If
closeArray, stitches the last path back to the first and averages those seam normals too.
Tube
Builds a ribbon where each row is a circle of tessellation vertices around path[i], radius radius (or radiusFunction(i, distance)):
- Computes Path3D frames →
tangents[i],normals[i],binormals[i]. - For each
path[i], starts fromnormals[i] * radiusand rotates it aroundtangents[i]by2π · arc / tessellationvia Rodrigues' rotation formula to build the ring. - Always sets
closePath = trueso the ring seals. capinserts extra rows at the start/end: a barycenter vertex plus a duplicate ring at zero scale (matches BJS cap geometry).
Extrude Shape
Sweeps a 2D shape (XY) along a 3D path:
- Computes Path3D frames.
- For each
path[i], transforms eachshape[k]:p = tangent[i] * sz + normal[i] * sx + binormal[i] * sy(reinterprets shape Z as tangent offset). - Applies cumulative
rotationaroundtangent[i](Rodrigues) andscale. - Translates to
path[i]. - Builds ribbon rows from the transformed rings, with optional start/end caps via barycenter + zero-scale duplicate ring.
Path3D (path3d.ts)
Port of BJS's parallel-transport frame computation:
- First tangent:
normalize(curve[1] - curve[0])(or the first non-null diff for degenerate leading duplicates). - First normal: arbitrary perpendicular via
_normalVector(tangent, null)— picks the most stable axis cross. - Subsequent frames:
tangent[i] = normalize(curve[i+1] - curve[i]), thennormal[i] = normalize(cross(binormal[i-1], tangent[i])),binormal[i] = cross(tangent[i], normal[i]). This parallel-transports the frame along the curve without swing/roll discontinuities. - Last point copies the previous tangent.
distances[i]is the cumulative arc length, used for UV normalization.
GPU Upload Pattern
All generators follow the same GPU upload pattern:
// Per-attribute buffer creationconst buffer = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, mappedAtCreation: true, // (ground, torus, sphere) // OR: writeBuffer after creation (box)});new Float32Array(buffer.getMappedRange()).set(data);buffer.unmap();Index buffers use GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST with Uint32Array.
Box variant: Uses device.queue.writeBuffer() instead of mappedAtCreation.
Vertex Data Layout
All generators use separate buffers (not interleaved):
| Buffer | Stride | Format | Content |
|---|---|---|---|
Buffer Position | Stride 12B | Format float32x3 | Content xyz coords |
Buffer Normal | Stride 12B | Format float32x3 | Content xyz normal |
Buffer UV | Stride 8B | Format float32x2 | Content uv coords |
Buffer Index | Stride 4B | Format uint32 | Content indices |
Exception: Sphere and Box do not generate UV coordinates.
Pipeline Configuration
Mesh generators do not create pipelines. They produce raw vertex data consumed by material pipelines (e.g., standard-textured-material.ts). The expected pipeline vertex layout is:
| Slot | Stride | Location | Format | Buffer |
|---|---|---|---|---|
Slot 0 | Stride 12B | Location 0 | Format float32x3 | Buffer position |
Slot 1 | Stride 12B | Location 1 | Format float32x3 | Buffer normal |
Slot 2 | Stride 8B | Location 2 | Format float32x2 | Buffer uv |
Shader Logic
No shaders. Mesh generators are CPU-only geometry producers.
State Machine / Lifecycle
Ground Lifecycle
Option A: Flat ground createFlatGroundData(opts) → GroundData
Option B: Heightmap ground createGroundFromHeightMap(url, opts) → GroundData (async) ├─ createFlatGroundData(opts) ├─ Load image via HTMLImageElement ├─ Draw to canvas, extract ImageData └─ applyHeightmap(ground, imageData, ...)
Option C: Manual heightmap createFlatGroundData(opts) → GroundData applyHeightmap(ground, pixelData, ...)Other Shapes
createTorusData(opts) → TorusData → uploadTorusToGPU(device, data) → TorusGPUcreateSphereData(opts) → SphereMeshData → uploadSphereToGPU(device, data) → {posBuffer, normBuffer, idxBuffer, idxCount}createBoxData(size) → BoxData → uploadBoxToGPU(device, data) → BoxGPUAll are single-call, synchronous generators (except createGroundFromHeightMap which is async).
Babylon.js Equivalence Map
| Babylon Lite | Babylon.js |
|---|---|
Babylon Lite createFlatGroundData(opts) | Babylon.js MeshBuilder.CreateGround(name, opts, scene) |
Babylon Lite createGroundFromHeightMap(url, opts) | Babylon.js MeshBuilder.CreateGroundFromHeightMap(name, url, opts, scene) |
Babylon Lite applyHeightmap() | Babylon.js Internal: GroundMesh._applyDisplacementMap() |
Babylon Lite Luminance: r*0.3 + g*0.59 + b*0.11 | Babylon.js Same luminance formula in Babylon |
Babylon Lite createTorusData(opts) | Babylon.js MeshBuilder.CreateTorus(name, opts, scene) |
Babylon Lite Torus outer angle offset -π/2 | Babylon.js Babylon's torus starts at -π/2 rotation |
Babylon Lite Torus inner angle offset +π | Babylon.js Babylon's tube cross-section starts at +π |
Babylon Lite createSphereData(opts) | Babylon.js MeshBuilder.CreateSphere(name, opts, scene) |
Babylon Lite totalZ = 2 + segments | Babylon.js Babylon's sphere tessellation formula |
Babylon Lite totalY = 2 * totalZ | Babylon.js Babylon's sphere azimuthal step count |
Babylon Lite createBoxData(size) | Babylon.js MeshBuilder.CreateBox(name, { size }, scene) |
Babylon Lite Face order: +Z,-Z,+X,-X,+Y,-Y | Babylon.js Same face order in Babylon |
Babylon Lite createCylinderData(opts) | Babylon.js MeshBuilder.CreateCylinder(name, opts, scene) |
Babylon Lite createPlaneData(opts) | Babylon.js MeshBuilder.CreatePlane(name, opts, scene) |
Babylon Lite createDiscData(opts) | Babylon.js MeshBuilder.CreateDisc(name, opts, scene) |
Babylon Lite createPolyhedronData(opts) | Babylon.js MeshBuilder.CreatePolyhedron(name, opts, scene) |
Babylon Lite createRibbonData(opts) | Babylon.js MeshBuilder.CreateRibbon(name, opts, scene) |
Babylon Lite createTubeData(opts) | Babylon.js MeshBuilder.CreateTube(name, opts, scene) |
Babylon Lite createExtrudeShapeData(opts) | Babylon.js MeshBuilder.ExtrudeShape(name, opts, scene) |
Babylon Lite CAP_NONE/CAP_START/CAP_END/CAP_ALL | Babylon.js Mesh.NO_CAP/CAP_START/CAP_END/CAP_ALL |
Babylon Lite Separate pos/normal/uv buffers | Babylon.js Babylon uses VertexBuffer per kind |
Dependencies
- None (all generators are self-contained)
- WebGPU API types (GPUDevice, GPUBuffer)
- Browser APIs:
Image,HTMLCanvasElement,CanvasRenderingContext2D(ground heightmap only)
Test Specification
Ground
- Flat ground dimensions — With
width=10, height=10, subdivisions=4: 25 vertices, 96 indices. - Position range — Vertex X in
[-width/2, width/2], Z in[-height/2, height/2], Y = 0. - UV range — All UVs in [0, 1].
- Heightmap luminance — Pixel
(255, 0, 0)→ gradient =0.3. With minHeight=0, maxHeight=10: Y = 3.0. - Normal recomputation — After heightmap: all normals should be unit length, Y-dominant for gentle slopes.
- Winding — CCW front face (consistent with
frontFace: 'ccw'in pipeline).
Torus
- Vertex count — With
tessellation=16:(17)² = 289vertices,289 × 6 = 1734indices. - Symmetry — Torus should be symmetric about Y axis.
- Major radius — Vertex distance from Y-axis should be approximately
R ± r. - UV wrap — UVs should tile correctly with wrapping indices.
Sphere
- Default tessellation —
segments=32:35 × 69 = 2415vertices,34 × 68 × 6 = 13872indices. - Unit radius — With
diameter=1: all vertex positions should have magnitude ≈ 0.5. - Poles — Top pole at
(0, ry, 0), bottom pole at(0, -ry, 0). - Normal direction — Each normal should point radially outward from origin.
- Ellipsoid — With
diameterX=2, diameterY=1, diameterZ=1: positions scaled non-uniformly but normals remain on unit sphere.
Box
- Counts — Always 24 vertices, 36 indices regardless of size.
- Size=1 optimization — Returns pre-computed constant arrays directly.
- Size scaling —
size=2: all positions multiplied by 2, normals unchanged. - Face normals — Each face has 4 identical axis-aligned normals.
- No UV — Box does not generate UV coordinates.
File Manifest
| File | Role |
|---|---|
File src/mesh/create-ground.ts | Role Ground plane with heightmap: flat generation, displacement, normal recomputation, GPU upload |
File src/mesh/create-torus.ts | Role Torus: parametric ring mesh generation, GPU upload |
File src/mesh/create-sphere.ts | Role UV sphere: parametric sphere generation, GPU upload |
File src/mesh/create-box.ts | Role Box: static 6-face geometry from constant arrays, GPU upload |
File src/mesh/create-cylinder.ts | Role Cylinder / cone / prism: height, diameterTop/Bottom, tessellation, subdivisions, CAP_ALL |
File src/mesh/create-plane.ts | Role Plane: quad in XY with -Z normal (size or width/height) |
File src/mesh/create-disc.ts | Role Disc / pie / ring: fan disc with configurable arc, -Z normal |
File src/mesh/create-polyhedron.ts | Role 15 BJS polyhedron presets (tetra/cube/octa/dodeca/icosa/etc), flat & smooth normals |
File src/mesh/polyhedron-data.ts | Role Vertex / face tables for the 15 polyhedra (auto-generated from BJS polyhedronData.js) |
File src/mesh/create-ribbon.ts | Role Parametric ribbon primitive: pathArray, closePath, closeArray, offset, sideOrientation |
File src/mesh/create-tube.ts | Role Tube: closed-circle ribbon along a 3D path; CAP_NONE/START/END/ALL; Rodrigues rotation |
File src/mesh/create-extrude.ts | Role ExtrudeShape: sweep a 2D shape along a 3D path using Frenet frames (tangent/normal/binormal) |
File src/mesh/path3d.ts | Role Path3D port: tangents, normals, binormals, cumulative distances (parallel-transport frames) |
File src/mesh/compute-normals.ts | Role BJS-equivalent normal accumulator (Float64Array) for shapes that compute normals post-build |