export { SVGRenderer } from "./svg-renderer.js"; /** * A tiny engine for 3D voxel scenes rendered to SVG. */ export class Heerich { /** * Compute bounding-box center of an iterable of items. * @param {Iterable<*>} items - Items to compute bounds for * @param {function(*): [number,number,number]} get - Extracts [x,y,z] from each item * @returns {[number,number,number]} Center of the bounding box */ static _bboxCenter(items: Iterable, get: (arg0: any) => [number, number, number]): [number, number, number]; /** * Rotate a point [x,y,z] around a center by N 90° turns on the given axis. * @param {number} x * @param {number} y * @param {number} z * @param {'x'|'y'|'z'} axis * @param {number} turns - Number of 90° turns (1-3) * @param {number} cx - Center X * @param {number} cy - Center Y * @param {number} cz - Center Z * @returns {[number, number, number]} */ static _rot90(x: number, y: number, z: number, axis: "x" | "y" | "z", turns: number, cx: number, cy: number, cz: number): [number, number, number]; /** * Reconstruct a Heerich instance from serialized data. * @param {Object} data - Output of `toJSON()` * @returns {Heerich} */ static fromJSON(data: any): Heerich; /** * Scale face vertices around an origin within a voxel. * @param {[number,number,number][]} vertices - 3D polygon vertices * @param {number} x - Voxel x position * @param {number} y - Voxel y position * @param {number} z - Voxel z position * @param {[number,number,number]} scale - Per-axis scale factors [sx, sy, sz] * @param {[number,number,number]} origin - Scale origin within voxel (0-1) * @returns {[number,number,number][]} Scaled vertices */ static _scaleVertices(vertices: [number, number, number][], x: number, y: number, z: number, scale: [number, number, number], origin: [number, number, number]): [number, number, number][]; /** * @param {Object} [options] * @param {number|[number,number]|[number,number,number]} [options.tile=10] - Tile size in pixels. Single number for uniform, [x,y] or [x,y,z] for independent axes. * @param {StyleObject} [options.style] - Default face style * @param {CameraOptions} [options.camera] - Camera configuration */ constructor(options?: { tile?: number | [number, number] | [number, number, number]; style?: StyleObject; camera?: CameraOptions; }); /** @type {StyleObject} */ defaultStyle: StyleObject; /** @type {{projection: string, tileW: number, tileH: number, tileZ: number, depthOffsetX: number, depthOffsetY: number, cameraX: number, cameraY: number, cameraDistance: number}} */ renderOptions: { projection: string; tileW: number; tileH: number; tileZ: number; depthOffsetX: number; depthOffsetY: number; cameraX: number; cameraY: number; cameraDistance: number; }; /** @type {number} Default gap between voxels (0–<0.5) */ defaultGap: number; /** @type {Map} */ voxels: Map; /** @type {Map} */ decals: Map; /** @type {boolean} */ /** @type {number} Monotonically increasing epoch — bumped on every mutation */ _epoch: number; /** @type {number} Epoch at which _cachedFaces was computed */ _cachedEpoch: number; /** @type {Face[]|null} */ _cachedFaces: Face[] | null; /** @type {number} Epoch at which _cachedRawFaces was computed */ _cachedRawEpoch: number; /** @type {Face[]|null} */ _cachedRawFaces: Face[] | null; /** @type {SVGRenderer|null} */ _svgRenderer: SVGRenderer | null; /** @type {boolean} */ _batching: boolean; /** @type {Set} Voxel keys that changed since last _buildFaces3D */ _dirtyKeys: Set; /** @type {Map} Cached 3D faces per voxel key */ _faceCache3D: Map; /** * Update camera settings. Oblique cameras use angle + distance; * perspective cameras use position + distance. * @param {CameraOptions} [opts] */ setCamera(opts?: CameraOptions): void; /** * Pack coordinates into a single integer key for fast Map lookups. * Supports coordinates from -512 to 511 on each axis (10 bits + sign). * @param {number} x * @param {number} y * @param {number} z * @returns {number} */ _k(x: number, y: number, z: number): number; /** Mark the scene as modified. */ _invalidate(): void; /** * Mark a voxel and its 6 neighbors as needing face regeneration. * @param {number} x * @param {number} y * @param {number} z */ _markDirty(x: number, y: number, z: number): void; /** * Current mutation epoch. Consumers can compare this against their own * last-rendered epoch to know whether they need to re-render. * @returns {number} */ get epoch(): number; /** * Batch multiple operations so face recomputation is deferred until the end. * @param {function(): void} fn - Operations to batch */ batch(fn: () => void): void; /** * Wrap a coordinate iterator with rotation. * If no center given, computes bounding box center of the coords. * @param {Iterable} coords * @param {RotateOptions} rotate * @returns {Generator} */ _rotateCoords(coords: Iterable, rotate: RotateOptions): Generator; /** * Rotate all existing voxels in place by 90-degree increments. * @param {RotateOptions} opts */ rotate(opts: RotateOptions): void; /** * Apply a boolean operation using coordinates from an iterator. * @param {Iterable} coords * @param {BooleanMode} mode * @param {StyleParam} [style] * @param {string} [content] - SVG content to embed in voxel * @param {boolean} [opaque] - Whether voxels occlude neighbors (default true) * @param {Object} [meta] - Arbitrary key-value pairs for data-* attributes * @param {[number,number,number]|function(number,number,number): [number,number,number]} [scale] - Per-axis scale 0-1 * @param {[number,number,number]|function(number,number,number): [number,number,number]} [scaleOrigin] - Scale origin within voxel * @param {number} [gap] - Per-geometry gap override (undefined = use defaultGap) */ _applyOp(coords: Iterable, mode: BooleanMode, style?: StyleParam, content?: string, opaque?: boolean, meta?: any, scale?: [number, number, number] | ((arg0: number, arg1: number, arg2: number) => [number, number, number]), scaleOrigin?: [number, number, number] | ((arg0: number, arg1: number, arg2: number) => [number, number, number]), gap?: number): void; /** * Resolves a style parameter (which might be a function) into a static style object. * @param {StyleParam} styleParam * @param {number} x * @param {number} y * @param {number} z * @param {Object|null} [existingStyles] - Existing per-face styles to merge into * @returns {Object} Resolved per-face style map (keys: 'default', 'top', etc.) */ _resolveStyles(styleParam: StyleParam, x: number, y: number, z: number, existingStyles?: any | null): any; /** Remove all voxels. */ clear(): void; /** * Get voxel data at a position. * @param {[number,number,number]} pos * @returns {Object|null} */ getVoxel(pos: [number, number, number]): any | null; /** * Check if a voxel exists at a position. * @param {[number,number,number]} pos * @returns {boolean} */ hasVoxel(pos: [number, number, number]): boolean; /** * Get the six axis-aligned neighbors of a position. * @param {[number,number,number]} pos * @returns {{top:Object|null, bottom:Object|null, left:Object|null, right:Object|null, front:Object|null, back:Object|null}} */ getNeighbors(pos: [number, number, number]): { top: any | null; bottom: any | null; left: any | null; right: any | null; front: any | null; back: any | null; }; /** * Find all voxels matching a predicate. * @param {function(Voxel): boolean} predicate * @returns {Voxel[]} * @example * // Find by meta ID * engine.findVoxels(v => v.meta?.id === 'tower') * // Find all voxels at a specific Y level * engine.findVoxels(v => v.y === 0) */ findVoxels(predicate: (arg0: Voxel) => boolean): Voxel[]; /** * Serialize the scene to a plain JSON-safe object. * Functional styles are omitted with a console warning. * @returns {Object} */ toJSON(): any; /** * Resolve geometry type to a coordinate iterator. * @param {Object} opts * @returns {Iterable} */ _resolveGeometry(opts: any): Iterable; /** * Register a named decal. Content must be one or more elements * authored in a 0–1 unit coordinate space. All path commands are supported * (M, L, H, V, C, S, Q, T, A, Z). Other SVG shapes (circle, rect, etc.) * must be converted to first. * @param {string} name - Unique decal name * @param {DecalDef|string} def - Decal definition object, or raw SVG string */ defineDecal(name: string, def: DecalDef | string): void; /** * Apply a geometry operation (union, subtract, intersect, exclude). * @param {Object} opts * @param {'box'|'sphere'|'line'|'fill'} opts.type - Geometry type * @param {BooleanMode} [opts.mode='union'] - Boolean operation * @param {StyleParam} [opts.style] - Per-face styles * @param {string} [opts.content] - SVG content to render instead of polygon faces * @param {boolean} [opts.opaque=true] - Whether voxels occlude neighbors * @param {Object} [opts.meta] - Key/value pairs emitted as data-* attributes * @param {RotateOptions} [opts.rotate] - Rotate coordinates before placement * @param {[number,number,number]|function(number,number,number): [number,number,number]} [opts.scale] - Per-axis scale 0-1 * @param {[number,number,number]|function(number,number,number): [number,number,number]} [opts.scaleOrigin=[0.5,0,0.5]] - Scale origin * @param {number} [opts.gap] - Gap between voxels (0–<0.5). Overrides constructor default. * * Box params: position, size * Sphere params: center, radius * Line params: from, to, radius, shape * Fill params: bounds, test */ applyGeometry(opts: { type: "box" | "sphere" | "line" | "fill"; mode?: BooleanMode; style?: StyleParam; content?: string; opaque?: boolean; meta?: any; rotate?: RotateOptions; scale?: [number, number, number] | ((arg0: number, arg1: number, arg2: number) => [number, number, number]); scaleOrigin?: [number, number, number] | ((arg0: number, arg1: number, arg2: number) => [number, number, number]); gap?: number; }): void; /** * Remove geometry (shortcut for applyGeometry with mode: 'subtract'). * @param {Object} opts - Same as applyGeometry (mode is forced to 'subtract') */ removeGeometry(opts: any): void; /** * Add geometry (shortcut for applyGeometry with mode: 'union'). * @param {Object} opts - Same as applyGeometry (mode is forced to 'union') */ addGeometry(opts: any): void; /** * Restyle existing voxels matching a geometry selection, or all voxels if no type given. * @param {Object} opts * @param {'box'|'sphere'|'line'|'fill'} [opts.type] - Geometry type (omit to style all voxels) * @param {StyleParam} opts.style - Style to apply * * Box params: position, size * Sphere params: center, radius * Line params: from, to, radius, shape * Fill params: bounds, test */ applyStyle(opts: { type?: "box" | "sphere" | "line" | "fill"; style: StyleParam; }): void; /** * Build all neighbor-exposed 3D faces for every voxel, using the incremental * per-voxel cache. Emits up to 6 faces per voxel. The `back` face (normal * [0,0,1]) is included so that orthographic/isometric cameras can see it when * rotated past ~90°; oblique always culls it via `cullTypes`. * * Each face carries `n` (normal) and `c` (center) so that `_projectAndSort` * can do backface culling and depth computation without extra lookups. * * @param {Set|null} [cullTypes] - Optional set of face type strings to * skip at generation time (e.g. oblique direction cull). When provided the * per-voxel incremental cache is bypassed so that the cache always stores * the full unculled set. * @returns {Object[]} Raw 3D face objects */ _buildFaces3D(cullTypes?: Set | null): any[]; /** * Generate faces from stored voxels. * * By default returns projected, depth-sorted 2D faces for SVG rendering. * Pass `{ raw: true }` to get all neighbour-exposed 3D faces without any * camera-dependent culling or projection — the correct input for * GPURenderer, which lets the GPU handle its own backface culling. * * Both modes are epoch-cached: repeated calls with no scene changes are free. * * @param {Object} [options] * @param {boolean} [options.raw=false] - Return raw 3D faces instead of projected 2D faces. * @returns {Face[]} */ getFaces(options?: { raw?: boolean; }): Face[]; /** * Generate faces from a test function without storing any voxels. * Zero Map allocations — useful for procedural/infinite scenes. * @param {Object} opts * @param {[[number,number,number],[number,number,number]]} [opts.bounds] - Single scan region * @param {Array<[[number,number,number],[number,number,number]]>} [opts.regions] - Multiple scan regions (auto-deduped) * @param {function(number,number,number): boolean} opts.test - Inclusion test * @param {StyleParam|function(number,number,number,string): StyleObject} [opts.style] - Style per voxel or per face * @param {number} [opts.gap] - Gap override (defaults to constructor gap) * @returns {Face[]} */ renderTest(opts: { bounds?: [[number, number, number], [number, number, number]]; regions?: Array<[[number, number, number], [number, number, number]]>; test: (arg0: number, arg1: number, arg2: number) => boolean; style?: StyleParam | ((arg0: number, arg1: number, arg2: number, arg3: string) => StyleObject); gap?: number; }): Face[]; /** * Project a single 3D point to 2D pixel space using the current camera. * @param {number} x * @param {number} y * @param {number} z * @returns {{x: number, y: number}} */ _projectPoint(x: number, y: number, z: number): { x: number; y: number; }; /** * Project 3D faces to 2D and sort by depth (shared by getFaces and renderTest). * @param {Object[]} faces3D - Face objects with `vertices` (3D) or `points` (already 2D) * @returns {Face[]} Projected, depth-sorted face array */ _projectAndSort(faces3D: any[]): Face[]; /** * Get the 2D bounding box of rendered faces, with optional padding. * @param {number} [padding=0] - Padding to add around the bounds * @param {Face[]} [faces] - Pre-computed faces. Uses stored voxels if omitted. * @returns {{x: number, y: number, w: number, h: number, faces: Face[]}} */ getBounds(padding?: number, faces?: Face[]): { x: number; y: number; w: number; h: number; faces: Face[]; }; /** * Query position and size data for a single voxel. * * Accepts either a `[x, y, z]` coordinate array or a voxel object reference * (e.g. one returned by `getVoxel()`, `findVoxels()`, or `face.voxel`). * * All 2D values are in the same pixel space as `getFaces()` / `getBounds()` — * i.e. before any SVG viewBox offset or padding is applied. * * @param {[number,number,number]|Voxel} posOrVoxel - Coordinate array or voxel reference * @returns {{ * voxel: Voxel|null, * center3D: [number,number,number], * center2D: {x:number, y:number}, * bounds2D: {x:number, y:number, w:number, h:number}|null, * normalizedCenter2D: {x:number, y:number}|null, * normalizedSize2D: {w:number, h:number}|null, * }} */ getVoxelInfo(posOrVoxel: [number, number, number] | Voxel): { voxel: Voxel | null; center3D: [number, number, number]; center2D: { x: number; y: number; }; bounds2D: { x: number; y: number; w: number; h: number; } | null; normalizedCenter2D: { x: number; y: number; } | null; normalizedSize2D: { w: number; h: number; } | null; }; /** * Find the frontmost voxel at a 2D screen-space position. * * Coordinates are expected in the same raw pixel space that `getFaces()` and * `getBounds()` use — i.e. before any SVG viewBox offset or padding. If you * are working from a mouse event on a rendered SVG, pass the viewBox origin * via `options.offset` to convert automatically: * * ```js * const bounds = h.getBounds(padding) * const hit = h.findByPosition([svgX, svgY], { offset: [bounds.x, bounds.y] }) * ``` * * @param {[number, number]} pos - 2D position [x, y] in screen/SVG space * @param {Object} [options] * @param {[number, number]} [options.offset] - [dx, dy] added to pos before testing (e.g. viewBox origin from getBounds()) * @returns {{ voxel: Voxel, face: Face } | null} */ findByPosition(pos: [number, number], options?: { offset?: [number, number]; }): { voxel: Voxel; face: Face; } | null; /** * Render the scene (or pre-computed faces) to an SVG string. * @param {Object} [options] * @param {number} [options.padding=20] - ViewBox padding in pixels * @param {Face[]} [options.faces] - Pre-computed faces (skips internal `getFaces()`) * @param {[number,number,number,number]} [options.viewBox] - Custom viewBox override [x, y, w, h] * @param {[number,number]} [options.offset=[0,0]] - Translate all geometry * @param {string} [options.prepend] - Raw SVG to insert before faces * @param {string} [options.append] - Raw SVG to insert after faces * @param {function(Face): Object|null} [options.faceAttributes] - Per-face attribute callback * @param {boolean} [options.occlusion=false] - Enable built-in occlusion culling * @param {function(number[][], number[][][]): string|null} [options.resolveOcclusion] - Custom occlusion resolver (overrides built-in). Providing this implicitly enables occlusion. * @returns {string} SVG markup */ toSVG(options?: { padding?: number; faces?: Face[]; viewBox?: [number, number, number, number]; offset?: [number, number]; prepend?: string; append?: string; faceAttributes?: (arg0: Face) => any | null; occlusion?: boolean; resolveOcclusion?: (arg0: number[][], arg1: number[][][]) => string | null; }): string; /** * Iterate over all voxels. Supports `for (const voxel of heerich)`. * @returns {Iterator} */ [Symbol.iterator](): Iterator; } export type StyleObject = { /** * - Fill color */ fill?: string; /** * - Stroke color */ stroke?: string; /** * - Stroke width */ strokeWidth?: number; /** * - Overall opacity */ opacity?: number; /** * - Fill opacity */ fillOpacity?: number; /** * - Stroke opacity */ strokeOpacity?: number; /** * - Dash pattern */ strokeDasharray?: string; /** * - Line cap style */ strokeLinecap?: string; /** * - Line join style */ strokeLinejoin?: string; /** * - Decal name or decal reference object */ decal?: string | DecalRef; /** * - Hatching lines drawn over the face */ hatch?: import("./hatch.js").HatchOptions; }; export type DecalRef = { /** * - Decal name (as registered via defineDecal) */ name: string; /** * - Style overrides applied to the element */ style?: any; }; export type DecalDef = { /** * - SVG markup containing one or more elements in 0–1 unit space. * Only elements are supported — other shapes (circle, rect, etc.) must be converted to paths first. */ content: string; }; export type FaceStyleMap = { default?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); top?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); bottom?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); left?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); right?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); front?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); back?: StyleObject | ((arg0: number, arg1: number, arg2: number) => StyleObject); }; /** * Per-face style map or a function that returns one. * Values can be StyleObjects or `(x,y,z) => StyleObject` callbacks. */ export type StyleParam = FaceStyleMap | ((arg0: number, arg1: number, arg2: number) => FaceStyleMap); export type BooleanMode = "union" | "subtract" | "intersect" | "exclude"; export type RotateOptions = { /** * - Rotation axis */ axis: "x" | "y" | "z"; /** * - Number of 90-degree turns (1-3) */ turns: number; /** * - Rotation center (defaults to bounding-box center) */ center?: [number, number, number]; }; export type Voxel = { x: number; y: number; z: number; /** * - Per-face resolved styles */ styles?: any; /** * - SVG content to embed */ content?: string; /** * - Whether this voxel occludes neighbors (default true) */ opaque?: boolean; /** * - Arbitrary key-value pairs for data-* attributes */ meta?: any; /** * - Per-axis scale factors [sx, sy, sz] (0-1) */ scale?: [number, number, number]; /** * - Scale transform origin within voxel */ scaleOrigin?: [number, number, number]; }; export type FaceType = "top" | "bottom" | "left" | "right" | "front" | "back" | "content"; export type CameraOptions = { /** * - Projection type */ type?: "oblique" | "perspective" | "orthographic" | "isometric"; /** * - Oblique: depth axis direction. Orthographic/isometric: horizontal rotation (pan). */ angle?: number; /** * - Orthographic only: vertical tilt in degrees */ pitch?: number; /** * - Oblique/perspective: camera distance */ distance?: number; /** * - Perspective camera position [x, y] */ position?: [number, number]; }; export type Face = { /** * - Face name */ type: FaceType; /** * - Source voxel data */ voxel: Voxel; /** * - Projected 2D polygon points */ points: import("./points.js").Points; /** * - Depth value for sorting */ depth: number; /** * - Resolved style for this face */ style?: StyleObject; /** * - 3D polygon vertices (before projection) */ vertices?: [number, number, number][]; /** * - Face normal vector (perspective/orthographic only) */ n?: [number, number, number]; /** * - Face center point in 3D (perspective/orthographic only) */ c?: [number, number, number]; /** * - SVG content string (content faces only) */ content?: string; /** * - Original 3D position (content faces only) */ _pos?: [number, number, number]; /** * - Projected 2D x (content faces only) */ _px?: number; /** * - Projected 2D y (content faces only) */ _py?: number; /** * - Perspective scale factor (content faces only) */ _scale?: number; }; import { boxCoords } from "./shapes.js"; import { sphereCoords } from "./shapes.js"; import { lineCoords } from "./shapes.js"; import { fillCoords } from "./shapes.js"; import { SVGRenderer } from "./svg-renderer.js"; export { boxCoords, sphereCoords, lineCoords, fillCoords };