@glyphcss/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1181 @@
1
+ declare const DEFAULT_PROJECTION: "cubic";
2
+ /**
3
+ * Mesh post-processing intent.
4
+ * - "lossless": preserve the authored surface while applying exact
5
+ * reductions such as interior culling and coplanar merge.
6
+ * - "lossy": allow bounded geometric approximation when it reduces the
7
+ * rendered polygon/DOM count.
8
+ */
9
+ type MeshResolution = "lossless" | "lossy";
10
+ /**
11
+ * 3D point/vector, stored as a `[x, y, z]` tuple. Tuple (rather than
12
+ * `{x, y, z}`) for compact JSON: meshes serialize to thousands of vertices
13
+ * and the difference adds up. Destructure with `const [x, y, z] = v` when
14
+ * you need named axes.
15
+ *
16
+ * Polycss world space convention: +X right, +Y forward, +Z up.
17
+ */
18
+ type Vec3 = [number, number, number];
19
+ /**
20
+ * 2D point/vector — `[u, v]`. Used for texture-atlas UV coordinates on
21
+ * polygons. Convention follows OBJ: u is horizontal (0=left, 1=right),
22
+ * v is vertical (0=bottom, 1=top). Renderers flip v when binding to raster
23
+ * image space whose Y-axis points down.
24
+ */
25
+ type Vec2 = [number, number];
26
+ interface TextureTriangle {
27
+ vertices: [Vec3, Vec3, Vec3];
28
+ uvs: [Vec2, Vec2, Vec2];
29
+ /** Hex color string (`#rrggbb`) propagated from source model material. */
30
+ color?: string;
31
+ }
32
+ /**
33
+ * Directional light — simulates a single distant source (sun, key light).
34
+ * Contributes Lambert shading scaled by `intensity`. `direction` is in
35
+ * scene-local coords and does not need to be pre-normalized.
36
+ * Mirrors three.js's `DirectionalLight`.
37
+ */
38
+ interface GlyphcssDirectionalLight {
39
+ /** Direction the light shines TOWARD (typical convention). */
40
+ direction: Vec3;
41
+ /** Light tint, hex string. White by default. */
42
+ color?: string;
43
+ /** Scalar multiplier on the directional contribution. Default 1. */
44
+ intensity?: number;
45
+ }
46
+ /**
47
+ * Ambient light — uniform fill that adds to every polygon regardless of
48
+ * orientation. Mirrors three.js's `AmbientLight`. Decoupled from the
49
+ * directional contribution: the two add independently rather than
50
+ * splitting a fixed energy budget.
51
+ */
52
+ interface GlyphcssAmbientLight {
53
+ /** Tint, hex string. White by default. */
54
+ color?: string;
55
+ /** Scalar multiplier on the ambient contribution. Default 0.4. */
56
+ intensity?: number;
57
+ }
58
+ /**
59
+ * Material — paint configuration shareable across many polygons.
60
+ *
61
+ * In CSS terms, a material bundles the `background-image` source plus paint
62
+ * config. When a polygon references a material AND its UVs form an
63
+ * axis-aligned rectangle, polycss renders the polygon as an <i> with
64
+ * `background-image: url(material.texture)` directly — no per-polygon canvas
65
+ * rasterization, browser-cached texture, mounting / unmounting one polygon
66
+ * does not affect any other.
67
+ *
68
+ * Three.js parallel: combines THREE.Texture + a basic Material in one. CSS
69
+ * has no shader/sampler concerns, so the texture/material split from
70
+ * Three.js doesn't pay rent here.
71
+ */
72
+ interface PolyMaterial {
73
+ /** Image source. Anything `background-image: url(...)` can use. */
74
+ texture: string;
75
+ /** Optional unique key (used by polycss to dedupe / cache). Caller can
76
+ * pass a stable string; if omitted, the material's identity is its object
77
+ * reference. */
78
+ key?: string;
79
+ }
80
+ /**
81
+ * The single polygon type for polycss. N coplanar vertices in 3D space,
82
+ * CCW winding from outside. No bbox field, no shape discriminator, no
83
+ * input/output distinction — one type, used by parsers, by the merge
84
+ * pass, and by the renderer.
85
+ */
86
+ interface Polygon {
87
+ /** N coplanar vertices in 3D space, CCW winding from outside. */
88
+ vertices: Vec3[];
89
+ /**
90
+ * Solid base color. Falls back to "#cccccc" when neither color nor
91
+ * texture is set.
92
+ */
93
+ color?: string;
94
+ /**
95
+ * Texture URL. When set with `uvs`, UV-mapped via affine; without
96
+ * `uvs`, single-tile fill. If the load fails, renderer falls back to
97
+ * `color` (or default gray).
98
+ */
99
+ texture?: string;
100
+ /**
101
+ * Shared material. When set, `material.texture` takes precedence over the
102
+ * inline `texture` field. If the polygon's UVs form an axis-aligned
103
+ * rectangle, polycss uses the direct CSS background-image path (no per-
104
+ * polygon canvas rasterization). Falls back to the atlas path otherwise.
105
+ */
106
+ material?: PolyMaterial;
107
+ /**
108
+ * Per-vertex UV coords (0..1, OBJ convention with v=0 at bottom).
109
+ * Length MUST equal vertices.length when set; mismatched UVs are
110
+ * stripped by `normalizePolygons`.
111
+ */
112
+ uvs?: Vec2[];
113
+ /**
114
+ * Renderer-internal source triangles for UV textures. Merge passes use this
115
+ * to reduce DOM planes while preserving per-triangle texture mapping in the
116
+ * generated atlas.
117
+ * @internal
118
+ */
119
+ textureTriangles?: TextureTriangle[];
120
+ /**
121
+ * User-controlled metadata. Reflected to DOM as `data-*` attributes via
122
+ * stringification by the framework wrappers. Only string|number|boolean
123
+ * values are kept; other shapes are dropped by `normalizePolygons`.
124
+ */
125
+ data?: Record<string, string | number | boolean>;
126
+ }
127
+ /** Rendering mode for `rasterize`. See README for tradeoffs. */
128
+ type RenderMode = "wireframe" | "solid" | "voxel";
129
+ /**
130
+ * Character ramp used by `solid` mode to map shaded intensity to a glyph.
131
+ * Index 0 = darkest (transparent / unset), last index = brightest.
132
+ */
133
+ type CharRamp = string[];
134
+ /**
135
+ * Wireframe edge weight. Maps to glyph density in the rasterizer:
136
+ * 1 — thin (spokes, inner cage)
137
+ * 2 — normal (main cage edges)
138
+ * 3 — core (focal accents)
139
+ */
140
+ type EdgeWeight = 1 | 2 | 3;
141
+ /** A single drawable edge in wireframe mode. */
142
+ interface WireframeEdge {
143
+ from: Vec3;
144
+ to: Vec3;
145
+ weight?: EdgeWeight;
146
+ /** Hex color string (`#rrggbb`) propagated from the adjacent triangle's material. */
147
+ color?: string;
148
+ }
149
+ /** Grid dimensions in character cells. */
150
+ interface GridSize {
151
+ cols: number;
152
+ rows: number;
153
+ /** Character cell aspect ratio (height / width). Typically ~2.0 for monospace. */
154
+ cellAspect: number;
155
+ }
156
+ /**
157
+ * A 3D anchor that should produce a 2D hitbox in the consumer's DOM.
158
+ * Consumers absolute-position a `<div>` at the projected cell, sized by
159
+ * `size` (in character cells). Pure-math: this module just projects.
160
+ */
161
+ interface Hotspot {
162
+ id: string;
163
+ at: Vec3;
164
+ /** Hitbox size in cells. Default `[1, 1]`. */
165
+ size?: [number, number];
166
+ }
167
+ /** Result of projecting a single hotspot through the camera. */
168
+ interface HotspotCell {
169
+ id: string;
170
+ col: number;
171
+ row: number;
172
+ /** Camera-space Z. Useful for `z-index` / occlusion checks. */
173
+ depth: number;
174
+ /** False if behind the camera or off-grid. */
175
+ visible: boolean;
176
+ }
177
+
178
+ /**
179
+ * normalizePolygons — validates a polygon list, drops degenerate inputs,
180
+ * triangulates non-coplanar N-gons, strips bad UVs, sanitizes data, and
181
+ * returns the cleaned polygons + a list of human-readable warnings.
182
+ *
183
+ * Validation rules are encoded here and covered by the normalization tests.
184
+ *
185
+ * Pure: no DOM, no I/O, deterministic. Bbox is NOT computed here — that's
186
+ * derived on demand by `buildSceneContext` / consumers.
187
+ */
188
+
189
+ interface NormalizeResult {
190
+ polygons: Polygon[];
191
+ warnings: string[];
192
+ }
193
+ declare function normalizePolygons(input: Polygon[]): NormalizeResult;
194
+
195
+ /**
196
+ * Scene context — the top-level entry point that takes a polygon mesh
197
+ * (already normalized) and returns the data the framework wrappers need
198
+ * to render.
199
+ *
200
+ * No cube grid, no per-Z layer bucketing, no wall mask, no neighbor-based
201
+ * occlusion. Just a polygon list and a scene bbox.
202
+ */
203
+
204
+ interface SceneBbox {
205
+ /** Minimum corner of the axis-aligned bounding box (inclusive). */
206
+ min: Vec3;
207
+ /** Maximum corner of the axis-aligned bounding box (inclusive). */
208
+ max: Vec3;
209
+ }
210
+ interface SceneContext {
211
+ /** Validated polygon list — the renderer iterates this. */
212
+ polygons: Polygon[];
213
+ /** Polygon-mesh bbox in world space. Used to size the scene container. */
214
+ sceneBbox: SceneBbox;
215
+ /** Warnings raised during normalization (already-applied fixes). */
216
+ warnings: string[];
217
+ }
218
+ interface SceneContextBuildArgs {
219
+ /**
220
+ * Polygon list. Pass parser output directly — `buildSceneContext` runs
221
+ * `normalizePolygons` for you.
222
+ */
223
+ polygons: Polygon[];
224
+ /**
225
+ * If true, skip the normalize pass (caller has already validated). Useful
226
+ * when chaining `mergePolygons` after a manual `normalizePolygons` call.
227
+ */
228
+ skipNormalize?: boolean;
229
+ }
230
+ interface SceneContextBuildResult {
231
+ context: SceneContext;
232
+ /**
233
+ * Mesh-bbox dimensions. Convenience copy of `context.sceneBbox` plus a
234
+ * `size` field (max - min) for callers that want a single number per axis.
235
+ */
236
+ dimensions: {
237
+ sceneBbox: SceneBbox;
238
+ size: Vec3;
239
+ };
240
+ /** Warnings raised during normalization. Mirrors `context.warnings`. */
241
+ warnings: string[];
242
+ }
243
+ /**
244
+ * Compute the axis-aligned bounding box across every vertex of every polygon.
245
+ * Returns a zero-extent bbox at origin for empty input — callers that care
246
+ * about that case should check `polygons.length` first.
247
+ */
248
+ declare function computeSceneBbox(polygons: Polygon[]): SceneBbox;
249
+ declare function buildSceneContext(args: SceneContextBuildArgs): SceneContextBuildResult;
250
+
251
+ /**
252
+ * Polygon geometry helpers — pure math operating on Polygon vertices.
253
+ *
254
+ * After cube removal in Phase 2, this module carries small polygon-level
255
+ * helpers for downstream consumers (lighting, debug metrics, etc.). The
256
+ * cube / ramp / wedge / spike face emitters lived here in voxcss; they're gone.
257
+ */
258
+
259
+ interface PolygonFace {
260
+ /** Vertices in CCW-from-outside order. Same as Polygon.vertices. */
261
+ v: Vec3[];
262
+ /** Original polygon's color, if any (for lighting helpers). */
263
+ color?: string;
264
+ }
265
+ /**
266
+ * Surface a polygon as a single face. The returned array always has length 1;
267
+ * the indirection exists so callers that historically iterated faces (e.g.
268
+ * the manifold check, the canvas validator) can keep their loop shape.
269
+ *
270
+ * Returns an empty array for degenerate polygons (< 3 vertices).
271
+ */
272
+ declare function polygonFaces(p: Polygon): PolygonFace[];
273
+
274
+ /**
275
+ * Apply CSS-style chained `rotateX(rx) rotateY(ry) rotateZ(rz)` rotation
276
+ * to a 3D vector. Matches the matrix composition used by polycss mesh
277
+ * wrapper transforms (see `buildTransform` in each PolyMesh implementation).
278
+ *
279
+ * CSS composes `transform: rotateX(rx) rotateY(ry) rotateZ(rz)` as the
280
+ * matrix `M = Rx · Ry · Rz`, applied to a point as `M · p` — so Rz acts
281
+ * first on the point, then Ry, then Rx. Compound rotations only commute
282
+ * when axes coincide; getting the order wrong silently corrupts results
283
+ * for any two-axis combination.
284
+ *
285
+ * Angles in degrees.
286
+ */
287
+ declare function rotateVec3(v: Vec3, rxDeg: number, ryDeg: number, rzDeg: number): Vec3;
288
+ /**
289
+ * Inverse of `rotateVec3` for the same rotation tuple — transforms a
290
+ * world-space vector into the mesh's local frame. Used by the baked
291
+ * atlas pipeline to inverse-rotate the directional light so the
292
+ * pre-multiplied Lambert shading stays correct after the mesh rotates,
293
+ * and by the dynamic-mode CSS-var override for the same reason.
294
+ *
295
+ * The inverse of `M = Rx · Ry · Rz` is `M⁻¹ = Rz⁻¹ · Ry⁻¹ · Rx⁻¹`, so
296
+ * Rx⁻¹ acts first on the vector, then Ry⁻¹, then Rz⁻¹.
297
+ *
298
+ * `rot` is `[rxDeg, ryDeg, rzDeg]` matching the mesh's CSS rotation prop.
299
+ */
300
+ declare function inverseRotateVec3(v: Vec3, rot: Vec3): Vec3;
301
+
302
+ /**
303
+ * Minimal quaternion helpers for composing rotations.
304
+ *
305
+ * Why we need quaternions: the public PolyMesh API exposes rotation as a
306
+ * Euler triple `[rx, ry, rz]` in degrees (drives CSS `rotateX rotateY
307
+ * rotateZ`, applied right-to-left). Euler triples don't compose by
308
+ * component addition — rotating Y after X must happen around the mesh's
309
+ * NEW local-Y axis, not world-Y. The transform-controls ring drag handler
310
+ * uses these helpers to compose around the mesh's local axis correctly:
311
+ *
312
+ * q_start = quatFromEulerXYZ(currentRotationDeg)
313
+ * q_delta = quatFromAxisAngle(localAxis, deltaRadians)
314
+ * q_new = quatMultiply(q_start, q_delta) // RIGHT-multiply = local frame
315
+ * next = eulerXYZFromQuat(q_new)
316
+ *
317
+ * Convention: "XYZ" Euler means the composed rotation matrix is
318
+ * `Rx(rx) · Ry(ry) · Rz(rz)`, which matches CSS `rotateX rotateY rotateZ`
319
+ * (right-to-left application to a point ⇒ Z first, then Y, then X).
320
+ *
321
+ * Quaternion format: `[w, x, y, z]` (real-first, like three.js's internal
322
+ * `_x/_y/_z/_w` reordered). Stored as plain tuples — no constructor or
323
+ * runtime allocations per drag.
324
+ */
325
+
326
+ /** Quaternion `[w, x, y, z]`, real component first. Unit-length is not
327
+ * enforced by the type — callers normalize when needed. */
328
+ type Quat = [number, number, number, number];
329
+ /** Identity quaternion. */
330
+ declare const QUAT_IDENTITY: Quat;
331
+ /** Hamilton product `q1 * q2`. Apply to a vector as `q v q⁻¹`. Right-
332
+ * multiplication composes the second rotation in the LOCAL frame of the
333
+ * first — that's the property the gizmo relies on for local-axis drag. */
334
+ declare function quatMultiply(q1: Quat, q2: Quat): Quat;
335
+ /** Quaternion from axis-angle. `axis` must be unit length (caller's
336
+ * responsibility — typically a CSS basis vector). `angleRad` in radians. */
337
+ declare function quatFromAxisAngle(axis: Vec3, angleRad: number): Quat;
338
+ /** Quaternion from Euler XYZ degrees — the order CSS `rotateX rotateY
339
+ * rotateZ` applies. Matches the composed matrix `Rx(rx)·Ry(ry)·Rz(rz)`. */
340
+ declare function quatFromEulerXYZ(eulerDeg: Vec3): Quat;
341
+ /** Euler XYZ degrees from a quaternion — inverse of `quatFromEulerXYZ`.
342
+ * Handles gimbal lock (|ry| → 90°) by collapsing rz onto rx. The output
343
+ * matches the convention used by CSS `rotateX rotateY rotateZ` so it can
344
+ * be written straight back into a PolyMesh rotation prop. */
345
+ declare function eulerXYZFromQuat(q: Quat): Vec3;
346
+
347
+ /**
348
+ * Base tile size in CSS pixels. One polycss world unit = BASE_TILE CSS
349
+ * pixels (pre-scale). Used to convert world-coordinate target values to
350
+ * CSS translations in the transform string.
351
+ */
352
+ declare const BASE_TILE = 50;
353
+ interface AutoRotateConfig {
354
+ axis?: "x" | "y";
355
+ speed?: number;
356
+ pauseOnInteraction?: boolean;
357
+ }
358
+ type AutoRotateOption = boolean | number | AutoRotateConfig;
359
+ /**
360
+ * World-coordinate camera state (Three.js-style).
361
+ *
362
+ * `target` is the world point that should appear at the viewport centre.
363
+ * Polycss world axes: [0]=X (rows/south), [1]=Y (cols/east), [2]=Z (up).
364
+ *
365
+ * `pan`, `tilt`, and `depthOffset` are gone. Translations now live inside
366
+ * `target` so they happen BEFORE rotations — enabling correct world-space
367
+ * pan at any tilt angle.
368
+ *
369
+ * `distance` is the camera's pull-back from the target in CSS pixels.
370
+ * Increasing distance moves the camera farther from the target along the
371
+ * view axis (dolly out) — analogous to three.js's spherical radius.
372
+ * Default 0 keeps the legacy behaviour unchanged.
373
+ */
374
+ interface CameraState {
375
+ target: Vec3;
376
+ rotX: number;
377
+ rotY: number;
378
+ zoom: number;
379
+ /** Camera pull-back from target in CSS pixels. Default 0. */
380
+ distance: number;
381
+ }
382
+ interface CameraStyleInput {
383
+ rows?: number;
384
+ cols?: number;
385
+ }
386
+ interface CameraHandle {
387
+ state: CameraState;
388
+ update(next: Partial<CameraState>): void;
389
+ getStyle(input?: CameraStyleInput): {
390
+ transform: string;
391
+ width: string;
392
+ height: string;
393
+ };
394
+ }
395
+ declare function normalizeInvertMultiplier(value: number | boolean | undefined): number | undefined;
396
+ declare const DEFAULT_CAMERA_STATE: CameraState;
397
+ declare function createIsometricCamera(initial?: Partial<CameraState>): CameraHandle;
398
+
399
+ interface ParsedColor {
400
+ rgb: [number, number, number];
401
+ alpha: number;
402
+ }
403
+ declare function parseHexColor(value: string): ParsedColor | null;
404
+ declare function parseRgbColor(value: string): ParsedColor | null;
405
+ /** Parse hex or rgb/rgba color strings. Pure — no DOM. */
406
+ declare function parsePureColor(input: string): ParsedColor | null;
407
+ declare function clampChannel(value: number): number;
408
+ declare function formatColor(color: ParsedColor): string;
409
+
410
+ declare function parseColor(input: string): ParsedColor | null;
411
+ /**
412
+ * Lighten/darken a color by a flat per-channel delta. Used by the framework
413
+ * wrappers for tinted-overlay debug renderers; per-polygon Lambert shading
414
+ * goes through `computeShapeLighting` instead.
415
+ */
416
+ declare function shadeColor(base: string, delta: number): string;
417
+ /**
418
+ * Per-polygon Lambert shading. Given a polygon's outward normal and the
419
+ * scene's lights, returns the shaded color as a CSS rgb string.
420
+ *
421
+ * Math (decoupled, three.js convention):
422
+ * tint = ambient.color · ambient.intensity
423
+ * + directional.color · directional.intensity · max(0, n · (−L))
424
+ * final = baseColor × tint
425
+ *
426
+ * Pass `directional` and/or `ambient` undefined to fall back to defaults
427
+ * (top-down white directional with intensity 1, white ambient with
428
+ * intensity 0.4) — useful for static SSR/validator renders.
429
+ */
430
+ declare function computeShapeLighting(normal: Vec3, baseColor: string, directional?: GlyphcssDirectionalLight, ambient?: GlyphcssAmbientLight): string;
431
+
432
+ /**
433
+ * Merge coplanar same-color adjacent triangles into N-vertex polygons.
434
+ *
435
+ * Each polygon is rendered as one atlas-backed DOM element — so a mesh whose
436
+ * triangles came from quads or pentagons collapses back into its original
437
+ * face count.
438
+ *
439
+ * - Geodesic spheres: ~half the triangles came from quad subdivisions
440
+ * - OBJ imports: many were quads/n-gons fan-triangulated by the importer
441
+ * - Hand-built dodecahedra: 36 triangles → 12 pentagons
442
+ *
443
+ * Algorithm:
444
+ * 1. For each input polygon, compute its plane (unit normal + signed
445
+ * distance from origin).
446
+ * 2. Build an undirected edge graph: every edge of every polygon indexes
447
+ * the polygons it belongs to.
448
+ * 3. Repeatedly walk shared edges and merge the two polygons sharing that
449
+ * edge if they pass the merge predicate (same color, near-coplanar,
450
+ * result is convex, edge is interior). Each merge replaces two
451
+ * polygons with one larger polygon and updates the edge index.
452
+ * 4. Iterate until no more merges fire — the fixed point grows triangles
453
+ * → quads → pentagons → … as far as the geometry allows.
454
+ *
455
+ * Polygons with < 3 vertices are passed through unchanged (the caller is
456
+ * expected to have run `normalizePolygons` first; this is a defensive copy).
457
+ */
458
+
459
+ declare function mergePolygons(input: Polygon[]): Polygon[];
460
+
461
+ /**
462
+ * dedupeOverlappingPolygons — drop polygons whose 3D footprint coincides
463
+ * with another polygon's, within an epsilon tolerance.
464
+ *
465
+ * Why this exists: modelers (and importers) often emit redundant geometry
466
+ * for the same visible surface — a doubled face on a wall, an inner shell
467
+ * coincident with an outer shell, or two N-gons that fan-triangulate the
468
+ * same region. Each duplicate is a real `<i>` element at render time:
469
+ * it costs DOM, Lambert math, atlas budget, AND it produces stacked
470
+ * shadow leaves that visibly multiply on the receiver (overlapping dark
471
+ * patches on the ground).
472
+ *
473
+ * This is a separate concern from `cullInteriorPolygons` (which removes
474
+ * polygons fully *enclosed* by other geometry, conservative against
475
+ * false positives) and from `mergePolygons` (which joins same-color
476
+ * coplanar polygons that share an edge). A polygon's exact twin
477
+ * doesn't share an edge with itself and isn't enclosed by anything —
478
+ * it slips through both passes.
479
+ *
480
+ * Algorithm:
481
+ * 1. Compute each polygon's plane (normal + signed offset along
482
+ * normal) and centroid.
483
+ * 2. Bucket polygons by quantized plane key (rounded normal direction
484
+ * with sign-folding so anti-parallel faces share a bucket, and
485
+ * rounded distance from origin along the unsigned normal axis).
486
+ * Polygons in different buckets cannot overlap.
487
+ * 3. Within each bucket, do an O(K²) pairwise check on at most K
488
+ * polygons. Two polygons overlap if their 2D projections onto
489
+ * the shared plane share a significant area fraction.
490
+ * 4. When a pair overlaps, drop one: prefer keeping the one whose
491
+ * normal points *away* from the mesh centroid (the "outward"
492
+ * face). For ties (truly identical orientation), keep the one
493
+ * with greater 2D area.
494
+ *
495
+ * Runs once at parse time in the same pipeline as mergePolygons. Zero
496
+ * cost at runtime — once it returns the polygon array is final and the
497
+ * dedup logic never executes again.
498
+ */
499
+
500
+ /** Tunable thresholds. Default values are conservative — only catch
501
+ * duplicates that are visually identical surfaces (exact twins,
502
+ * back-to-back winding flips, nested polys on the same plane).
503
+ * Looser values are appropriate for shadow-casting purposes, where
504
+ * any polygons whose projections land in the same place can share a
505
+ * shadow without affecting the rendered model. */
506
+ interface DedupeOverlappingPolygonsOptions {
507
+ /** Maximum 1 - |dot(n_a, n_b)| for normals to count as "parallel".
508
+ * Default 1e-3 (strict — must be near-identical orientation).
509
+ * Looser values (~5e-2 ≈ 18° off) treat near-parallel normals as
510
+ * duplicates, useful for shadow dedup where small orientation
511
+ * differences project to nearly the same shadow shape. */
512
+ normalTolerance?: number;
513
+ /** Maximum signed-distance difference between two polygons' plane
514
+ * offsets (along their shared normal) to count as coplanar.
515
+ * Default 0.05 (world units). Looser values treat distinct
516
+ * parallel shells (e.g. an inner cavity wall behind an outer
517
+ * wall) as shadow-duplicates. */
518
+ distanceTolerance?: number;
519
+ /** Minimum overlap fraction (max of A-in-B and B-in-A vertex
520
+ * containment ratios) for a pair to count as a duplicate.
521
+ * Default 0.7. Lower (~0.4) is liberal; higher (~0.9) is strict. */
522
+ overlapFraction?: number;
523
+ }
524
+ /** Identify polygons that are duplicates within tolerance. Returns the
525
+ * set of indices into the input array that should be dropped (the
526
+ * losers of duplicate pairs). The "winner" of a pair is the polygon
527
+ * whose normal faces away from the mesh centroid (outward), with
528
+ * larger area as a tiebreaker.
529
+ *
530
+ * Exposed for callers that want to act on the index set directly —
531
+ * e.g. shadow casting can use a looser tolerance to skip shadow leaves
532
+ * for redundant casters without removing them from the renderable
533
+ * polygon set. */
534
+ declare function findOverlappingPolygonDuplicates(input: Polygon[], options?: DedupeOverlappingPolygonsOptions): Set<number>;
535
+ declare function dedupeOverlappingPolygons(input: Polygon[], options?: DedupeOverlappingPolygonsOptions): Polygon[];
536
+
537
+ interface CoverPlanarPolygonsOptions {
538
+ /** Smallest connected coplanar group worth attempting. Default 4. */
539
+ minGroupPolygons?: number;
540
+ /** Maximum candidate 2D axes tested per group. Default 8. */
541
+ maxCandidateAxes?: number;
542
+ /** Plane normal/distance tolerance in scene units. Default 1e-3. */
543
+ planeEpsilon?: number;
544
+ }
545
+ /**
546
+ * Re-cover flat same-color mesh regions with generated convex polygons.
547
+ *
548
+ * `mergePolygons` preserves source topology: it can only combine existing
549
+ * neighboring faces. This pass is more aggressive for solid-color planar
550
+ * regions: it projects each connected coplanar patch into 2D, covers the
551
+ * patch from its outer boundary, then lets `mergePolygons` collapse the
552
+ * generated cover into large rects/quads where possible.
553
+ */
554
+ declare function coverPlanarPolygons(input: Polygon[], options?: CoverPlanarPolygonsOptions): Polygon[];
555
+
556
+ interface ApproximateMergeOptions {
557
+ maxAngleDeg?: number;
558
+ maxPlaneDisplacement?: number;
559
+ maxBoundaryDisplacement?: number;
560
+ isolatedPairs?: boolean;
561
+ }
562
+ interface OptimizeMeshPolygonsOptions {
563
+ /** Public quality/resolution intent. Defaults to "lossy". */
564
+ meshResolution?: MeshResolution;
565
+ /**
566
+ * Run the planar cover pass as an exact candidate for untextured coplanar
567
+ * regions. Defaults to true.
568
+ */
569
+ rectCover?: boolean | CoverPlanarPolygonsOptions;
570
+ /**
571
+ * Lossy approximate merge settings. Ignored for lossless resolution.
572
+ * When omitted, lossy evaluates isolated-pair and small plane-group
573
+ * strategies, then chooses the lowest render-cost result with a near-cost
574
+ * preference for candidates that reduce detected internal gaps.
575
+ */
576
+ approximateMerge?: boolean | ApproximateMergeOptions;
577
+ }
578
+ declare function optimizeMeshPolygons(polygons: Polygon[], options?: OptimizeMeshPolygonsOptions): Polygon[];
579
+
580
+ /**
581
+ * cullInteriorPolygons — remove polygons that are fully enclosed by other
582
+ * polygons of the same mesh and therefore never visible from any external
583
+ * camera direction.
584
+ *
585
+ * Algorithm: for each polygon p,
586
+ * 1. Sample K unit directions on the hemisphere above p's normal.
587
+ * 2. Cast a ray from a point just above p's centroid in each direction.
588
+ * 3. If at least one ray escapes without hitting any other polygon → p
589
+ * is potentially visible from some external camera → keep it.
590
+ * 4. If every ray hits another polygon → p is fully surrounded → cull it.
591
+ *
592
+ * Acceleration: flat-array SAH-built binary BVH with slab-test AABB traversal.
593
+ * All BVH data is stored in typed Float64Array / Int32Array for cache efficiency.
594
+ * Ray traversal visits only the subtrees whose AABBs the ray intersects.
595
+ *
596
+ * Runs once at parse time inside `loadMesh`, before `mergePolygons`. Zero
597
+ * runtime cost. Conservative by design — false negatives (failing to cull
598
+ * a truly hidden poly) are safe; false positives would be a visual bug.
599
+ */
600
+
601
+ interface CullInteriorOptions {
602
+ /** Hemisphere ray samples per polygon. Higher = fewer false positives, slower. Default 12. */
603
+ samples?: number;
604
+ }
605
+ declare function cullInteriorPolygons(polygons: Polygon[], options?: CullInteriorOptions): Polygon[];
606
+
607
+ declare const CAMERA_BACKFACE_CULL_EPS = 0.00001;
608
+ declare const VOXEL_CAMERA_CULL_AXIS_EPS = 0.001;
609
+ declare const VOXEL_CAMERA_CULL_NORMAL_LIMIT = 6;
610
+ interface CameraCullRotation {
611
+ rotX: number;
612
+ rotY: number;
613
+ meshRotation?: Vec3;
614
+ }
615
+ interface CameraCullNormalGroup {
616
+ key: string;
617
+ normal: Vec3;
618
+ }
619
+ declare function polygonCssSurfaceNormal(polygon: Polygon): Vec3 | null;
620
+ declare function cameraFacingDepth(normal: Vec3, rotation: CameraCullRotation): number;
621
+ declare function normalFacesCamera(normal: Vec3, rotation: CameraCullRotation, depthThreshold?: number): boolean;
622
+ declare function polygonFacesCamera(polygon: Polygon, rotation: CameraCullRotation, depthThreshold?: number): boolean;
623
+ declare function cameraCullNormalKey(normal: Vec3): string;
624
+ declare function cameraCullNormalGroups(normals: Iterable<Vec3 | null | undefined>): CameraCullNormalGroup[];
625
+ declare function cameraCullNormalGroupsFromPolygons(polygons: readonly Polygon[]): CameraCullNormalGroup[];
626
+ declare function isAxisAlignedSurfaceNormal(normal: Vec3, axisEpsilon?: number): boolean;
627
+ declare function isVoxelCameraCullableNormalGroups(groups: readonly CameraCullNormalGroup[]): boolean;
628
+ declare function cameraCullVisibleSignature(groups: readonly CameraCullNormalGroup[], rotation: CameraCullRotation, depthThreshold?: number): string;
629
+
630
+ /**
631
+ * Geometry for the three.js-style debug axes gizmo: three thin colored
632
+ * cuboids stretching along world-X, world-Y and world-Z. Mirrors the
633
+ * convention `red=X, green=Y, blue=Z`.
634
+ *
635
+ * Returned polygons are in the standard polycss world-space convention
636
+ * (`+X right, +Y forward, +Z up`). Wrap with the framework's PolyMesh /
637
+ * PolyScene equivalent to render.
638
+ */
639
+
640
+ interface AxesHelperOptions {
641
+ /** Length of each axis bar in world units. */
642
+ size?: number;
643
+ /** Bar cross-section width as a fraction of `size`. */
644
+ thickness?: number;
645
+ /** When true, also draws bars in the −X / −Y / −Z direction. */
646
+ negative?: boolean;
647
+ /** X-axis bar color. */
648
+ xColor?: string;
649
+ /** Y-axis bar color. */
650
+ yColor?: string;
651
+ /** Z-axis bar color. */
652
+ zColor?: string;
653
+ }
654
+ /**
655
+ * Build the polygons for an AxesHelper-style gizmo. Three thin cuboids,
656
+ * one per world axis. Defaults match `<PolyAxesHelper>` in the framework
657
+ * packages.
658
+ */
659
+ declare function axesHelperPolygons(options?: AxesHelperOptions): Polygon[];
660
+
661
+ /**
662
+ * Geometry for a single 3D arrow: a thin axis-aligned cuboid shaft
663
+ * stretching from the origin along one signed axis, capped with a
664
+ * 4-sided pyramid head pointing further in that direction. Used as the
665
+ * drag handle for `<TransformControls>` — same primitive recipe as
666
+ * `axesHelperPolygons`, plus an arrowhead.
667
+ *
668
+ * Returned polygons are in standard polycss world space and intended
669
+ * to be wrapped in the framework's PolyMesh equivalent for rendering.
670
+ */
671
+
672
+ interface ArrowPolygonsOptions {
673
+ /** World axis the arrow extends along: 0=X, 1=Y, 2=Z. */
674
+ axis: 0 | 1 | 2;
675
+ /** Direction along the axis: +1 (positive) or -1 (negative). Default +1. */
676
+ sign?: 1 | -1;
677
+ /** Length of the rectangular shaft along the axis. */
678
+ shaftLength?: number;
679
+ /** Half cross-section of the shaft (perpendicular to the axis). */
680
+ shaftHalfThickness?: number;
681
+ /** Length of the pyramid head along the axis (extends past the shaft). */
682
+ headLength?: number;
683
+ /** Half-extent of the pyramid base. */
684
+ headHalfThickness?: number;
685
+ /** Fill color. */
686
+ color?: string;
687
+ /** Emit the rectangular shaft polygons. Default `true`. Set `false` to
688
+ * render just the pyramid head — used by transform-control gizmos to
689
+ * declutter back-facing axes (only the head still identifies direction
690
+ * while the shaft would visually overlap the front-facing arrow). */
691
+ shaft?: boolean;
692
+ }
693
+ /** Build the polygons for one signed-axis arrow. */
694
+ declare function arrowPolygons(options: ArrowPolygonsOptions): Polygon[];
695
+
696
+ /**
697
+ * Geometry for a flat ring (annulus) lying in the plane perpendicular
698
+ * to a chosen axis. Used as the rotation handle in
699
+ * `<TransformControls mode="rotate">` — three rings, one per axis,
700
+ * each draggable to rotate the target around that axis.
701
+ *
702
+ * The ring is a sequence of quad segments around a circle. We don't
703
+ * model a true torus (tube) — a flat annulus reads cleanly as a
704
+ * "rotation circle" and keeps the polygon count proportional to the
705
+ * `segments` knob.
706
+ *
707
+ * Returned polygons are in standard polycss world space and intended
708
+ * to be wrapped in the framework's PolyMesh equivalent for rendering.
709
+ */
710
+
711
+ interface RingPolygonsOptions {
712
+ /** World axis the ring is perpendicular to: 0=X, 1=Y, 2=Z. The ring
713
+ * itself lies in the plane spanned by the other two axes. */
714
+ axis: 0 | 1 | 2;
715
+ /** Mid-radius of the ring (distance from center to the middle of
716
+ * the annulus band). */
717
+ radius: number;
718
+ /** Half-width of the annulus band — the ring spans `radius - half`
719
+ * to `radius + half`. */
720
+ halfThickness?: number;
721
+ /** Number of quad segments around the circle. Higher = smoother. */
722
+ segments?: number;
723
+ /** Fill color. */
724
+ color?: string;
725
+ }
726
+ /** Build the polygons for a flat ring (annulus). */
727
+ declare function ringPolygons(options: RingPolygonsOptions): Polygon[];
728
+
729
+ /**
730
+ * One square quad covering the bounding box of a ring (annulus) in the
731
+ * plane perpendicular to a chosen axis. Used by `<PolyTransformControls
732
+ * mode="rotate">` together with a CSS `mask: radial-gradient(...)` to
733
+ * render the visible donut, replacing the segmented quad-strip approach
734
+ * of `ringPolygons` with a single DOM element per ring.
735
+ *
736
+ * The caller is responsible for applying the mask CSS and using a donut-
737
+ * shaped hit-test (the quad's bounding rect alone would over-hit the
738
+ * inner hole). The recommended setup is to set the CSS custom property
739
+ * `--ring-inner-ratio` on the mesh element so the mask scales with the
740
+ * caller's chosen thickness ratio.
741
+ */
742
+
743
+ interface RingQuadPolygonsOptions {
744
+ /** World axis the ring is perpendicular to: 0=X, 1=Y, 2=Z. The quad
745
+ * lies in the plane spanned by the other two axes. */
746
+ axis: 0 | 1 | 2;
747
+ /** Outer radius of the ring. The quad spans ±outerRadius in both
748
+ * in-plane axes. */
749
+ outerRadius: number;
750
+ /** Fill color. */
751
+ color?: string;
752
+ }
753
+ /** Build a single 4-vertex polygon (a square) bounding the ring's outer
754
+ * circle. CSS `mask` is expected to clip this to the donut shape at
755
+ * render time. */
756
+ declare function ringQuadPolygons(options: RingQuadPolygonsOptions): Polygon[];
757
+
758
+ /**
759
+ * A flat quad on one of the three axis-aligned planes, offset diagonally
760
+ * along the two in-plane axes. Used as a planar drag handle in
761
+ * `<PolyTransformControls>` — clicking and dragging this handle moves the
762
+ * attached mesh along two axes simultaneously (XY, XZ, or YZ), instead of
763
+ * the single-axis motion the arrow shafts provide.
764
+ *
765
+ * The polygon lives in standard polycss world space; wrap it in the
766
+ * framework's PolyMesh equivalent for rendering.
767
+ */
768
+
769
+ interface PlanePolygonsOptions {
770
+ /** Axis perpendicular to the plane: 0 = YZ plane, 1 = XZ plane,
771
+ * 2 = XY plane. The quad lies on the OTHER two axes. */
772
+ axis: 0 | 1 | 2;
773
+ /** Half-extent of the quad along each in-plane axis. Default `0.4`. */
774
+ size?: number;
775
+ /** Center of the quad along the two in-plane axes. Pass a single number
776
+ * to use the same offset on both (positive places the handle in the
777
+ * +A/+B corner). Pass `[offsetA, offsetB]` to control each
778
+ * independently — sign flips move the handle to a different octant.
779
+ * `A = (axis+1)%3`, `B = (axis+2)%3`. Default `size * 2`. */
780
+ offset?: number | [number, number];
781
+ /** Position along the perpendicular axis. Default `0` (on the plane). */
782
+ along?: number;
783
+ /** Fill color. */
784
+ color?: string;
785
+ }
786
+ /** Build the polygons for one axis-aligned planar drag handle. */
787
+ declare function planePolygons(options: PlanePolygonsOptions): Polygon[];
788
+
789
+ /**
790
+ * Geometry for a small solid-color octahedron — the marker shape used by
791
+ * `GlyphcssDirectionalLightHelper` to indicate where a directional light is
792
+ * shining from. Eight CCW-from-outside triangular faces, vertices at
793
+ * `center ± (size, 0, 0)` etc.
794
+ */
795
+
796
+ interface OctahedronPolygonsOptions {
797
+ /** Center of the octahedron in world space. */
798
+ center: Vec3;
799
+ /** Half-extent (distance from center to each pole vertex). */
800
+ size: number;
801
+ /** Fill color applied to all eight faces. */
802
+ color?: string;
803
+ }
804
+ declare function octahedronPolygons(options: OctahedronPolygonsOptions): Polygon[];
805
+
806
+ /**
807
+ * Unified parser return type. All polygon-emitting parsers (parseObj,
808
+ * parseGltf, the loadMesh dispatcher) return this exact shape.
809
+ *
810
+ * The asymmetric helper `parseMtl` returns its own `MtlParseResult` (it
811
+ * emits materials, not polygons) — see parseMtl.ts for the rationale.
812
+ *
813
+ * Lifecycle contract: callers MUST call `dispose()` when the result is no
814
+ * longer needed. Idempotent — safe to call on unmount even if `objectUrls`
815
+ * is empty (e.g. `parseObj`, where it's a no-op).
816
+ */
817
+
818
+ interface ParseAnimationClip {
819
+ /** Stable numeric index in the source file's animation array. */
820
+ index: number;
821
+ /** Human-readable clip name. Falls back to `animation_N` when omitted. */
822
+ name: string;
823
+ /** Clip duration in seconds, derived from its sampler input accessors. */
824
+ duration: number;
825
+ /** Number of glTF animation channels in the clip. */
826
+ channelCount: number;
827
+ }
828
+ interface ParseAnimationController {
829
+ /** Animation clips exposed by the parsed mesh. Empty when none are usable. */
830
+ clips: ParseAnimationClip[];
831
+ /**
832
+ * Sample a clip at `timeSeconds` and return a fresh polygon list.
833
+ * `clip` accepts either the clip index or its name. Time wraps by duration.
834
+ */
835
+ sample: (clip: number | string, timeSeconds: number) => Polygon[];
836
+ }
837
+ interface ParseResult {
838
+ /** The mesh, as a flat polygon list. Already vertex-permuted to polycss space. */
839
+ polygons: Polygon[];
840
+ /** Optional animation sampler for formats that carry timeline data. */
841
+ animation?: ParseAnimationController;
842
+ /**
843
+ * Blob/object URLs minted during parse (e.g. embedded GLB images). Pass-by-
844
+ * reference — the same array is exposed on the result for visibility, and
845
+ * `dispose()` revokes each one. Do NOT mutate this array externally.
846
+ */
847
+ objectUrls: string[];
848
+ /**
849
+ * Idempotent — revokes object URLs. Safe to call on unmount, safe to call
850
+ * twice. Parsers without minted URLs (parseObj, parseMtl) supply a no-op.
851
+ */
852
+ dispose: () => void;
853
+ /**
854
+ * Non-fatal warnings raised during parse. Empty for parsers that don't
855
+ * have a warning channel; populated when downstream `normalizePolygons`
856
+ * is invoked through the high-level pipeline.
857
+ */
858
+ warnings: string[];
859
+ /** Optional format-specific metadata. */
860
+ metadata?: {
861
+ /** Triangle count after fan-triangulation (parseObj) or post-triangulation (parseGltf). */
862
+ triangleCount?: number;
863
+ /** Mesh names from the file (for glTF, from doc.meshes[].name). */
864
+ meshes?: string[];
865
+ /** Material names (in first-seen order). */
866
+ materials?: string[];
867
+ /** Animation clips from the file, mirrored from `animation.clips`. */
868
+ animations?: ParseAnimationClip[];
869
+ /** Source file size in bytes (for diagnostics). */
870
+ sourceBytes?: number;
871
+ };
872
+ }
873
+
874
+ /**
875
+ * GlyphcssAnimationMixer — three.js-shaped animation API for glyphcss.
876
+ *
877
+ * Mirrors three.js's AnimationMixer + AnimationAction surface closely enough
878
+ * that users familiar with drei's `useAnimations` can migrate without friction.
879
+ *
880
+ * Loop mode constants match three.js numeric values exactly:
881
+ * LoopOnce = 2200, LoopRepeat = 2201, LoopPingPong = 2202
882
+ */
883
+
884
+ declare const LoopOnce: 2200;
885
+ declare const LoopRepeat: 2201;
886
+ declare const LoopPingPong: 2202;
887
+ type LoopMode = typeof LoopOnce | typeof LoopRepeat | typeof LoopPingPong;
888
+
889
+ /**
890
+ * Minimal target interface the mixer requires. `GlyphcssMeshHandle` from both
891
+ * the glyphcss vanilla API and the React/Vue frameworks satisfies this
892
+ * structurally — no import needed.
893
+ */
894
+ interface GlyphcssAnimationTarget {
895
+ setPolygons(polygons: Polygon[]): void;
896
+ }
897
+ /**
898
+ * Per-clip playback action. Mirrors three.js `AnimationAction` method surface.
899
+ * All mutating methods return `this` for chaining.
900
+ */
901
+ interface GlyphcssAnimationAction {
902
+ /** Start playing (sets weight=1, resets time if not already playing). */
903
+ play(): GlyphcssAnimationAction;
904
+ /** Stop playing and reset time to 0. */
905
+ stop(): GlyphcssAnimationAction;
906
+ /** Reset time to 0 without stopping. */
907
+ reset(): GlyphcssAnimationAction;
908
+ /** Fade weight from 0 to 1 over `durationSeconds`. */
909
+ fadeIn(durationSeconds: number): GlyphcssAnimationAction;
910
+ /** Fade weight from current to 0 over `durationSeconds`. */
911
+ fadeOut(durationSeconds: number): GlyphcssAnimationAction;
912
+ /**
913
+ * Cross-fade from this action to `target` over `durationSeconds`.
914
+ * Fades this out and target in simultaneously.
915
+ */
916
+ crossFadeTo(target: GlyphcssAnimationAction, durationSeconds: number): GlyphcssAnimationAction;
917
+ /**
918
+ * Cross-fade from `from` into this action over `durationSeconds`.
919
+ * Sugar for `from.fadeOut(d); this.fadeIn(d)`.
920
+ */
921
+ crossFadeFrom(from: GlyphcssAnimationAction, durationSeconds: number): GlyphcssAnimationAction;
922
+ /** Set loop mode and repetition count. */
923
+ setLoop(mode: LoopMode, repetitions: number): GlyphcssAnimationAction;
924
+ /** Override the effective time scale. */
925
+ setEffectiveTimeScale(scale: number): GlyphcssAnimationAction;
926
+ /** Override the effective weight. */
927
+ setEffectiveWeight(weight: number): GlyphcssAnimationAction;
928
+ /** When true, the action freezes on the last frame after finishing. */
929
+ clampWhenFinished: boolean;
930
+ /** Playback speed multiplier. Default 1. */
931
+ timeScale: number;
932
+ /** Blend weight [0, 1]. Default 1. */
933
+ weight: number;
934
+ /** Current playback position in seconds. */
935
+ time: number;
936
+ /**
937
+ * When false, the action contributes 0 weight to the blend even if
938
+ * `weight > 0`. Time still advances. Default true.
939
+ */
940
+ enabled: boolean;
941
+ /**
942
+ * When true, time does NOT advance on `mixer.update()` but the action
943
+ * remains active and contributes its current weight to the blend. Default false.
944
+ */
945
+ paused: boolean;
946
+ /** Whether the action is currently playing. */
947
+ readonly isRunning: boolean;
948
+ }
949
+ /**
950
+ * Drives one or more `GlyphcssAnimationAction`s against a single mesh target.
951
+ * Mirrors the three.js `AnimationMixer` API.
952
+ */
953
+ interface GlyphcssAnimationMixer {
954
+ /**
955
+ * Return the action for a clip (by index or name). Creates the action if it
956
+ * doesn't exist yet (lazy instantiation, same as three.js).
957
+ */
958
+ clipAction(clip: number | string): GlyphcssAnimationAction;
959
+ /**
960
+ * Return an existing action without creating one. Returns null if the
961
+ * action hasn't been instantiated yet.
962
+ */
963
+ existingAction(clip: number | string): GlyphcssAnimationAction | null;
964
+ /**
965
+ * Advance all active actions by `deltaSeconds` and apply the resulting
966
+ * polygon frame to the root target. Call this once per animation frame.
967
+ */
968
+ update(deltaSeconds: number): void;
969
+ /** Stop all active actions. */
970
+ stopAllAction(): void;
971
+ /** Remove a cached action for `clip`. */
972
+ uncacheClip(clip: number | string): void;
973
+ /** Remove all cached actions for this mixer's root. */
974
+ uncacheRoot(): void;
975
+ }
976
+ declare function createGlyphcssAnimationMixer(root: GlyphcssAnimationTarget, controller: ParseAnimationController): GlyphcssAnimationMixer;
977
+
978
+ interface ObjParseOptions {
979
+ /**
980
+ * Largest mesh extent (in scene-space units). The mesh is uniformly
981
+ * scaled so its longest bbox dimension equals this. Default: 60.
982
+ */
983
+ targetSize?: number;
984
+ /**
985
+ * Padding added to the bbox of every emitted polygon so they don't land
986
+ * at coordinate "0". Default: 1.
987
+ */
988
+ gridShift?: number;
989
+ /**
990
+ * Color used for faces that have no `usemtl` in scope, or whose material
991
+ * name doesn't resolve via `materialColors`. Default: "#888888".
992
+ */
993
+ defaultColor?: string;
994
+ /**
995
+ * Override map: material name → CSS color string. Falls back to:
996
+ * 1. The material name interpreted as a 6-char hex (e.g. "FF9800" → "#FF9800"),
997
+ * 2. Otherwise a slot from `palette` indexed by first-seen material order,
998
+ * 3. Otherwise `defaultColor`.
999
+ */
1000
+ materialColors?: Record<string, string>;
1001
+ /**
1002
+ * Optional map: material name → texture URL. When set, every triangle
1003
+ * emitted under that material gets `texture` populated. The renderer
1004
+ * stamps the image across the triangle's local 2D plane.
1005
+ */
1006
+ materialTextures?: Record<string, string>;
1007
+ /**
1008
+ * Palette used to assign colors to materials whose names aren't hex.
1009
+ * Each new non-hex material name takes the next palette slot.
1010
+ */
1011
+ palette?: string[];
1012
+ /**
1013
+ * Names of `o <name>` objects to KEEP. When set, faces outside these
1014
+ * objects are dropped.
1015
+ */
1016
+ includeObjects?: string[];
1017
+ /**
1018
+ * Names of `o <name>` objects to DROP. Applied after `includeObjects`.
1019
+ * Faces with no enclosing `o` line are kept unless `includeObjects` is set.
1020
+ */
1021
+ excludeObjects?: string[];
1022
+ }
1023
+ declare function parseObj(text: string, options?: ObjParseOptions): ParseResult;
1024
+
1025
+ /**
1026
+ * Wavefront `.mtl` material file parser. Companion to parseObj — reads the
1027
+ * material library that ships next to a `.obj` and returns per-material
1028
+ * diffuse color (`Kd`) and optional diffuse texture map path (`map_Kd`).
1029
+ *
1030
+ * Usage:
1031
+ * const mtl = await fetch("/foo.mtl").then(r => r.text());
1032
+ * const { colors, textures } = parseMtl(mtl);
1033
+ * const obj = await fetch("/foo.obj").then(r => r.text());
1034
+ * const { polygons } = parseObj(obj, { materialColors: colors, materialTextures: textures });
1035
+ *
1036
+ * Texture paths are returned exactly as written in the .mtl — relative paths,
1037
+ * Windows backslashes etc. are not normalized. Callers are expected to
1038
+ * resolve them against the .mtl's base URL.
1039
+ *
1040
+ * NOTE: parseMtl intentionally returns its own `MtlParseResult` shape
1041
+ * (NOT the unified `ParseResult`). It's an asymmetric helper — it emits
1042
+ * materials, not polygons — and forcing it into ParseResult would mean
1043
+ * an empty `polygons[]` and a misleading `dispose()`.
1044
+ */
1045
+ interface MtlParseResult {
1046
+ /** Material name → CSS hex color (from `Kd r g b`). */
1047
+ colors: Record<string, string>;
1048
+ /** Material name → texture path (from `map_Kd <path>`). Path is unresolved. */
1049
+ textures: Record<string, string>;
1050
+ }
1051
+ declare function parseMtl(text: string): MtlParseResult;
1052
+
1053
+ interface GltfParseOptions {
1054
+ /** Largest mesh extent (units). Mesh is uniformly scaled to fit. Default 60. */
1055
+ targetSize?: number;
1056
+ /** Padding offset (avoids coordinate "0"). Default 1. */
1057
+ gridShift?: number;
1058
+ /** Color used when a primitive has no material or no baseColorFactor. */
1059
+ defaultColor?: string;
1060
+ /**
1061
+ * Override map: glTF material name → CSS color string. Falls back to the
1062
+ * material's `pbrMetallicRoughness.baseColorFactor` if not in this map.
1063
+ */
1064
+ materialColors?: Record<string, string>;
1065
+ /**
1066
+ * Which axis is "up" in the source mesh.
1067
+ * - "y" (default, glTF spec): cyclic permutation (x,y,z) → (z,x,y) so
1068
+ * +Y ends up on polycss's +Z (elevation).
1069
+ * - "z" (Blender-style, FBX2glTF often emits this): identity, no swap.
1070
+ * Pick "z" if the model lands on its side / lies down instead of
1071
+ * standing.
1072
+ */
1073
+ upAxis?: "y" | "z";
1074
+ /**
1075
+ * For .gltf (non-binary) — resolve a glTF buffer URI to its bytes. The
1076
+ * built-in parser handles GLB binary chunks natively; .gltf files with
1077
+ * external .bin files need this.
1078
+ */
1079
+ resolveBuffer?: (uri: string) => Promise<Uint8Array> | Uint8Array;
1080
+ /**
1081
+ * Base URL the source file lives at. Used to resolve external image URIs
1082
+ * (`doc.images[i].uri = "Textures/foo.png"`) against the GLB/glTF's
1083
+ * location. Without this, relative URIs would resolve against the page,
1084
+ * which 404s. Pass the same URL you fetched the file from.
1085
+ */
1086
+ baseUrl?: string;
1087
+ }
1088
+ declare function parseGltf(input: ArrayBuffer | Uint8Array, options?: GltfParseOptions): ParseResult;
1089
+
1090
+ interface SolidTextureSampleOptions {
1091
+ /**
1092
+ * Set false to keep every textured polygon texture-backed. Defaults to true
1093
+ * when a browser-like Image + canvas environment is available.
1094
+ */
1095
+ enabled?: boolean;
1096
+ /** Per-channel tolerance for declaring sampled texels uniform. Default 2. */
1097
+ colorTolerance?: number;
1098
+ /** Skip decoding very large textures for this optimization. Default 16 MP. */
1099
+ maxTexturePixels?: number;
1100
+ }
1101
+ declare function bakeSolidTextureSampledPolygons(polygons: Polygon[], options?: SolidTextureSampleOptions): Promise<Polygon[]>;
1102
+ declare function bakeSolidTextureSamples(result: ParseResult, options?: SolidTextureSampleOptions): Promise<ParseResult>;
1103
+
1104
+ interface VoxParseOptions {
1105
+ /**
1106
+ * Largest mesh extent (in scene-space units). The mesh is uniformly
1107
+ * scaled so its longest bbox dimension equals this. Default: 60.
1108
+ */
1109
+ targetSize?: number;
1110
+ /**
1111
+ * Per-coordinate offset added after scaling. Keeps coordinates away from
1112
+ * zero (matching OBJ/glTF parsers). Default: 0 — vox already starts at
1113
+ * non-negative integers so zero makes sensible default.
1114
+ */
1115
+ gridShift?: number;
1116
+ }
1117
+ declare function parseVox(buffer: ArrayBuffer, options?: VoxParseOptions): ParseResult;
1118
+
1119
+ /**
1120
+ * loadMesh — high-level fetch+parse dispatcher. Picks the parser by file
1121
+ * extension, fetches the URL, runs the parser, returns the unified
1122
+ * `ParseResult`.
1123
+ *
1124
+ * Supported:
1125
+ * - `.obj` → text fetch + `parseObj`
1126
+ * - `.glb` → ArrayBuffer fetch + `parseGltf`
1127
+ * - `.gltf` → ArrayBuffer fetch + `parseGltf` (caller may pass `baseUrl`)
1128
+ * - `.vox` → ArrayBuffer fetch + `parseVox`
1129
+ *
1130
+ * `.mtl` is rejected — it's a material file, not a mesh. Use `parseMtl`
1131
+ * directly if you want to read materials.
1132
+ *
1133
+ * Other extensions throw. Future formats (STL, PLY) plug in here.
1134
+ */
1135
+
1136
+ interface LoadMeshOptions {
1137
+ /**
1138
+ * Base URL for resolving relative texture/buffer URIs inside the mesh
1139
+ * (passed through to `parseGltf` for embedded image extraction). When
1140
+ * omitted, the URL passed to `loadMesh` is used as the base.
1141
+ */
1142
+ baseUrl?: string;
1143
+ /**
1144
+ * Companion `.mtl` URL for OBJ files. When set, loadMesh fetches the
1145
+ * mtl, runs `parseMtl`, and threads `materialColors` + `materialTextures`
1146
+ * into `parseObj` — so the OBJ renders with its authored materials.
1147
+ * Texture paths inside the mtl are resolved against the mtl URL.
1148
+ * Ignored for `.glb` / `.gltf` (they carry materials inline).
1149
+ */
1150
+ mtlUrl?: string;
1151
+ /** Forwarded to `parseObj` (merged with materials derived from `mtlUrl`). */
1152
+ objOptions?: ObjParseOptions;
1153
+ /** Forwarded to `parseGltf`. */
1154
+ gltfOptions?: GltfParseOptions;
1155
+ /** Forwarded to `parseVox`. */
1156
+ voxOptions?: VoxParseOptions;
1157
+ /**
1158
+ * Converts texture-backed faces whose UV samples are a uniform color into
1159
+ * solid-color polygons before culling/merging. This avoids atlas sprites for
1160
+ * low-poly models that use texture atlases as color swatches.
1161
+ */
1162
+ solidTextureSamples?: boolean | SolidTextureSampleOptions;
1163
+ /**
1164
+ * Mesh optimization intent. Defaults to "lossy"; set "lossless" to keep
1165
+ * exact planar candidates only.
1166
+ */
1167
+ meshResolution?: MeshResolution;
1168
+ }
1169
+ declare function loadMesh(url: string, options?: LoadMeshOptions): Promise<ParseResult>;
1170
+
1171
+ /**
1172
+ * Perspective projection identical to RadiantHero's: `persp = 4 / (z + 3)`.
1173
+ * Returns `[col, row, depth]` in grid space. `cellAspect` corrects for the
1174
+ * non-square character cell (height / width).
1175
+ */
1176
+ declare function project(v: Vec3, cols: number, rows: number, cellAspect: number, cx?: number, cy?: number, scale?: number): [number, number, number];
1177
+
1178
+ /** Public: derive feature edges from a triangle list. `featureAngleDeg = 0` = all edges. */
1179
+ declare function trianglesToFeatureEdges(triangles: TextureTriangle[], featureAngleDeg?: number): WireframeEdge[];
1180
+
1181
+ export { type ApproximateMergeOptions, type ArrowPolygonsOptions, type AutoRotateConfig, type AutoRotateOption, type AxesHelperOptions, BASE_TILE, CAMERA_BACKFACE_CULL_EPS, type CameraCullNormalGroup, type CameraCullRotation, type CameraHandle, type CameraState, type CameraStyleInput, type CharRamp, type CoverPlanarPolygonsOptions, type CullInteriorOptions, DEFAULT_CAMERA_STATE, DEFAULT_PROJECTION, type DedupeOverlappingPolygonsOptions, type EdgeWeight, type GltfParseOptions, type GlyphcssAmbientLight, type GlyphcssAnimationAction, type ParseAnimationClip as GlyphcssAnimationClip, type GlyphcssAnimationMixer, type GlyphcssAnimationTarget, type GlyphcssDirectionalLight, type GridSize, type Hotspot, type HotspotCell, type LoadMeshOptions, type LoopMode, LoopOnce, LoopPingPong, LoopRepeat, type MeshResolution, type MtlParseResult, type NormalizeResult, type ObjParseOptions, type OctahedronPolygonsOptions, type OptimizeMeshPolygonsOptions, type ParseAnimationClip, type ParseAnimationController, type ParseResult, type ParsedColor, type PlanePolygonsOptions, type Polygon, type PolygonFace, QUAT_IDENTITY, type Quat, type RenderMode, type RingPolygonsOptions, type RingQuadPolygonsOptions, type SceneBbox, type SceneContext, type SceneContextBuildArgs, type SceneContextBuildResult, type SolidTextureSampleOptions, type TextureTriangle, VOXEL_CAMERA_CULL_AXIS_EPS, VOXEL_CAMERA_CULL_NORMAL_LIMIT, type Vec2, type Vec3, type VoxParseOptions, type WireframeEdge, arrowPolygons, axesHelperPolygons, bakeSolidTextureSampledPolygons, bakeSolidTextureSamples, buildSceneContext, cameraCullNormalGroups, cameraCullNormalGroupsFromPolygons, cameraCullNormalKey, cameraCullVisibleSignature, cameraFacingDepth, clampChannel, computeSceneBbox, computeShapeLighting, coverPlanarPolygons, createGlyphcssAnimationMixer, createIsometricCamera, cullInteriorPolygons, dedupeOverlappingPolygons, eulerXYZFromQuat, findOverlappingPolygonDuplicates, formatColor, inverseRotateVec3, isAxisAlignedSurfaceNormal, isVoxelCameraCullableNormalGroups, loadMesh, mergePolygons, normalFacesCamera, normalizeInvertMultiplier, normalizePolygons, octahedronPolygons, optimizeMeshPolygons, parseColor, parseGltf, parseHexColor, parseMtl, parseObj, parsePureColor, parseRgbColor, parseVox, planePolygons, polygonCssSurfaceNormal, polygonFaces, polygonFacesCamera, project, quatFromAxisAngle, quatFromEulerXYZ, quatMultiply, ringPolygons, ringQuadPolygons, rotateVec3, shadeColor, trianglesToFeatureEdges };