@layoutit/polycss-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,914 @@
1
+ declare const DEFAULT_PROJECTION: "cubic";
2
+ /**
3
+ * How polygon lighting is applied by DOM renderers.
4
+ * - "baked": multiply the light tint into the off-DOM canvas before the
5
+ * polygon becomes an atlas sprite. Best fidelity (full RGB tint) but
6
+ * the atlas re-rasterizes whenever the light changes.
7
+ * - "dynamic": lighting computed entirely in CSS via per-polygon normals
8
+ * embedded in calc() and scene-root light vars (background-color +
9
+ * background-blend-mode multiply, masked by the atlas alpha). Atlas
10
+ * stays light-independent — sliding the light only writes a few CSS
11
+ * variables, no JS work, no atlas re-rasterization.
12
+ */
13
+ type PolyTextureLightingMode = "baked" | "dynamic";
14
+ /**
15
+ * 3D point/vector, stored as a `[x, y, z]` tuple. Tuple (rather than
16
+ * `{x, y, z}`) for compact JSON: meshes serialize to thousands of vertices
17
+ * and the difference adds up. Destructure with `const [x, y, z] = v` when
18
+ * you need named axes.
19
+ *
20
+ * Polycss world space convention: +X right, +Y forward, +Z up.
21
+ */
22
+ type Vec3 = [number, number, number];
23
+ /**
24
+ * 2D point/vector — `[u, v]`. Used for texture-atlas UV coordinates on
25
+ * polygons. Convention follows OBJ: u is horizontal (0=left, 1=right),
26
+ * v is vertical (0=bottom, 1=top). Renderers flip v when binding to raster
27
+ * image space whose Y-axis points down.
28
+ */
29
+ type Vec2 = [number, number];
30
+ interface TextureTriangle {
31
+ vertices: [Vec3, Vec3, Vec3];
32
+ uvs: [Vec2, Vec2, Vec2];
33
+ }
34
+ /**
35
+ * Directional light — simulates a single distant source (sun, key light).
36
+ * Contributes Lambert shading scaled by `intensity`. `direction` is in
37
+ * scene-local CSS-pixel coords and does not need to be pre-normalized.
38
+ * Mirrors three.js's `DirectionalLight`.
39
+ */
40
+ interface PolyDirectionalLight {
41
+ /** Direction the light shines TOWARD (typical convention). */
42
+ direction: Vec3;
43
+ /** Light tint, hex string. White by default. */
44
+ color?: string;
45
+ /** Scalar multiplier on the directional contribution. Default 1. */
46
+ intensity?: number;
47
+ }
48
+ /**
49
+ * Ambient light — uniform fill that adds to every polygon regardless of
50
+ * orientation. Mirrors three.js's `AmbientLight`. Decoupled from the
51
+ * directional contribution: the two add independently rather than
52
+ * splitting a fixed energy budget.
53
+ */
54
+ interface PolyAmbientLight {
55
+ /** Tint, hex string. White by default. */
56
+ color?: string;
57
+ /** Scalar multiplier on the ambient contribution. Default 0.4. */
58
+ intensity?: number;
59
+ }
60
+ /**
61
+ * Material — paint configuration shareable across many polygons.
62
+ *
63
+ * In CSS terms, a material bundles the `background-image` source plus paint
64
+ * config. When a polygon references a material AND its UVs form an
65
+ * axis-aligned rectangle, polycss renders the polygon as an <i> with
66
+ * `background-image: url(material.texture)` directly — no per-polygon canvas
67
+ * rasterization, browser-cached texture, mounting / unmounting one polygon
68
+ * does not affect any other.
69
+ *
70
+ * Three.js parallel: combines THREE.Texture + a basic Material in one. CSS
71
+ * has no shader/sampler concerns, so the texture/material split from
72
+ * Three.js doesn't pay rent here.
73
+ */
74
+ interface PolyMaterial {
75
+ /** Image source. Anything `background-image: url(...)` can use. */
76
+ texture: string;
77
+ /** Optional unique key (used by polycss to dedupe / cache). Caller can
78
+ * pass a stable string; if omitted, the material's identity is its object
79
+ * reference. */
80
+ key?: string;
81
+ }
82
+ /**
83
+ * The single polygon type for polycss. N coplanar vertices in 3D space,
84
+ * CCW winding from outside. No bbox field, no shape discriminator, no
85
+ * input/output distinction — one type, used by parsers, by the merge
86
+ * pass, and by the renderer.
87
+ */
88
+ interface Polygon {
89
+ /** N coplanar vertices in 3D space, CCW winding from outside. */
90
+ vertices: Vec3[];
91
+ /**
92
+ * Solid base color. Falls back to "#cccccc" when neither color nor
93
+ * texture is set.
94
+ */
95
+ color?: string;
96
+ /**
97
+ * Texture URL. When set with `uvs`, UV-mapped via affine; without
98
+ * `uvs`, single-tile fill. If the load fails, renderer falls back to
99
+ * `color` (or default gray).
100
+ */
101
+ texture?: string;
102
+ /**
103
+ * Shared material. When set, `material.texture` takes precedence over the
104
+ * inline `texture` field. If the polygon's UVs form an axis-aligned
105
+ * rectangle, polycss uses the direct CSS background-image path (no per-
106
+ * polygon canvas rasterization). Falls back to the atlas path otherwise.
107
+ */
108
+ material?: PolyMaterial;
109
+ /**
110
+ * Per-vertex UV coords (0..1, OBJ convention with v=0 at bottom).
111
+ * Length MUST equal vertices.length when set; mismatched UVs are
112
+ * stripped by `normalizePolygons`.
113
+ */
114
+ uvs?: Vec2[];
115
+ /**
116
+ * Renderer-internal source triangles for UV textures. Merge passes use this
117
+ * to reduce DOM planes while preserving per-triangle texture mapping in the
118
+ * generated atlas.
119
+ * @internal
120
+ */
121
+ textureTriangles?: TextureTriangle[];
122
+ /**
123
+ * User-controlled metadata. Reflected to DOM as `data-*` attributes via
124
+ * stringification by the framework wrappers. Only string|number|boolean
125
+ * values are kept; other shapes are dropped by `normalizePolygons`.
126
+ */
127
+ data?: Record<string, string | number | boolean>;
128
+ }
129
+
130
+ /**
131
+ * normalizePolygons — validates a polygon list, drops degenerate inputs,
132
+ * triangulates non-coplanar N-gons, strips bad UVs, sanitizes data, and
133
+ * returns the cleaned polygons + a list of human-readable warnings.
134
+ *
135
+ * Validation rules are encoded here and covered by the normalization tests.
136
+ *
137
+ * Pure: no DOM, no I/O, deterministic. Bbox is NOT computed here — that's
138
+ * derived on demand by `buildSceneContext` / consumers.
139
+ */
140
+
141
+ interface NormalizeResult {
142
+ polygons: Polygon[];
143
+ warnings: string[];
144
+ }
145
+ declare function normalizePolygons(input: Polygon[]): NormalizeResult;
146
+
147
+ /**
148
+ * Scene context — the top-level entry point that takes a polygon mesh
149
+ * (already normalized) and returns the data the framework wrappers need
150
+ * to render.
151
+ *
152
+ * No cube grid, no per-Z layer bucketing, no wall mask, no neighbor-based
153
+ * occlusion. Just a polygon list and a scene bbox.
154
+ */
155
+
156
+ interface SceneBbox {
157
+ /** Minimum corner of the axis-aligned bounding box (inclusive). */
158
+ min: Vec3;
159
+ /** Maximum corner of the axis-aligned bounding box (inclusive). */
160
+ max: Vec3;
161
+ }
162
+ interface SceneContext {
163
+ /** Validated polygon list — the renderer iterates this. */
164
+ polygons: Polygon[];
165
+ /** Polygon-mesh bbox in world space. Used to size the scene container. */
166
+ sceneBbox: SceneBbox;
167
+ /** Warnings raised during normalization (already-applied fixes). */
168
+ warnings: string[];
169
+ }
170
+ interface SceneContextBuildArgs {
171
+ /**
172
+ * Polygon list. Pass parser output directly — `buildSceneContext` runs
173
+ * `normalizePolygons` for you.
174
+ */
175
+ polygons: Polygon[];
176
+ /**
177
+ * If true, skip the normalize pass (caller has already validated). Useful
178
+ * when chaining `mergePolygons` after a manual `normalizePolygons` call.
179
+ */
180
+ skipNormalize?: boolean;
181
+ }
182
+ interface SceneContextBuildResult {
183
+ context: SceneContext;
184
+ /**
185
+ * Mesh-bbox dimensions. Convenience copy of `context.sceneBbox` plus a
186
+ * `size` field (max - min) for callers that want a single number per axis.
187
+ */
188
+ dimensions: {
189
+ sceneBbox: SceneBbox;
190
+ size: Vec3;
191
+ };
192
+ /** Warnings raised during normalization. Mirrors `context.warnings`. */
193
+ warnings: string[];
194
+ }
195
+ /**
196
+ * Compute the axis-aligned bounding box across every vertex of every polygon.
197
+ * Returns a zero-extent bbox at origin for empty input — callers that care
198
+ * about that case should check `polygons.length` first.
199
+ */
200
+ declare function computeSceneBbox(polygons: Polygon[]): SceneBbox;
201
+ declare function buildSceneContext(args: SceneContextBuildArgs): SceneContextBuildResult;
202
+
203
+ /**
204
+ * Polygon geometry helpers — pure math operating on Polygon vertices.
205
+ *
206
+ * After cube removal in Phase 2, this module carries small polygon-level
207
+ * helpers for downstream consumers (lighting, debug metrics, etc.). The
208
+ * cube / ramp / wedge / spike face emitters lived here in voxcss; they're gone.
209
+ */
210
+
211
+ interface PolygonFace {
212
+ /** Vertices in CCW-from-outside order. Same as Polygon.vertices. */
213
+ v: Vec3[];
214
+ /** Original polygon's color, if any (for lighting helpers). */
215
+ color?: string;
216
+ }
217
+ interface TexturePaintMetricsOptions {
218
+ /** CSS pixels per world X/Y unit. Matches renderer default. */
219
+ tileSize?: number;
220
+ /** CSS pixels per world Z unit. Defaults to tileSize. */
221
+ layerElevation?: number;
222
+ /** When true, skip untextured polygons. Defaults to true. */
223
+ texturedOnly?: boolean;
224
+ }
225
+ interface TexturePaintMetrics {
226
+ /** Input polygon count before filtering. */
227
+ totalPolygons: number;
228
+ /** Polygons included in the metric after filtering and degeneracy checks. */
229
+ measuredPolygons: number;
230
+ /** Included polygons with a texture URL. */
231
+ texturedPolygons: number;
232
+ /** Sum of rectangular element areas in CSS px^2. */
233
+ elementArea: number;
234
+ /** Sum of projected polygon areas in CSS px^2. */
235
+ polygonArea: number;
236
+ /** elementArea - polygonArea, clamped at 0. */
237
+ transparentArea: number;
238
+ /** transparentArea / elementArea. */
239
+ transparentRatio: number;
240
+ /** elementArea / polygonArea. */
241
+ overdrawRatio: number;
242
+ /** Highest per-polygon transparentRatio. */
243
+ worstTransparentRatio: number;
244
+ }
245
+ /**
246
+ * Surface a polygon as a single face. The returned array always has length 1;
247
+ * the indirection exists so callers that historically iterated faces (e.g.
248
+ * the manifold check, the canvas validator) can keep their loop shape.
249
+ *
250
+ * Returns an empty array for degenerate polygons (< 3 vertices).
251
+ */
252
+ declare function polygonFaces(p: Polygon): PolygonFace[];
253
+ declare function computeTexturePaintMetrics(polygons: Polygon[], options?: TexturePaintMetricsOptions): TexturePaintMetrics;
254
+
255
+ /**
256
+ * Apply CSS-style chained `rotateX(rx) rotateY(ry) rotateZ(rz)` rotation
257
+ * to a 3D vector. Matches the matrix composition used by polycss mesh
258
+ * wrapper transforms (see `buildTransform` in each PolyMesh implementation).
259
+ *
260
+ * CSS composes `transform: rotateX(rx) rotateY(ry) rotateZ(rz)` as the
261
+ * matrix `M = Rx · Ry · Rz`, applied to a point as `M · p` — so Rz acts
262
+ * first on the point, then Ry, then Rx. Compound rotations only commute
263
+ * when axes coincide; getting the order wrong silently corrupts results
264
+ * for any two-axis combination.
265
+ *
266
+ * Angles in degrees.
267
+ */
268
+ declare function rotateVec3(v: Vec3, rxDeg: number, ryDeg: number, rzDeg: number): Vec3;
269
+ /**
270
+ * Inverse of `rotateVec3` for the same rotation tuple — transforms a
271
+ * world-space vector into the mesh's local frame. Used by the baked
272
+ * atlas pipeline to inverse-rotate the directional light so the
273
+ * pre-multiplied Lambert shading stays correct after the mesh rotates,
274
+ * and by the dynamic-mode CSS-var override for the same reason.
275
+ *
276
+ * The inverse of `M = Rx · Ry · Rz` is `M⁻¹ = Rz⁻¹ · Ry⁻¹ · Rx⁻¹`, so
277
+ * Rx⁻¹ acts first on the vector, then Ry⁻¹, then Rz⁻¹.
278
+ *
279
+ * `rot` is `[rxDeg, ryDeg, rzDeg]` matching the mesh's CSS rotation prop.
280
+ */
281
+ declare function inverseRotateVec3(v: Vec3, rot: Vec3): Vec3;
282
+
283
+ /**
284
+ * Base tile size in CSS pixels. One polycss world unit = BASE_TILE CSS
285
+ * pixels (pre-scale). Used to convert world-coordinate target values to
286
+ * CSS translations in the transform string.
287
+ */
288
+ declare const BASE_TILE = 50;
289
+ interface AutoRotateConfig {
290
+ axis?: "x" | "y";
291
+ speed?: number;
292
+ pauseOnInteraction?: boolean;
293
+ }
294
+ type AutoRotateOption = boolean | number | AutoRotateConfig;
295
+ /**
296
+ * World-coordinate camera state (Three.js-style).
297
+ *
298
+ * `target` is the world point that should appear at the viewport centre.
299
+ * Polycss world axes: [0]=X (rows/south), [1]=Y (cols/east), [2]=Z (up).
300
+ *
301
+ * `pan`, `tilt`, and `depthOffset` are gone. Translations now live inside
302
+ * `target` so they happen BEFORE rotations — enabling correct world-space
303
+ * pan at any tilt angle.
304
+ *
305
+ * `distance` is the camera's pull-back from the target in CSS pixels.
306
+ * Increasing distance moves the camera farther from the target along the
307
+ * view axis (dolly out) — analogous to three.js's spherical radius.
308
+ * Default 0 keeps the legacy behaviour unchanged.
309
+ */
310
+ interface CameraState {
311
+ target: Vec3;
312
+ rotX: number;
313
+ rotY: number;
314
+ zoom: number;
315
+ /** Camera pull-back from target in CSS pixels. Default 0. */
316
+ distance: number;
317
+ }
318
+ interface CameraStyleInput {
319
+ rows?: number;
320
+ cols?: number;
321
+ }
322
+ interface CameraHandle {
323
+ state: CameraState;
324
+ update(next: Partial<CameraState>): void;
325
+ getStyle(input?: CameraStyleInput): {
326
+ transform: string;
327
+ width: string;
328
+ height: string;
329
+ };
330
+ }
331
+ declare function normalizeInvertMultiplier(value: number | boolean | undefined): number | undefined;
332
+ declare const DEFAULT_CAMERA_STATE: CameraState;
333
+ declare function createIsometricCamera(initial?: Partial<CameraState>): CameraHandle;
334
+
335
+ interface ParsedColor {
336
+ rgb: [number, number, number];
337
+ alpha: number;
338
+ }
339
+ declare function parseHexColor(value: string): ParsedColor | null;
340
+ declare function parseRgbColor(value: string): ParsedColor | null;
341
+ /** Parse hex or rgb/rgba color strings. Pure — no DOM. */
342
+ declare function parsePureColor(input: string): ParsedColor | null;
343
+ declare function clampChannel(value: number): number;
344
+ declare function formatColor(color: ParsedColor): string;
345
+
346
+ declare function parseColor(input: string): ParsedColor | null;
347
+ /**
348
+ * Lighten/darken a color by a flat per-channel delta. Used by the framework
349
+ * wrappers for tinted-overlay debug renderers; per-polygon Lambert shading
350
+ * goes through `computeShapeLighting` instead.
351
+ */
352
+ declare function shadeColor(base: string, delta: number): string;
353
+ /**
354
+ * Per-polygon Lambert shading. Given a polygon's outward normal and the
355
+ * scene's lights, returns the shaded color as a CSS rgb string.
356
+ *
357
+ * Math (decoupled, three.js convention):
358
+ * tint = ambient.color · ambient.intensity
359
+ * + directional.color · directional.intensity · max(0, n · (−L))
360
+ * final = baseColor × tint
361
+ *
362
+ * Pass `directional` and/or `ambient` undefined to fall back to defaults
363
+ * (top-down white directional with intensity 1, white ambient with
364
+ * intensity 0.4) — useful for static SSR/validator renders.
365
+ */
366
+ declare function computeShapeLighting(normal: Vec3, baseColor: string, directional?: PolyDirectionalLight, ambient?: PolyAmbientLight): string;
367
+
368
+ /**
369
+ * Merge coplanar same-color adjacent triangles into N-vertex polygons.
370
+ *
371
+ * Each polygon is rendered as one atlas-backed DOM element — so a mesh whose
372
+ * triangles came from quads or pentagons collapses back into its original
373
+ * face count.
374
+ *
375
+ * - Geodesic spheres: ~half the triangles came from quad subdivisions
376
+ * - OBJ imports: many were quads/n-gons fan-triangulated by the importer
377
+ * - Hand-built dodecahedra: 36 triangles → 12 pentagons
378
+ *
379
+ * Algorithm:
380
+ * 1. For each input polygon, compute its plane (unit normal + signed
381
+ * distance from origin).
382
+ * 2. Build an undirected edge graph: every edge of every polygon indexes
383
+ * the polygons it belongs to.
384
+ * 3. Repeatedly walk shared edges and merge the two polygons sharing that
385
+ * edge if they pass the merge predicate (same color, near-coplanar,
386
+ * result is convex, edge is interior). Each merge replaces two
387
+ * polygons with one larger polygon and updates the edge index.
388
+ * 4. Iterate until no more merges fire — the fixed point grows triangles
389
+ * → quads → pentagons → … as far as the geometry allows.
390
+ *
391
+ * Polygons with < 3 vertices are passed through unchanged (the caller is
392
+ * expected to have run `normalizePolygons` first; this is a defensive copy).
393
+ */
394
+
395
+ declare function mergePolygons(input: Polygon[]): Polygon[];
396
+
397
+ interface CoverPlanarPolygonsOptions {
398
+ /** Smallest connected coplanar group worth attempting. Default 4. */
399
+ minGroupPolygons?: number;
400
+ /** Maximum candidate 2D axes tested per group. Default 8. */
401
+ maxCandidateAxes?: number;
402
+ /** Plane normal/distance tolerance in scene units. Default 1e-3. */
403
+ planeEpsilon?: number;
404
+ }
405
+ /**
406
+ * Re-cover flat same-color mesh regions with generated convex polygons.
407
+ *
408
+ * `mergePolygons` preserves source topology: it can only combine existing
409
+ * neighboring faces. This pass is more aggressive for solid-color planar
410
+ * regions: it projects each connected coplanar patch into 2D, covers the
411
+ * patch from its outer boundary, then lets `mergePolygons` collapse the
412
+ * generated cover into large rects/quads where possible.
413
+ */
414
+ declare function coverPlanarPolygons(input: Polygon[], options?: CoverPlanarPolygonsOptions): Polygon[];
415
+
416
+ /**
417
+ * cullInteriorPolygons — remove polygons that are fully enclosed by other
418
+ * polygons of the same mesh and therefore never visible from any external
419
+ * camera direction.
420
+ *
421
+ * Algorithm: for each polygon p,
422
+ * 1. Sample K unit directions on the hemisphere above p's normal.
423
+ * 2. Cast a ray from a point just above p's centroid in each direction.
424
+ * 3. If at least one ray escapes without hitting any other polygon → p
425
+ * is potentially visible from some external camera → keep it.
426
+ * 4. If every ray hits another polygon → p is fully surrounded → cull it.
427
+ *
428
+ * Acceleration: flat-array SAH-built binary BVH with slab-test AABB traversal.
429
+ * All BVH data is stored in typed Float64Array / Int32Array for cache efficiency.
430
+ * Ray traversal visits only the subtrees whose AABBs the ray intersects.
431
+ *
432
+ * Runs once at parse time inside `loadMesh`, before `mergePolygons`. Zero
433
+ * runtime cost. Conservative by design — false negatives (failing to cull
434
+ * a truly hidden poly) are safe; false positives would be a visual bug.
435
+ */
436
+
437
+ interface CullInteriorOptions {
438
+ /** Hemisphere ray samples per polygon. Higher = fewer false positives, slower. Default 12. */
439
+ samples?: number;
440
+ }
441
+ declare function cullInteriorPolygons(polygons: Polygon[], options?: CullInteriorOptions): Polygon[];
442
+
443
+ /**
444
+ * Geometry for the three.js-style debug axes gizmo: three thin colored
445
+ * cuboids stretching along world-X, world-Y and world-Z. Mirrors the
446
+ * convention `red=X, green=Y, blue=Z`.
447
+ *
448
+ * Returned polygons are in the standard polycss world-space convention
449
+ * (`+X right, +Y forward, +Z up`). Wrap with the framework's PolyMesh /
450
+ * PolyScene equivalent to render.
451
+ */
452
+
453
+ interface AxesHelperOptions {
454
+ /** Length of each axis bar in world units. */
455
+ size?: number;
456
+ /** Bar cross-section width as a fraction of `size`. */
457
+ thickness?: number;
458
+ /** When true, also draws bars in the −X / −Y / −Z direction. */
459
+ negative?: boolean;
460
+ /** X-axis bar color. */
461
+ xColor?: string;
462
+ /** Y-axis bar color. */
463
+ yColor?: string;
464
+ /** Z-axis bar color. */
465
+ zColor?: string;
466
+ }
467
+ /**
468
+ * Build the polygons for an AxesHelper-style gizmo. Three thin cuboids,
469
+ * one per world axis. Defaults match `<PolyAxesHelper>` in the framework
470
+ * packages.
471
+ */
472
+ declare function axesHelperPolygons(options?: AxesHelperOptions): Polygon[];
473
+
474
+ /**
475
+ * Geometry for a single 3D arrow: a thin axis-aligned cuboid shaft
476
+ * stretching from the origin along one signed axis, capped with a
477
+ * 4-sided pyramid head pointing further in that direction. Used as the
478
+ * drag handle for `<TransformControls>` — same primitive recipe as
479
+ * `axesHelperPolygons`, plus an arrowhead.
480
+ *
481
+ * Returned polygons are in standard polycss world space and intended
482
+ * to be wrapped in the framework's PolyMesh equivalent for rendering.
483
+ */
484
+
485
+ interface ArrowPolygonsOptions {
486
+ /** World axis the arrow extends along: 0=X, 1=Y, 2=Z. */
487
+ axis: 0 | 1 | 2;
488
+ /** Direction along the axis: +1 (positive) or -1 (negative). Default +1. */
489
+ sign?: 1 | -1;
490
+ /** Length of the rectangular shaft along the axis. */
491
+ shaftLength?: number;
492
+ /** Half cross-section of the shaft (perpendicular to the axis). */
493
+ shaftHalfThickness?: number;
494
+ /** Length of the pyramid head along the axis (extends past the shaft). */
495
+ headLength?: number;
496
+ /** Half-extent of the pyramid base. */
497
+ headHalfThickness?: number;
498
+ /** Fill color. */
499
+ color?: string;
500
+ }
501
+ /** Build the polygons for one signed-axis arrow. */
502
+ declare function arrowPolygons(options: ArrowPolygonsOptions): Polygon[];
503
+
504
+ /**
505
+ * Geometry for a flat ring (annulus) lying in the plane perpendicular
506
+ * to a chosen axis. Used as the rotation handle in
507
+ * `<TransformControls mode="rotate">` — three rings, one per axis,
508
+ * each draggable to rotate the target around that axis.
509
+ *
510
+ * The ring is a sequence of quad segments around a circle. We don't
511
+ * model a true torus (tube) — a flat annulus reads cleanly as a
512
+ * "rotation circle" and keeps the polygon count proportional to the
513
+ * `segments` knob.
514
+ *
515
+ * Returned polygons are in standard polycss world space and intended
516
+ * to be wrapped in the framework's PolyMesh equivalent for rendering.
517
+ */
518
+
519
+ interface RingPolygonsOptions {
520
+ /** World axis the ring is perpendicular to: 0=X, 1=Y, 2=Z. The ring
521
+ * itself lies in the plane spanned by the other two axes. */
522
+ axis: 0 | 1 | 2;
523
+ /** Mid-radius of the ring (distance from center to the middle of
524
+ * the annulus band). */
525
+ radius: number;
526
+ /** Half-width of the annulus band — the ring spans `radius - half`
527
+ * to `radius + half`. */
528
+ halfThickness?: number;
529
+ /** Number of quad segments around the circle. Higher = smoother. */
530
+ segments?: number;
531
+ /** Fill color. */
532
+ color?: string;
533
+ }
534
+ /** Build the polygons for a flat ring (annulus). */
535
+ declare function ringPolygons(options: RingPolygonsOptions): Polygon[];
536
+
537
+ /**
538
+ * Geometry for a small solid-color octahedron — the marker shape used by
539
+ * `PolyDirectionalLightHelper` to indicate where a directional light is
540
+ * shining from. Eight CCW-from-outside triangular faces, vertices at
541
+ * `center ± (size, 0, 0)` etc.
542
+ */
543
+
544
+ interface OctahedronPolygonsOptions {
545
+ /** Center of the octahedron in world space. */
546
+ center: Vec3;
547
+ /** Half-extent (distance from center to each pole vertex). */
548
+ size: number;
549
+ /** Fill color applied to all eight faces. */
550
+ color?: string;
551
+ }
552
+ declare function octahedronPolygons(options: OctahedronPolygonsOptions): Polygon[];
553
+
554
+ /**
555
+ * Unified parser return type. All polygon-emitting parsers (parseObj,
556
+ * parseGltf, the loadMesh dispatcher) return this exact shape.
557
+ *
558
+ * The asymmetric helper `parseMtl` returns its own `MtlParseResult` (it
559
+ * emits materials, not polygons) — see parseMtl.ts for the rationale.
560
+ *
561
+ * Lifecycle contract: callers MUST call `dispose()` when the result is no
562
+ * longer needed. Idempotent — safe to call on unmount even if `objectUrls`
563
+ * is empty (e.g. `parseObj`, where it's a no-op).
564
+ */
565
+
566
+ interface ParseAnimationClip {
567
+ /** Stable numeric index in the source file's animation array. */
568
+ index: number;
569
+ /** Human-readable clip name. Falls back to `animation_N` when omitted. */
570
+ name: string;
571
+ /** Clip duration in seconds, derived from its sampler input accessors. */
572
+ duration: number;
573
+ /** Number of glTF animation channels in the clip. */
574
+ channelCount: number;
575
+ }
576
+ interface ParseAnimationController {
577
+ /** Animation clips exposed by the parsed mesh. Empty when none are usable. */
578
+ clips: ParseAnimationClip[];
579
+ /**
580
+ * Sample a clip at `timeSeconds` and return a fresh polygon list.
581
+ * `clip` accepts either the clip index or its name. Time wraps by duration.
582
+ */
583
+ sample: (clip: number | string, timeSeconds: number) => Polygon[];
584
+ }
585
+ interface ParseResult {
586
+ /** The mesh, as a flat polygon list. Already vertex-permuted to polycss space. */
587
+ polygons: Polygon[];
588
+ /** Optional animation sampler for formats that carry timeline data. */
589
+ animation?: ParseAnimationController;
590
+ /**
591
+ * Blob/object URLs minted during parse (e.g. embedded GLB images). Pass-by-
592
+ * reference — the same array is exposed on the result for visibility, and
593
+ * `dispose()` revokes each one. Do NOT mutate this array externally.
594
+ */
595
+ objectUrls: string[];
596
+ /**
597
+ * Idempotent — revokes object URLs. Safe to call on unmount, safe to call
598
+ * twice. Parsers without minted URLs (parseObj, parseMtl) supply a no-op.
599
+ */
600
+ dispose: () => void;
601
+ /**
602
+ * Non-fatal warnings raised during parse. Empty for parsers that don't
603
+ * have a warning channel; populated when downstream `normalizePolygons`
604
+ * is invoked through the high-level pipeline.
605
+ */
606
+ warnings: string[];
607
+ /** Optional format-specific metadata. */
608
+ metadata?: {
609
+ /** Triangle count after fan-triangulation (parseObj) or post-triangulation (parseGltf). */
610
+ triangleCount?: number;
611
+ /** Mesh names from the file (for glTF, from doc.meshes[].name). */
612
+ meshes?: string[];
613
+ /** Material names (in first-seen order). */
614
+ materials?: string[];
615
+ /** Animation clips from the file, mirrored from `animation.clips`. */
616
+ animations?: ParseAnimationClip[];
617
+ /** Source file size in bytes (for diagnostics). */
618
+ sourceBytes?: number;
619
+ };
620
+ }
621
+
622
+ /**
623
+ * PolyAnimationMixer — three.js-shaped animation API for polycss.
624
+ *
625
+ * Mirrors three.js's AnimationMixer + AnimationAction surface closely enough
626
+ * that users familiar with drei's `useAnimations` can migrate without friction.
627
+ *
628
+ * Loop mode constants match three.js numeric values exactly:
629
+ * LoopOnce = 2200, LoopRepeat = 2201, LoopPingPong = 2202
630
+ */
631
+
632
+ declare const LoopOnce: 2200;
633
+ declare const LoopRepeat: 2201;
634
+ declare const LoopPingPong: 2202;
635
+ type LoopMode = typeof LoopOnce | typeof LoopRepeat | typeof LoopPingPong;
636
+
637
+ /**
638
+ * Minimal target interface the mixer requires. `PolyMeshHandle` from both
639
+ * the polycss vanilla API and the React/Vue frameworks satisfies this
640
+ * structurally — no import needed.
641
+ */
642
+ interface PolyAnimationTarget {
643
+ setPolygons(polygons: Polygon[]): void;
644
+ }
645
+ /**
646
+ * Per-clip playback action. Mirrors three.js `AnimationAction` method surface.
647
+ * All mutating methods return `this` for chaining.
648
+ */
649
+ interface PolyAnimationAction {
650
+ /** Start playing (sets weight=1, resets time if not already playing). */
651
+ play(): PolyAnimationAction;
652
+ /** Stop playing and reset time to 0. */
653
+ stop(): PolyAnimationAction;
654
+ /** Reset time to 0 without stopping. */
655
+ reset(): PolyAnimationAction;
656
+ /** Fade weight from 0 to 1 over `durationSeconds`. */
657
+ fadeIn(durationSeconds: number): PolyAnimationAction;
658
+ /** Fade weight from current to 0 over `durationSeconds`. */
659
+ fadeOut(durationSeconds: number): PolyAnimationAction;
660
+ /**
661
+ * Cross-fade from this action to `target` over `durationSeconds`.
662
+ * Fades this out and target in simultaneously.
663
+ */
664
+ crossFadeTo(target: PolyAnimationAction, durationSeconds: number): PolyAnimationAction;
665
+ /**
666
+ * Cross-fade from `from` into this action over `durationSeconds`.
667
+ * Sugar for `from.fadeOut(d); this.fadeIn(d)`.
668
+ */
669
+ crossFadeFrom(from: PolyAnimationAction, durationSeconds: number): PolyAnimationAction;
670
+ /** Set loop mode and repetition count. */
671
+ setLoop(mode: LoopMode, repetitions: number): PolyAnimationAction;
672
+ /** Override the effective time scale. */
673
+ setEffectiveTimeScale(scale: number): PolyAnimationAction;
674
+ /** Override the effective weight. */
675
+ setEffectiveWeight(weight: number): PolyAnimationAction;
676
+ /** When true, the action freezes on the last frame after finishing. */
677
+ clampWhenFinished: boolean;
678
+ /** Playback speed multiplier. Default 1. */
679
+ timeScale: number;
680
+ /** Blend weight [0, 1]. Default 1. */
681
+ weight: number;
682
+ /** Current playback position in seconds. */
683
+ time: number;
684
+ /**
685
+ * When false, the action contributes 0 weight to the blend even if
686
+ * `weight > 0`. Time still advances. Default true.
687
+ */
688
+ enabled: boolean;
689
+ /**
690
+ * When true, time does NOT advance on `mixer.update()` but the action
691
+ * remains active and contributes its current weight to the blend. Default false.
692
+ */
693
+ paused: boolean;
694
+ /** Whether the action is currently playing. */
695
+ readonly isRunning: boolean;
696
+ }
697
+ /**
698
+ * Drives one or more `PolyAnimationAction`s against a single mesh target.
699
+ * Mirrors the three.js `AnimationMixer` API.
700
+ */
701
+ interface PolyAnimationMixer {
702
+ /**
703
+ * Return the action for a clip (by index or name). Creates the action if it
704
+ * doesn't exist yet (lazy instantiation, same as three.js).
705
+ */
706
+ clipAction(clip: number | string): PolyAnimationAction;
707
+ /**
708
+ * Return an existing action without creating one. Returns null if the
709
+ * action hasn't been instantiated yet.
710
+ */
711
+ existingAction(clip: number | string): PolyAnimationAction | null;
712
+ /**
713
+ * Advance all active actions by `deltaSeconds` and apply the resulting
714
+ * polygon frame to the root target. Call this once per animation frame.
715
+ */
716
+ update(deltaSeconds: number): void;
717
+ /** Stop all active actions. */
718
+ stopAllAction(): void;
719
+ /** Remove a cached action for `clip`. */
720
+ uncacheClip(clip: number | string): void;
721
+ /** Remove all cached actions for this mixer's root. */
722
+ uncacheRoot(): void;
723
+ }
724
+ declare function createPolyAnimationMixer(root: PolyAnimationTarget, controller: ParseAnimationController): PolyAnimationMixer;
725
+
726
+ interface ObjParseOptions {
727
+ /**
728
+ * Largest mesh extent (in scene-space units). The mesh is uniformly
729
+ * scaled so its longest bbox dimension equals this. Default: 60.
730
+ */
731
+ targetSize?: number;
732
+ /**
733
+ * Padding added to the bbox of every emitted polygon so they don't land
734
+ * at coordinate "0". Default: 1.
735
+ */
736
+ gridShift?: number;
737
+ /**
738
+ * Color used for faces that have no `usemtl` in scope, or whose material
739
+ * name doesn't resolve via `materialColors`. Default: "#888888".
740
+ */
741
+ defaultColor?: string;
742
+ /**
743
+ * Override map: material name → CSS color string. Falls back to:
744
+ * 1. The material name interpreted as a 6-char hex (e.g. "FF9800" → "#FF9800"),
745
+ * 2. Otherwise a slot from `palette` indexed by first-seen material order,
746
+ * 3. Otherwise `defaultColor`.
747
+ */
748
+ materialColors?: Record<string, string>;
749
+ /**
750
+ * Optional map: material name → texture URL. When set, every triangle
751
+ * emitted under that material gets `texture` populated. The renderer
752
+ * stamps the image across the triangle's local 2D plane.
753
+ */
754
+ materialTextures?: Record<string, string>;
755
+ /**
756
+ * Palette used to assign colors to materials whose names aren't hex.
757
+ * Each new non-hex material name takes the next palette slot.
758
+ */
759
+ palette?: string[];
760
+ /**
761
+ * Names of `o <name>` objects to KEEP. When set, faces outside these
762
+ * objects are dropped.
763
+ */
764
+ includeObjects?: string[];
765
+ /**
766
+ * Names of `o <name>` objects to DROP. Applied after `includeObjects`.
767
+ * Faces with no enclosing `o` line are kept unless `includeObjects` is set.
768
+ */
769
+ excludeObjects?: string[];
770
+ }
771
+ declare function parseObj(text: string, options?: ObjParseOptions): ParseResult;
772
+
773
+ /**
774
+ * Wavefront `.mtl` material file parser. Companion to parseObj — reads the
775
+ * material library that ships next to a `.obj` and returns per-material
776
+ * diffuse color (`Kd`) and optional diffuse texture map path (`map_Kd`).
777
+ *
778
+ * Usage:
779
+ * const mtl = await fetch("/foo.mtl").then(r => r.text());
780
+ * const { colors, textures } = parseMtl(mtl);
781
+ * const obj = await fetch("/foo.obj").then(r => r.text());
782
+ * const { polygons } = parseObj(obj, { materialColors: colors, materialTextures: textures });
783
+ *
784
+ * Texture paths are returned exactly as written in the .mtl — relative paths,
785
+ * Windows backslashes etc. are not normalized. Callers are expected to
786
+ * resolve them against the .mtl's base URL.
787
+ *
788
+ * NOTE: parseMtl intentionally returns its own `MtlParseResult` shape
789
+ * (NOT the unified `ParseResult`). It's an asymmetric helper — it emits
790
+ * materials, not polygons — and forcing it into ParseResult would mean
791
+ * an empty `polygons[]` and a misleading `dispose()`.
792
+ */
793
+ interface MtlParseResult {
794
+ /** Material name → CSS hex color (from `Kd r g b`). */
795
+ colors: Record<string, string>;
796
+ /** Material name → texture path (from `map_Kd <path>`). Path is unresolved. */
797
+ textures: Record<string, string>;
798
+ }
799
+ declare function parseMtl(text: string): MtlParseResult;
800
+
801
+ interface GltfParseOptions {
802
+ /** Largest mesh extent (units). Mesh is uniformly scaled to fit. Default 60. */
803
+ targetSize?: number;
804
+ /** Padding offset (avoids coordinate "0"). Default 1. */
805
+ gridShift?: number;
806
+ /** Color used when a primitive has no material or no baseColorFactor. */
807
+ defaultColor?: string;
808
+ /**
809
+ * Override map: glTF material name → CSS color string. Falls back to the
810
+ * material's `pbrMetallicRoughness.baseColorFactor` if not in this map.
811
+ */
812
+ materialColors?: Record<string, string>;
813
+ /**
814
+ * Which axis is "up" in the source mesh.
815
+ * - "y" (default, glTF spec): cyclic permutation (x,y,z) → (z,x,y) so
816
+ * +Y ends up on polycss's +Z (elevation).
817
+ * - "z" (Blender-style, FBX2glTF often emits this): identity, no swap.
818
+ * Pick "z" if the model lands on its side / lies down instead of
819
+ * standing.
820
+ */
821
+ upAxis?: "y" | "z";
822
+ /**
823
+ * For .gltf (non-binary) — resolve a glTF buffer URI to its bytes. The
824
+ * built-in parser handles GLB binary chunks natively; .gltf files with
825
+ * external .bin files need this.
826
+ */
827
+ resolveBuffer?: (uri: string) => Promise<Uint8Array> | Uint8Array;
828
+ /**
829
+ * Base URL the source file lives at. Used to resolve external image URIs
830
+ * (`doc.images[i].uri = "Textures/foo.png"`) against the GLB/glTF's
831
+ * location. Without this, relative URIs would resolve against the page,
832
+ * which 404s. Pass the same URL you fetched the file from.
833
+ */
834
+ baseUrl?: string;
835
+ }
836
+ declare function parseGltf(input: ArrayBuffer | Uint8Array, options?: GltfParseOptions): ParseResult;
837
+
838
+ interface SolidTextureSampleOptions {
839
+ /**
840
+ * Set false to keep every textured polygon texture-backed. Defaults to true
841
+ * when a browser-like Image + canvas environment is available.
842
+ */
843
+ enabled?: boolean;
844
+ /** Per-channel tolerance for declaring sampled texels uniform. Default 2. */
845
+ colorTolerance?: number;
846
+ /** Skip decoding very large textures for this optimization. Default 16 MP. */
847
+ maxTexturePixels?: number;
848
+ }
849
+ declare function bakeSolidTextureSampledPolygons(polygons: Polygon[], options?: SolidTextureSampleOptions): Promise<Polygon[]>;
850
+ declare function bakeSolidTextureSamples(result: ParseResult, options?: SolidTextureSampleOptions): Promise<ParseResult>;
851
+
852
+ interface VoxParseOptions {
853
+ /**
854
+ * Largest mesh extent (in scene-space units). The mesh is uniformly
855
+ * scaled so its longest bbox dimension equals this. Default: 60.
856
+ */
857
+ targetSize?: number;
858
+ /**
859
+ * Per-coordinate offset added after scaling. Keeps coordinates away from
860
+ * zero (matching OBJ/glTF parsers). Default: 0 — vox already starts at
861
+ * non-negative integers so zero makes sensible default.
862
+ */
863
+ gridShift?: number;
864
+ }
865
+ declare function parseVox(buffer: ArrayBuffer, options?: VoxParseOptions): ParseResult;
866
+
867
+ /**
868
+ * loadMesh — high-level fetch+parse dispatcher. Picks the parser by file
869
+ * extension, fetches the URL, runs the parser, returns the unified
870
+ * `ParseResult`.
871
+ *
872
+ * Supported:
873
+ * - `.obj` → text fetch + `parseObj`
874
+ * - `.glb` → ArrayBuffer fetch + `parseGltf`
875
+ * - `.gltf` → ArrayBuffer fetch + `parseGltf` (caller may pass `baseUrl`)
876
+ * - `.vox` → ArrayBuffer fetch + `parseVox`
877
+ *
878
+ * `.mtl` is rejected — it's a material file, not a mesh. Use `parseMtl`
879
+ * directly if you want to read materials.
880
+ *
881
+ * Other extensions throw. Future formats (STL, PLY) plug in here.
882
+ */
883
+
884
+ interface LoadMeshOptions {
885
+ /**
886
+ * Base URL for resolving relative texture/buffer URIs inside the mesh
887
+ * (passed through to `parseGltf` for embedded image extraction). When
888
+ * omitted, the URL passed to `loadMesh` is used as the base.
889
+ */
890
+ baseUrl?: string;
891
+ /**
892
+ * Companion `.mtl` URL for OBJ files. When set, loadMesh fetches the
893
+ * mtl, runs `parseMtl`, and threads `materialColors` + `materialTextures`
894
+ * into `parseObj` — so the OBJ renders with its authored materials.
895
+ * Texture paths inside the mtl are resolved against the mtl URL.
896
+ * Ignored for `.glb` / `.gltf` (they carry materials inline).
897
+ */
898
+ mtlUrl?: string;
899
+ /** Forwarded to `parseObj` (merged with materials derived from `mtlUrl`). */
900
+ objOptions?: ObjParseOptions;
901
+ /** Forwarded to `parseGltf`. */
902
+ gltfOptions?: GltfParseOptions;
903
+ /** Forwarded to `parseVox`. */
904
+ voxOptions?: VoxParseOptions;
905
+ /**
906
+ * Converts texture-backed faces whose UV samples are a uniform color into
907
+ * solid-color polygons before culling/merging. This avoids atlas sprites for
908
+ * low-poly models that use texture atlases as color swatches.
909
+ */
910
+ solidTextureSamples?: boolean | SolidTextureSampleOptions;
911
+ }
912
+ declare function loadMesh(url: string, options?: LoadMeshOptions): Promise<ParseResult>;
913
+
914
+ export { type ArrowPolygonsOptions, type AutoRotateConfig, type AutoRotateOption, type AxesHelperOptions, BASE_TILE, type CameraHandle, type CameraState, type CameraStyleInput, type CoverPlanarPolygonsOptions, type CullInteriorOptions, DEFAULT_CAMERA_STATE, DEFAULT_PROJECTION, type GltfParseOptions, type LoadMeshOptions, type LoopMode, LoopOnce, LoopPingPong, LoopRepeat, type MtlParseResult, type NormalizeResult, type ObjParseOptions, type OctahedronPolygonsOptions, type ParseAnimationClip, type ParseAnimationController, type ParseResult, type ParsedColor, type PolyAmbientLight, type PolyAnimationAction, type ParseAnimationClip as PolyAnimationClip, type PolyAnimationMixer, type PolyAnimationTarget, type PolyDirectionalLight, type PolyMaterial, type PolyTextureLightingMode, type Polygon, type PolygonFace, type RingPolygonsOptions, type SceneBbox, type SceneContext, type SceneContextBuildArgs, type SceneContextBuildResult, type SolidTextureSampleOptions, type TexturePaintMetrics, type TexturePaintMetricsOptions, type TextureTriangle, type Vec2, type Vec3, type VoxParseOptions, arrowPolygons, axesHelperPolygons, bakeSolidTextureSampledPolygons, bakeSolidTextureSamples, buildSceneContext, clampChannel, computeSceneBbox, computeShapeLighting, computeTexturePaintMetrics, coverPlanarPolygons, createIsometricCamera, createPolyAnimationMixer, cullInteriorPolygons, formatColor, inverseRotateVec3, loadMesh, mergePolygons, normalizeInvertMultiplier, normalizePolygons, octahedronPolygons, parseColor, parseGltf, parseHexColor, parseMtl, parseObj, parsePureColor, parseRgbColor, parseVox, polygonFaces, ringPolygons, rotateVec3, shadeColor };