atp-geometry
v1.2.0
Published
General-purpose 3D spatial math, polygon mesh data structures, and streaming Wavefront OBJ reader/writer
Maintainers
Readme
atp-geometry
High-performance, zero-dependency 3D spatial math, polygon mesh data structures, and streaming Wavefront OBJ reader/writer for Node.js, Electron, and modern web environments.
Table of Contents
- Architectural Overview
- Installation
- Module Exports
- Core API Reference
- Wavefront OBJ Syntax Support
- Recipes & Integration Examples
- Performance & Memory Optimization
- Developer Workflow & Testing
Architectural Overview
atp-geometry is a vendor-neutral 3D geometry engine. It is strictly decoupled from rigging engines (such as Poser) and designed from the ground up for high-performance desktop applications (Electron) and graphics pipelines:
- Zero Runtime Dependencies: Pure TypeScript compiling to modern ES2022 JavaScript. Uses only native Node.js streams (
node:fs,node:stream) for disk operations. - Allocation Efficiency: Mathematical primitives (
Vector3,Vector2,Rotation3) provide both immutable return-new and mutable in-place operations (addInPlace,multiplyScalarInPlace) to avoid garbage collector pressure in tight generation loops. - True Streaming Serialization: Unlike traditional libraries that buffer gigabyte-sized OBJ strings in memory,
WavefrontWriterwrites directly to a Node.jsWritableStream, allowing massive multi-million polygon meshes to export with low memory usage. - Group-Relative Indexing: First-class support for mapping vertices between global mesh index space and local group index space (
getGroupVertexIndex,getTrueVertexIndex), which is critical for skeletal deformation, rigging actors, morph targets, and blendshapes.
Installation
# npm
npm install atp-geometry
# yarn
yarn add atp-geometryModule Exports
atp-geometry provides both a root entry point and dedicated subpath imports:
// Root entry point
import { Vector3, Vector2, Rotation3, WavefrontMesh, WavefrontWriter, WavefrontMtlWriter, WavefrontReader } from 'atp-geometry';
// Subpath imports
import { Vector3, Vector2, Rotation3 } from 'atp-geometry/math';
import { WavefrontMesh, WavefrontWriter, WavefrontMtlWriter, WavefrontReader } from 'atp-geometry/wavefront';
import { Mesh, Vertex, TextureCoordinate } from 'atp-geometry/mesh';Core API Reference
Vector3
Represents a 3D Cartesian coordinate or direction vector (x, y, z).
import { Vector3 } from 'atp-geometry';
const v = new Vector3(1.0, 2.0, 3.0);Properties
x: number— X coordinate (default0).y: number— Y coordinate (default0).z: number— Z coordinate (default0).
Methods
| Method | Return | Description |
| :--- | :--- | :--- |
| set(x, y, z) | this | Mutates vector coordinates in place. |
| clone() | Vector3 | Returns an exact duplicate of this vector. |
| copyFrom(other) | this | Copies coordinates from other into this vector. |
| add(other) | Vector3 | Returns a new vector: this + other. |
| addInPlace(other) | this | In-place addition without allocation. |
| subtract(other) | Vector3 | Returns a new vector: this - other. |
| subtractInPlace(other) | this | In-place subtraction without allocation. |
| multiplyScalar(scalar) | Vector3 | Multiplies coordinates by scalar. Alias: scale(scalar). |
| multiplyScalarInPlace(s) | this | In-place scalar multiplication. |
| divideScalar(scalar) | Vector3 | Divides coordinates by scalar. |
| dot(other) | number | Calculates dot product: x1*x2 + y1*y2 + z1*z2. |
| cross(other) | Vector3 | Calculates vector cross product: this × other. |
| length() | number | Euclidean length: √(x² + y² + z²). |
| lengthSq() | number | Squared length: x² + y² + z² (fast comparison). |
| distanceTo(other) | number | Euclidean distance between this point and other. |
| distanceToSq(other) | number | Squared distance between this point and other. |
| normalize() | Vector3 | Returns a unit vector with length 1.0. |
| normalizeInPlace() | this | Normalizes this vector in place. |
| equals(other, epsilon?) | boolean | Approximate equality check with customizable tolerance (default 1e-6). |
| toArray() | [number, number, number] | Exports coordinates as a 3-element tuple. |
| Vector3.fromArray(arr) | Vector3 | Static factory creating a Vector3 from an array or tuple. |
Vector2
Represents a 2D coordinate (u, v) primarily used for UV texture coordinates.
import { Vector2 } from 'atp-geometry';
const uv = new Vector2(0.5, 1.0);Properties
u: number(alias:x) — Horizontal coordinate.v: number(alias:y) — Vertical coordinate.
Methods
set(u: number, v: number): this— Mutates coordinates in place.clone(): Vector2— Returns a copy.copyFrom(other: Vector2): this— Copies values fromother.add(other: Vector2): Vector2— Returnsthis + other.scale(factor: number): Vector2— Scales coordinates byfactor.equals(other: Vector2, epsilon?: number): boolean— Approximate equality check.toArray(): [number, number]— Exports[u, v].
Rotation3
Represents 3D Euler angles in degrees (xrot, yrot, zrot).
import { Rotation3 } from 'atp-geometry';
const rot = new Rotation3(45, 90, 0);Properties
xrot: number— Pitch / rotation around X axis (degrees).yrot: number— Yaw / rotation around Y axis (degrees).zrot: number— Roll / rotation around Z axis (degrees).
Methods
normalizeAngles(): this— Normalizes all angles to[-180, 180)degrees.add(other: Rotation3): Rotation3— Combines Euler angle offsets.toArray(): [number, number, number]— Exports[xrot, yrot, zrot].Rotation3.fromArray(arr): Rotation3— Factory from array.
WavefrontMesh
In-memory polygon mesh data structure holding 3D geometric vertices, UV texture coordinates, surface normals, and multi-group polygon faces.
import { WavefrontMesh } from 'atp-geometry';
const mesh = new WavefrontMesh();Data Storage
vertices: Vector3[]— List of 3D spatial points.uvs: Vector2[]— List of 2D texture coordinates.normals: Vector3[]— List of surface normal vectors.faces: MeshFace[]— List of polygon faces.
Face & Vertex Types
export interface FaceVertex {
vIndex: number; // 0-based index into mesh.vertices
tIndex?: number; // Optional 0-based index into mesh.uvs
nIndex?: number; // Optional 0-based index into mesh.normals
}
export interface MeshFace {
vertices: FaceVertex[]; // Polygon vertices (3 = triangle, 4 = quad, n = ngone)
group: string; // Named group (e.g. 'hairRoot', 'head', 'arm')
material: string; // Material name (e.g. 'HairMaterial', 'Skin')
}Methods
Mesh Construction
addVertex(x: number, y: number, z: number): numberaddVertex(v: Vector3): number- Adds a 3D vertex to
mesh.vertices. Returns the assigned 0-based vertex index.
- Adds a 3D vertex to
addUV(u: number, v: number): numberaddUV(uv: Vector2): number- Adds a UV coordinate to
mesh.uvs. Returns the assigned 0-based UV index.
- Adds a UV coordinate to
addNormal(x: number, y: number, z: number): numberaddNormal(n: Vector3): number- Adds a surface normal to
mesh.normals. Returns the assigned 0-based normal index.
- Adds a surface normal to
addFace(vertices: FaceVertex[], group?: string, material?: string): number- Adds a polygon face to the mesh assigned to
group(default'default') andmaterial(default'default'). Returns the assigned face index.
- Adds a polygon face to the mesh assigned to
Group & Material Queries
getGroups(): string[]— Returns all unique named groups (g) present in the mesh faces.getMaterials(): string[]— Returns all unique material names (usemtl) assigned to faces.getFacesByGroup(groupName: string): MeshFace[]— Filters faces belonging to a specific group.
Group-Relative Indexing
getGroupVertices(groupName: string): { globalIndex: number; vertex: Vector3 }[]- Returns an ordered list of unique vertices referenced by the specified group, including both their global index in
mesh.verticesand theVector3instance.
- Returns an ordered list of unique vertices referenced by the specified group, including both their global index in
getGroupVertexIndex(groupName: string, globalIndex: number): number- Maps a global vertex index to its 0-based local index within
groupName. Returns-1if the vertex is not in the group.
- Maps a global vertex index to its 0-based local index within
getTrueVertexIndex(groupName: string, localGroupIndex: number): number- Maps a 0-based local group vertex index back to the global mesh vertex index. Returns
-1if out of bounds.
- Maps a 0-based local group vertex index back to the global mesh vertex index. Returns
Normal Generation
calculateNormals(mode: 'smooth' | 'faceted' = 'smooth'): void- Automatically computes surface normals for all polygon faces:
'smooth': Computes area-weighted polygon normals, accumulates them per vertex, normalizes the result, and setsnIndexon every face vertex.'faceted': Computes flat per-face cross products $(v_1 - v_0) \times (v_2 - v_0)$ and assigns one normal per face.
- Automatically computes surface normals for all polygon faces:
Cloning & Memory Reset
clone(): WavefrontMesh— Deep clones all vertices, UVs, normals, and face structures.clear(): void— Resets all internal arrays.
WavefrontWriter
Streaming Wavefront .obj file serializer.
import { WavefrontWriter } from 'atp-geometry';Formatting Options
export interface WavefrontWriteOptions {
headerComment?: string; // Header comment at line 1 (default: 'Exported Wavefront OBJ')
precision?: number; // Decimal places for floats (default: 6)
mtllib?: string; // Optional .mtl file reference (e.g. 'hair.mtl')
}Static Methods
1. exportToFile(mesh, filePath, options?): Promise<void>
Streams the OBJ content directly to disk using node:fs.createWriteStream. Automatically formats groups (g <group>), materials (usemtl <material>), and 1-based face indices (f v/vt/vn).
await WavefrontWriter.exportToFile(mesh, 'C:/assets/model.obj', {
headerComment: 'Generated by ATP',
precision: 6,
mtllib: 'materials.mtl'
});2. exportToStream(mesh, stream, options?): Promise<void>
Pipes serialized OBJ output to any NodeJS.WritableStream (e.g. HTTP response, zip stream, IPC pipe):
await WavefrontWriter.exportToStream(mesh, myWritableStream);3. writeToString(mesh, options?): string
Generates an in-memory string representation (recommended for small meshes or unit tests).
const objContent = WavefrontWriter.writeToString(mesh);WavefrontMtlWriter
Generates standard Wavefront MTL material library files pairing with WavefrontMesh and WavefrontWriter.
import { WavefrontMtlWriter, type WavefrontMaterialDef } from 'atp-geometry';Material Definition Interface
export interface WavefrontMaterialDef {
name: string;
kdColor?: [number, number, number]; // Kd r g b (default: [0.72, 0.48, 0.32])
kaColor?: [number, number, number]; // Ka r g b (default: [0.2, 0.2, 0.2])
ksColor?: [number, number, number]; // Ks r g b (default: [0.45, 0.45, 0.45])
nsExponent?: number; // Ns (default: 45)
illum?: number; // illum (default: 2)
dissolve?: number; // d (default: 1.0)
diffuseMap?: string; // map_Kd
transparencyMap?: string; // map_d
bumpMap?: string; // map_Bump
specularMap?: string; // map_Ks
}Static Methods
1. writeToString(materials, options?): string
Serializes a list of materials to a formatted MTL string with CRLF line breaks.
const mtlContent = WavefrontMtlWriter.writeToString([
{
name: 'Hair_Material',
diffuseMap: ':Runtime:Textures:MyHair_diffuse.png',
transparencyMap: ':Runtime:Textures:MyHair_trans.png'
}
]);2. exportToFile(materials, filePath, options?): Promise<void>
Asynchronously writes material definitions to a file on disk using Node.js streaming.
await WavefrontMtlWriter.exportToFile(materials, 'C:/assets/materials.mtl', {
headerComment: 'Hair Materials Library',
precision: 4
});WavefrontReader
Robust, high-speed tokenizer for loading arbitrary Wavefront OBJ files into a WavefrontMesh.
import { WavefrontReader } from 'atp-geometry';Static Methods
1. parseString(content: string): WavefrontMesh
Parses raw OBJ text into a populated WavefrontMesh:
const mesh = WavefrontReader.parseString(objText);
console.log(`Loaded ${mesh.vertices.length} vertices, ${mesh.faces.length} faces.`);2. loadFromFile(filePath: string): Promise<WavefrontMesh>
Asynchronously reads and parses an OBJ file from disk:
const mesh = await WavefrontReader.loadFromFile('C:/models/character.obj');Parsing Features
- Directives:
v(vertices),vt(texture coordinates),vn(normals),f(faces),g(groups),o(objects),usemtl(materials),mtllib(material library),#(comments). - Indexing styles:
f 1 2 3(vertex only)f 1/1 2/2 3/3(vertex + texture)f 1//1 2//2 3//3(vertex + normal)f 1/1/1 2/2/1 3/3/1(vertex + texture + normal)
- Negative relative indexing: correctly resolves relative offsets like
f -3 -2 -1.
Mesh Facade (atp-geometry/mesh)
For interoperability with existing ATC codebases, atp-geometry/mesh exports aliased types:
import { Mesh, Vertex, TextureCoordinate, type FaceVertex, type MeshFace } from 'atp-geometry/mesh';
const m: Mesh = new Mesh();
const v: Vertex = new Vertex(0, 1, 0);Wavefront OBJ Syntax Support
| Directive | Format | Description |
| :---: | :--- | :--- |
| v | v x y z | 3D geometric vertex coordinates |
| vt | vt u v | 2D texture coordinates |
| vn | vn x y z | 3D surface normal vector |
| f | f v/vt/vn ... | Polygon face indices (strictly 1-based upon export) |
| g | g groupName | Polygon group name (used to bind bones in rigging) |
| usemtl | usemtl matName | Material assignment for subsequent faces |
| mtllib | mtllib file.mtl | Material library declaration |
| # | # comment | Header comments and metadata |
Recipes & Integration Examples
1. Procedural Hair Ribbon Mesh Generation
Generate a multi-segmented hair strand mesh divided into three rigging groups (hairRoot, hairMid, hairTip) for Poser or game rigging:
import { WavefrontMesh, WavefrontWriter, Vector3 } from 'atp-geometry';
function generateHairRibbon(strandLength: number = 2.0, segments: number = 10, width: number = 0.04): WavefrontMesh {
const mesh = new WavefrontMesh();
const step = strandLength / segments;
for (let i = 0; i <= segments; i++) {
const y = 2.0 - i * step;
const zCurve = Math.sin((i / segments) * Math.PI * 0.5) * 0.15; // Natural drape curve
const currentWidth = width * (1.0 - (i / segments) * 0.7); // Taper toward tip
// Left vertex
mesh.addVertex(-currentWidth * 0.5, y, zCurve);
mesh.addUV(0.0, i / segments);
// Right vertex
mesh.addVertex(currentWidth * 0.5, y, zCurve);
mesh.addUV(1.0, i / segments);
}
// Create quad faces grouped by bone region
for (let i = 0; i < segments; i++) {
const v0 = i * 2;
const v1 = i * 2 + 1;
const v2 = (i + 1) * 2 + 1;
const v3 = (i + 1) * 2;
// Segment group assignment
let group = 'hairRoot';
if (i >= segments * 0.66) group = 'hairTip';
else if (i >= segments * 0.33) group = 'hairMid';
// Quad as two triangles
mesh.addFace(
[{ vIndex: v0, tIndex: v0 }, { vIndex: v1, tIndex: v1 }, { vIndex: v2, tIndex: v2 }],
group,
'HairStrandMat'
);
mesh.addFace(
[{ vIndex: v0, tIndex: v0 }, { vIndex: v2, tIndex: v2 }, { vIndex: v3, tIndex: v3 }],
group,
'HairStrandMat'
);
}
// Compute smooth surface normals
mesh.calculateNormals('smooth');
return mesh;
}2. Streaming File Export in Electron
In an Electron application main process (ipcMain handler):
import { ipcMain, dialog } from 'electron';
import { WavefrontMesh, WavefrontWriter } from 'atp-geometry';
ipcMain.handle('export-obj', async (event, meshData) => {
const { filePath } = await dialog.showSaveDialog({
filters: [{ name: 'Wavefront OBJ', extensions: ['obj'] }],
});
if (!filePath) return false;
const mesh = new WavefrontMesh();
// ... populate mesh ...
// Stream directly to the chosen file path
await WavefrontWriter.exportToFile(mesh, filePath, {
headerComment: 'Exported from Hair Studio 3D',
precision: 6,
});
return true;
});3. Converting to Three.js BufferGeometry
Easily bridge atp-geometry meshes into Three.js viewports:
import * as THREE from 'three';
import { WavefrontMesh } from 'atp-geometry';
export function toThreeBufferGeometry(mesh: WavefrontMesh): THREE.BufferGeometry {
const geom = new THREE.BufferGeometry();
// Position buffer
const posArray = new Float32Array(mesh.vertices.length * 3);
mesh.vertices.forEach((v, idx) => {
posArray[idx * 3] = v.x;
posArray[idx * 3 + 1] = v.y;
posArray[idx * 3 + 2] = v.z;
});
geom.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
// UV buffer
if (mesh.uvs.length > 0) {
const uvArray = new Float32Array(mesh.uvs.length * 2);
mesh.uvs.forEach((uv, idx) => {
uvArray[idx * 2] = uv.u;
uvArray[idx * 2 + 1] = uv.v;
});
geom.setAttribute('uv', new THREE.BufferAttribute(uvArray, 2));
}
// Index buffer from triangulated faces
const indices: number[] = [];
for (const face of mesh.faces) {
for (let i = 1; i < face.vertices.length - 1; i++) {
indices.push(face.vertices[0].vIndex, face.vertices[i].vIndex, face.vertices[i + 1].vIndex);
}
}
geom.setIndex(indices);
geom.computeVertexNormals();
return geom;
}Performance & Memory Optimization
- Avoid In-Memory Buffering for Large Meshes:
Always prefer
WavefrontWriter.exportToFile()orWavefrontWriter.exportToStream()overWavefrontWriter.writeToString()when exporting meshes with > 50,000 polygons. - Batch Face Grouping:
WavefrontWriterautomatically batches faces bygroupthen bymaterial. This ensures minimal state switching (gandusemtlcommands) when the resulting OBJ is ingested by 3D software (Blender, Poser, Maya, Three.js). - In-Place Math Operations:
In procedural loops where vectors are updated iteratively (such as spline evaluation or physics relaxation), use
addInPlace()andmultiplyScalarInPlace()to eliminate temporary object allocations.
Developer Workflow & Testing
This package is managed inside the ATP monorepo.
# Run unit tests
yarn test
# Watch mode for unit tests
yarn test:watch
# Typecheck and build to dist/
yarn buildLicense
MIT © Darkwynd / ATC Library Authors
