@glyphcss/compile 0.0.9

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Layoutit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @glyphcss/compile
2
+
3
+ Compile 3D meshes to **static glyphcss ASCII at build time** — a Vite plugin, a
4
+ CLI, and a Node API. Because glyphcss renders to a single `<pre>` of text, a
5
+ scene can be rendered ahead of time and inlined into HTML with **zero runtime
6
+ JS**. Defaults match the glyphcss library exactly, so a compiled scene is
7
+ byte-identical to what the runtime would render for the same inputs.
8
+
9
+ ## Vite plugin
10
+
11
+ ```ts
12
+ // vite.config.ts
13
+ import { glyphcssCompile } from "@glyphcss/compile/vite";
14
+ export default { plugins: [glyphcssCompile()] };
15
+ ```
16
+
17
+ ```ts
18
+ // import a mesh with `?glyph` → the build-time-rendered <pre> string
19
+ import dog from "./dog.glb?glyph&autoCenter=1&rotX=60&rotY=45&zoom=0.5&cols=80&rows=30";
20
+ document.querySelector("#app").innerHTML = dog; // no runtime, no WebGL
21
+ ```
22
+
23
+ Works in any Vite pipeline — Astro, vanilla Vite, Vite-React (import the string
24
+ and inject it). Query params map to the options below.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ npm i -g @glyphcss/compile # global `glyphcss` command
30
+ # or run ad hoc:
31
+ npx @glyphcss/compile cube --auto-center
32
+ # or as a build dep (Vite plugin / Node API):
33
+ npm i -D @glyphcss/compile
34
+ ```
35
+
36
+ It pulls in `glyphcss` + `@glyphcss/core` automatically. Ships a
37
+ [**skill**](./SKILL.md) (`SKILL.md`) so coding agents know how to drive the CLI.
38
+
39
+ ## CLI
40
+
41
+ ```sh
42
+ glyphcss cube --auto-center # a primitive shape → color ASCII
43
+ glyphcss dog.glb --auto-center # a mesh file → ANSI color in the terminal
44
+ glyphcss --polygons-json '[{"vertices":[[0,0,0],[2,0,0],[1,2,0]],"color":"#f00"}]'
45
+ glyphcss dog.glb -f text # plain ASCII
46
+ glyphcss dog.glb -f full -o dog.html # full HTML document
47
+ glyphcss dog.glb --fit 60 -f text # fit width to 60 columns
48
+ glyphcss model.obj --mtl other.mtl # explicit OBJ material override
49
+ ```
50
+
51
+ Input is a **mesh file** (`.obj/.glb/.gltf/.vox/.stl`), a **primitive shape** name
52
+ (`cube`, `sphere`, `icosahedron`, `torus`, `cone`, … — 44 shapes), or **custom
53
+ polygons** (`--polygons FILE.json` / `--polygons-json '…'`, an array of
54
+ `{ vertices, color? }`).
55
+
56
+ Output **`-f, --format`**: `ansi` (truecolor terminal), `text` (plain), `html`
57
+ (a `<pre>`), or `full` (HTML doc). The default picks by destination — **terminal →
58
+ ansi**, `-o` file → html, piped → text. Omit `--cols`/`--rows` and it auto-fits
59
+ the grid + zoom to the model, cropped tight (give just one and the other adapts).
60
+ The universal escape hatch — works in any pipeline (Hugo, Eleventy, CI, a Makefile).
61
+
62
+ ## Node API
63
+
64
+ ```ts
65
+ import { compileFile, compileScene, loadMeshFromFile } from "@glyphcss/compile";
66
+
67
+ const { html, inner, cols, rows } = await compileFile("dog.glb", { autoCenter: true });
68
+ // or, with polygons you already have:
69
+ const { html } = compileScene({ polygons, cols: 80, rows: 24 }); // pure, no DOM
70
+ ```
71
+
72
+ ## Options (query params / CLI flags / `CompileFileOptions`)
73
+
74
+ | Option | Query / flag | Default (library) |
75
+ |---|---|---|
76
+ | Camera angle | `rotX` `rotY` / `--rot-x` `--rot-y` | 65 / 45 |
77
+ | Zoom | `zoom` / `--zoom` | 0.3 |
78
+ | Projection | `projection=orthographic` / `--ortho` | perspective |
79
+ | Grid | `cols` `rows` `cellAspect` / `--cols` … | 80 / 24 / 2.0 |
80
+ | Render mode | `mode` / `--mode` | solid |
81
+ | Palette | `palette` / `--palette` | default |
82
+ | Colors | `colors=0` / `--no-colors` | on |
83
+ | Recenter mesh | `autoCenter=1` / `--auto-center` | off |
84
+ | Mesh optimize | `meshResolution` / `--mesh-resolution` | lossy |
85
+
86
+ > Defaults are the **library** defaults (`createGlyphScene`). A loaded mesh is
87
+ > not recentered or auto-fit unless you ask — pass `autoCenter` + a camera/zoom
88
+ > to frame a model, the same as `<glyph-mesh>` in the runtime.
89
+
90
+ ## Interactive export
91
+
92
+ Declare the interactions you want; only those ship. The manifest drives both the
93
+ wired control (the snippet imports just that one) and the decimation budget
94
+ (coarser for orbit, finer when `zoom`/`fpv` let the camera get close):
95
+
96
+ ```sh
97
+ glyphcss dog.glb --interactions orbit,zoom --auto-center --full -o dog.html
98
+ ```
99
+
100
+ ```ts
101
+ import { compileInteractive, toCodepenPrefill } from "@glyphcss/compile";
102
+
103
+ const r = await compileInteractive("dog.glb", { interactions: ["orbit", "zoom"], autoCenter: true });
104
+ r.html; // self-contained: <div> + <script type=module> (glyphcss from CDN, mesh inlined)
105
+ r.polygonCount; // triangles shipped after decimation (vs r.sourcePolygonCount)
106
+ toCodepenPrefill(r); // → { action, data } to POST a new CodePen
107
+ ```
108
+
109
+ The pure, browser-safe builder is `buildGlyphInteractiveExport(polygons, opts)` in
110
+ `glyphcss` — the gallery's "CodePen" button calls it directly on the loaded mesh.
111
+
112
+ | `interactions` | Ships |
113
+ |---|---|
114
+ | `[]` | static scene, no control |
115
+ | `["orbit"]` | orbit control, coarse decimation |
116
+ | `["orbit","zoom"]` | + wheel zoom, finer decimation |
117
+ | `["pan","zoom"]` | map controls |
118
+ | `["fpv"]` | first-person controls, finest decimation |
119
+
120
+ ## Notes
121
+
122
+ - **Textures**: the CLI / Node API decode PNG/JPG and bake each face to its
123
+ sampled color, so external (OBJ + `.mtl`) textures render in **true color** in
124
+ the terminal. GLB-embedded textures fall back to material/vertex colors. The
125
+ pure `compileScene` stays DOM-free — decoding lives in `loadMeshFromFile`.
package/SKILL.md ADDED
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: glyphcss
3
+ description: Render a 3D mesh, a primitive shape (cube, sphere, icosahedron, torus, cone…), or custom polygons to ASCII art — in the terminal (truecolor ANSI) or as HTML. Use when asked to render, preview, or visualize a 3D model or shape as text/ASCII.
4
+ ---
5
+
6
+ # glyphcss — render 3D to ASCII
7
+
8
+ `glyphcss` is a CLI that compiles 3D geometry to ASCII and prints it. In a
9
+ terminal it defaults to **truecolor ANSI**, so the output shows in color directly.
10
+
11
+ Install once: `npm i -g @glyphcss/compile` (the command is `glyphcss`). Or run ad
12
+ hoc with `npx @glyphcss/compile <args>`.
13
+
14
+ ## Render a primitive shape
15
+
16
+ ```sh
17
+ glyphcss cube --auto-center # color ASCII cube in the terminal
18
+ glyphcss sphere --auto-center
19
+ glyphcss icosahedron --rot-x 60 --rot-y 30 --auto-center
20
+ ```
21
+
22
+ A bare name with no file extension is treated as a shape. Available shapes:
23
+ `tetrahedron cube octahedron dodecahedron icosahedron sphere cylinder cone torus
24
+ pyramid prism antiprism bipyramid trapezohedron cuboctahedron icosidodecahedron`
25
+ and the Archimedean/Catalan/Kepler-Poinsot solids (truncated*, snub*, rhombic*,
26
+ triakis*, deltoidal*, great*/small* …).
27
+
28
+ ## Render a mesh file
29
+
30
+ ```sh
31
+ glyphcss model.obj --auto-center # .obj/.glb/.gltf/.vox/.stl
32
+ glyphcss model.glb -f full -o out.html # write an HTML document
33
+ ```
34
+ OBJ companion `.mtl` is auto-detected (or `--mtl FILE`); PNG/JPG textures are
35
+ decoded and baked to per-face color.
36
+
37
+ ## Render custom polygons
38
+
39
+ ```sh
40
+ glyphcss --polygons-json '[{"vertices":[[0,0,0],[2,0,0],[1,2,0]],"color":"#ff0000"}]'
41
+ glyphcss --polygons shape.json --auto-center
42
+ ```
43
+ JSON is an array (or `{ "polygons": [...] }`) of `{ vertices: [[x,y,z] ×3+], color?: "#rgb" }`.
44
+
45
+ ## Key options
46
+
47
+ - **Output** `-f, --format` `ansi` | `text` | `html` | `full` (default: terminal→ansi, `-o` file→html, pipe→text)
48
+ - **Frame** `--auto-center` recenters; with no `--cols`/`--rows` it auto-fits the grid to the content (or `--fit N` cols; give one of `--cols`/`--rows` and the other adapts)
49
+ - **Camera** `--rot-x N` `--rot-y N` `--zoom N` `--ortho`
50
+ - **Render** `--mode solid|wireframe|voxel` `--palette default|ascii|blocks|…` `--no-colors`
51
+ - **Output to file** `-o FILE`
52
+ - `glyphcss --help` lists everything.
53
+
54
+ ## Recipe: show a shape in the console
55
+
56
+ ```sh
57
+ glyphcss cube --auto-center
58
+ ```
59
+ That prints a centered, colored ASCII cube. Swap `cube` for any shape, a file, or
60
+ `--polygons-json`. Use `-f text` for plain (no color), `-f full -o x.html` for a page.
package/dist/cli.cjs ADDED
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_promises3 = require("fs/promises");
28
+
29
+ // src/compileFile.ts
30
+ var import_glyphcss = require("glyphcss");
31
+
32
+ // src/loadMeshFromFile.ts
33
+ var import_promises2 = require("fs/promises");
34
+ var import_core = require("@glyphcss/core");
35
+
36
+ // src/textureBakeNode.ts
37
+ var import_promises = require("fs/promises");
38
+ var import_pngjs = require("pngjs");
39
+ var import_jpeg_js = __toESM(require("jpeg-js"), 1);
40
+ function stripQuery(url) {
41
+ return url.split("?")[0].split("#")[0];
42
+ }
43
+ function toFilePath(url) {
44
+ const clean = stripQuery(url);
45
+ if (/^https?:\/\//i.test(clean)) {
46
+ try {
47
+ return decodeURIComponent(new URL(clean).pathname);
48
+ } catch {
49
+ }
50
+ }
51
+ if (clean.startsWith("file://")) {
52
+ try {
53
+ return decodeURIComponent(new URL(clean).pathname);
54
+ } catch {
55
+ }
56
+ }
57
+ return clean;
58
+ }
59
+ async function decode(path) {
60
+ const fp = toFilePath(path);
61
+ try {
62
+ const buf = await (0, import_promises.readFile)(fp);
63
+ if (/\.png$/i.test(fp)) {
64
+ const p = import_pngjs.PNG.sync.read(buf);
65
+ return { w: p.width, h: p.height, data: p.data };
66
+ }
67
+ if (/\.jpe?g$/i.test(fp)) {
68
+ const j = import_jpeg_js.default.decode(buf, { useTArray: true });
69
+ return { w: j.width, h: j.height, data: j.data };
70
+ }
71
+ } catch {
72
+ }
73
+ return null;
74
+ }
75
+ function texelAt(img, u, v) {
76
+ const x = Math.min(img.w - 1, Math.max(0, Math.round(u * (img.w - 1))));
77
+ const y = Math.min(img.h - 1, Math.max(0, Math.round((1 - v) * (img.h - 1))));
78
+ const i = (y * img.w + x) * 4;
79
+ return [img.data[i], img.data[i + 1], img.data[i + 2]];
80
+ }
81
+ function sampleFace(img, uvs) {
82
+ const pts = [uvs[0], uvs[1], uvs[2], [(uvs[0][0] + uvs[1][0] + uvs[2][0]) / 3, (uvs[0][1] + uvs[1][1] + uvs[2][1]) / 3]];
83
+ let r = 0, g = 0, b = 0;
84
+ for (const [u, v] of pts) {
85
+ const c = texelAt(img, u, v);
86
+ r += c[0];
87
+ g += c[1];
88
+ b += c[2];
89
+ }
90
+ const n = pts.length;
91
+ const h = (x) => Math.round(x / n).toString(16).padStart(2, "0");
92
+ return `#${h(r)}${h(g)}${h(b)}`;
93
+ }
94
+ async function bakeTexturesNode(polygons) {
95
+ const cache = /* @__PURE__ */ new Map();
96
+ const get = async (url) => {
97
+ if (!cache.has(url)) cache.set(url, await decode(url));
98
+ return cache.get(url) ?? null;
99
+ };
100
+ const out = [];
101
+ for (const p of polygons) {
102
+ const tt = p.textureTriangles?.[0];
103
+ const tex = p.texture ?? p.material?.texture ?? tt?.texture;
104
+ const uvs = tt?.uvs ?? (p.uvs && p.uvs.length >= 3 ? [p.uvs[0], p.uvs[1], p.uvs[2]] : void 0);
105
+ if (tex && uvs) {
106
+ const img = await get(tex);
107
+ if (img) {
108
+ out.push({ ...p, color: sampleFace(img, uvs), texture: void 0, textureTriangles: void 0, uvs: void 0 });
109
+ continue;
110
+ }
111
+ }
112
+ out.push(p);
113
+ }
114
+ return out;
115
+ }
116
+ function hasTextures(polygons) {
117
+ return polygons.some((p) => p.texture || p.material?.texture || p.textureTriangles?.[0]?.texture);
118
+ }
119
+
120
+ // src/loadMeshFromFile.ts
121
+ function stripQuery2(url) {
122
+ return url.split("?")[0].split("#")[0];
123
+ }
124
+ async function siblingMtl(objPath) {
125
+ const clean = stripQuery2(objPath);
126
+ const dir = clean.replace(/[^/\\]+$/, "");
127
+ let candidate;
128
+ try {
129
+ const m = (await (0, import_promises2.readFile)(clean, "utf8")).match(/^\s*mtllib\s+(.+?)\s*$/im);
130
+ candidate = m ? dir + m[1].trim() : clean.replace(/\.obj$/i, ".mtl");
131
+ } catch {
132
+ candidate = clean.replace(/\.obj$/i, ".mtl");
133
+ }
134
+ try {
135
+ await (0, import_promises2.readFile)(candidate);
136
+ return candidate;
137
+ } catch {
138
+ return void 0;
139
+ }
140
+ }
141
+ async function fileFetch(url) {
142
+ const path = stripQuery2(url);
143
+ try {
144
+ const buf = await (0, import_promises2.readFile)(path);
145
+ const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
146
+ return {
147
+ ok: true,
148
+ status: 200,
149
+ text: async () => buf.toString("utf8"),
150
+ arrayBuffer: async () => ab
151
+ };
152
+ } catch {
153
+ return { ok: false, status: 404, text: async () => "", arrayBuffer: async () => new ArrayBuffer(0) };
154
+ }
155
+ }
156
+ async function loadMeshFromFile(path, options) {
157
+ const g = globalThis;
158
+ const prev = g.fetch;
159
+ g.fetch = fileFetch;
160
+ let result;
161
+ try {
162
+ const mtlUrl = options?.mtlUrl ?? (/\.obj(\?|$)/i.test(path) ? await siblingMtl(path) : void 0);
163
+ result = await (0, import_core.loadMesh)(path, { solidTextureSamples: false, ...options, mtlUrl });
164
+ } finally {
165
+ g.fetch = prev;
166
+ }
167
+ if (hasTextures(result.polygons)) {
168
+ return { ...result, polygons: await bakeTexturesNode(result.polygons) };
169
+ }
170
+ return result;
171
+ }
172
+
173
+ // src/compileFile.ts
174
+ function worldMaxDim(polys) {
175
+ let mnx = Infinity, mxx = -Infinity, mny = Infinity, mxy = -Infinity, mnz = Infinity, mxz = -Infinity;
176
+ for (const p of polys) for (const v of p.vertices) {
177
+ if (v[0] < mnx) mnx = v[0];
178
+ if (v[0] > mxx) mxx = v[0];
179
+ if (v[1] < mny) mny = v[1];
180
+ if (v[1] > mxy) mxy = v[1];
181
+ if (v[2] < mnz) mnz = v[2];
182
+ if (v[2] > mxz) mxz = v[2];
183
+ }
184
+ if (!isFinite(mnx)) return 1;
185
+ return Math.max(mxx - mnx, mxy - mny, mxz - mnz, 1e-6);
186
+ }
187
+ function measureContent(inner) {
188
+ const lines = inner.replace(/<[^>]*>/g, "").split("\n");
189
+ let minC = Infinity, maxC = -1, minR = Infinity, maxR = -1;
190
+ lines.forEach((l, r) => {
191
+ for (let c = 0; c < l.length; c++) if (l[c] !== " ") {
192
+ if (c < minC) minC = c;
193
+ if (c > maxC) maxC = c;
194
+ if (r < minR) minR = r;
195
+ if (r > maxR) maxR = r;
196
+ }
197
+ });
198
+ return maxC < 0 ? { w: 0, h: 0 } : { w: maxC - minC + 1, h: maxR - minR + 1 };
199
+ }
200
+ function compilePolygons(polygons, options = {}) {
201
+ const buildCam = (zoom) => options.projection === "orthographic" ? (0, import_glyphcss.createGlyphOrthographicCamera)({ rotX: options.rotX, rotY: options.rotY, zoom }) : (0, import_glyphcss.createGlyphPerspectiveCamera)({ rotX: options.rotX, rotY: options.rotY, zoom, distance: options.distance, perspective: options.perspective });
202
+ const shared = {
203
+ autoCenter: options.autoCenter,
204
+ cellAspect: options.cellAspect,
205
+ mode: options.mode,
206
+ glyphPalette: options.glyphPalette,
207
+ useColors: options.useColors,
208
+ smoothShading: options.smoothShading,
209
+ creaseAngle: options.creaseAngle,
210
+ doubleSided: options.doubleSided,
211
+ supersample: options.supersample
212
+ };
213
+ if (options.autoFit && options.autoFit.target > 0) {
214
+ const { target, by } = options.autoFit;
215
+ const probeZoom = 40 / worldMaxDim(polygons);
216
+ const probe = (0, import_glyphcss.compileScene)({ polygons, camera: buildCam(probeZoom), cols: 200, rows: 120, ...shared, autoCenter: true });
217
+ const m = measureContent(probe.inner);
218
+ if (m.w > 0 && m.h > 0) {
219
+ const scale = by === "rows" ? target / m.h : target / m.w;
220
+ const zoom = probeZoom * scale;
221
+ const cols = Math.ceil(m.w * scale * 1.4) + 6;
222
+ const rows = Math.ceil(m.h * scale * 1.4) + 6;
223
+ const full = (0, import_glyphcss.compileScene)({ polygons, camera: buildCam(zoom), cols, rows, ...shared, autoCenter: true });
224
+ const inner = (0, import_glyphcss.cropGlyphInner)(full.inner);
225
+ const lines = inner.split("\n");
226
+ const w = lines.reduce((a, l) => Math.max(a, l.replace(/<[^>]*>/g, "").length), 0);
227
+ return { html: `<pre class="glyph-output">${inner}</pre>`, inner, cols: w, rows: lines.length, cellAspect: options.cellAspect ?? 2 };
228
+ }
229
+ }
230
+ return (0, import_glyphcss.compileScene)({
231
+ polygons,
232
+ camera: buildCam(options.zoom),
233
+ cols: options.cols,
234
+ rows: options.rows,
235
+ ...shared
236
+ });
237
+ }
238
+ async function compileFile(path, options = {}) {
239
+ const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
240
+ return compilePolygons(polygons, options);
241
+ }
242
+
243
+ // src/compileInteractive.ts
244
+ var import_glyphcss2 = require("glyphcss");
245
+ async function compileInteractive(path, options = {}) {
246
+ const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
247
+ return (0, import_glyphcss2.buildGlyphInteractiveExport)(polygons, options);
248
+ }
249
+
250
+ // src/cli.ts
251
+ var import_glyphcss3 = require("glyphcss");
252
+ var import_core2 = require("@glyphcss/core");
253
+ var MESH_EXT = /\.(obj|glb|gltf|vox|stl)$/i;
254
+ function normalizePolygons(data) {
255
+ const arr = Array.isArray(data) ? data : data?.polygons;
256
+ if (!Array.isArray(arr)) throw new Error("polygons JSON must be an array or { polygons: [...] }");
257
+ return arr.map((p, i) => {
258
+ const vertices = p.vertices ?? p.v;
259
+ if (!Array.isArray(vertices) || vertices.length < 3) throw new Error(`polygon ${i}: needs vertices [[x,y,z], \u2026] (>= 3)`);
260
+ return { vertices, color: p.color ?? p.c };
261
+ });
262
+ }
263
+ function parseArgs(argv) {
264
+ const opts = {};
265
+ let file;
266
+ let out;
267
+ let format;
268
+ let fit;
269
+ let shape;
270
+ let polygonsFile;
271
+ let polygonsJson;
272
+ let interactive = false;
273
+ let interactions;
274
+ let decimateGrid;
275
+ let cdnVersion;
276
+ const num = (v) => {
277
+ const n = Number(v);
278
+ return v !== void 0 && Number.isFinite(n) ? n : void 0;
279
+ };
280
+ for (let i = 0; i < argv.length; i++) {
281
+ const a = argv[i];
282
+ const next = () => argv[++i];
283
+ switch (a) {
284
+ case "--rot-x":
285
+ opts.rotX = num(next());
286
+ break;
287
+ case "--rot-y":
288
+ opts.rotY = num(next());
289
+ break;
290
+ case "--zoom":
291
+ opts.zoom = num(next());
292
+ break;
293
+ case "--distance":
294
+ opts.distance = num(next());
295
+ break;
296
+ case "--perspective":
297
+ opts.perspective = num(next());
298
+ break;
299
+ case "--cols":
300
+ opts.cols = num(next());
301
+ break;
302
+ case "--rows":
303
+ opts.rows = num(next());
304
+ break;
305
+ case "--cell-aspect":
306
+ opts.cellAspect = num(next());
307
+ break;
308
+ case "--crease-angle":
309
+ opts.creaseAngle = num(next());
310
+ break;
311
+ case "--supersample":
312
+ opts.supersample = num(next());
313
+ break;
314
+ case "--mode":
315
+ opts.mode = next();
316
+ break;
317
+ case "--palette":
318
+ opts.glyphPalette = next();
319
+ break;
320
+ case "--mesh-resolution":
321
+ opts.meshResolution = next();
322
+ break;
323
+ case "--mtl":
324
+ opts.mtlUrl = next();
325
+ break;
326
+ // Geometry input (instead of a mesh file).
327
+ case "--shape":
328
+ shape = next();
329
+ break;
330
+ case "--polygons":
331
+ polygonsFile = next();
332
+ break;
333
+ case "--polygons-json":
334
+ polygonsJson = next();
335
+ break;
336
+ case "--ortho":
337
+ opts.projection = "orthographic";
338
+ break;
339
+ case "--auto-center":
340
+ opts.autoCenter = true;
341
+ break;
342
+ case "--smooth":
343
+ opts.smoothShading = true;
344
+ break;
345
+ case "--double-sided":
346
+ opts.doubleSided = true;
347
+ break;
348
+ case "--fit":
349
+ fit = num(next());
350
+ break;
351
+ // Output format (+ back-compat aliases).
352
+ case "-f":
353
+ case "--format":
354
+ format = next();
355
+ break;
356
+ case "--ansi":
357
+ format = "ansi";
358
+ break;
359
+ case "--no-colors":
360
+ case "--text":
361
+ format = "text";
362
+ break;
363
+ case "--pre":
364
+ case "--html":
365
+ format = "html";
366
+ break;
367
+ case "--full":
368
+ format = "full";
369
+ break;
370
+ case "--interactive":
371
+ interactive = true;
372
+ break;
373
+ case "--interactions":
374
+ interactive = true;
375
+ interactions = (next() ?? "").split(",").map((s) => s.trim()).filter(Boolean);
376
+ break;
377
+ case "--decimate-grid":
378
+ decimateGrid = num(next());
379
+ break;
380
+ case "--cdn-version":
381
+ cdnVersion = next();
382
+ break;
383
+ case "-o":
384
+ case "--out":
385
+ out = next();
386
+ break;
387
+ case "-h":
388
+ case "--help":
389
+ file = void 0;
390
+ return { file, out, format, fit, shape, polygonsFile, polygonsJson, interactive, interactions, decimateGrid, cdnVersion, opts };
391
+ default:
392
+ if (!a.startsWith("-") && !file) file = a;
393
+ }
394
+ }
395
+ return { file, out, format, fit, shape, polygonsFile, polygonsJson, interactive, interactions, decimateGrid, cdnVersion, opts };
396
+ }
397
+ var HELP = `glyphcss <mesh-file | shape> [options]
398
+
399
+ Input a mesh file (.obj/.glb/.gltf/.vox/.stl), a primitive shape name
400
+ (e.g. cube, sphere, icosahedron, torus, cone), or:
401
+ --shape NAME --polygons FILE.json --polygons-json '[{"vertices":[...],"color":"#f00"}]'
402
+ Camera --rot-x N --rot-y N --zoom N --distance N --perspective N --ortho
403
+ Grid --cols N --rows N --cell-aspect N (omit cols/rows \u2192 auto-fit to content)
404
+ Fit --fit N target width in columns for auto-fit (default: terminal width or 80)
405
+ Render --mode solid|wireframe|voxel --palette NAME --no-colors --smooth --double-sided
406
+ Mesh --auto-center --mtl FILE (OBJ material override) --mesh-resolution lossy|lossless --crease-angle N --supersample N
407
+ Interact --interactive --interactions orbit,zoom,pan,fpv --decimate-grid N --cdn-version V
408
+ Output -f, --format text|ansi|html|full (default: terminal \u2192 ansi, file \u2192 html, pipe \u2192 text)
409
+ -o, --out FILE
410
+ `;
411
+ function wrapHtml(inner) {
412
+ return `<!doctype html>
413
+ <html><head><meta charset="utf-8"><title>glyphcss</title>
414
+ <style>
415
+ html,body{margin:0;background:#0b0d10;color:#e2e8f0}
416
+ pre.glyph-output{margin:0;font:13px/1 ui-monospace,"SF Mono",Menlo,monospace;white-space:pre}
417
+ </style></head>
418
+ <body>${inner}</body></html>`;
419
+ }
420
+ async function main() {
421
+ const { file, out, format: fmtArg, fit, shape, polygonsFile, polygonsJson, interactive, interactions, decimateGrid, cdnVersion, opts } = parseArgs(process.argv.slice(2));
422
+ const positionalShape = file && !MESH_EXT.test(file) ? file : void 0;
423
+ const meshFile = file && MESH_EXT.test(file) ? file : void 0;
424
+ const shapeName = shape ?? positionalShape;
425
+ if (!meshFile && !shapeName && !polygonsFile && !polygonsJson) {
426
+ process.stderr.write(HELP);
427
+ process.exit(process.argv.length <= 2 ? 1 : 0);
428
+ return;
429
+ }
430
+ const format = fmtArg ?? (out ? "html" : process.stdout.isTTY ? "ansi" : "text");
431
+ if (interactive) {
432
+ if (!meshFile) throw new Error("--interactive needs a mesh file (shapes/polygons are static-only for now)");
433
+ const r = await compileInteractive(meshFile, { ...opts, interactions, decimateGrid, cdnVersion });
434
+ const output2 = format === "full" ? wrapHtml(r.html) : r.html;
435
+ if (out) {
436
+ await (0, import_promises3.writeFile)(out, output2, "utf8");
437
+ process.stderr.write(`glyphcss: wrote ${out} \u2014 interactive [${r.interactions.join(", ")}], ${r.polygonCount}/${r.sourcePolygonCount} tris
438
+ `);
439
+ } else {
440
+ process.stdout.write(output2 + "\n");
441
+ }
442
+ return;
443
+ }
444
+ opts.useColors = format !== "text";
445
+ const hasCols = opts.cols !== void 0, hasRows = opts.rows !== void 0;
446
+ const termW = process.stdout.columns ? process.stdout.columns - 1 : 80;
447
+ if (fit !== void 0) opts.autoFit = { target: fit, by: "cols" };
448
+ else if (hasCols && !hasRows) opts.autoFit = { target: opts.cols, by: "cols" };
449
+ else if (hasRows && !hasCols) opts.autoFit = { target: opts.rows, by: "rows" };
450
+ else if (!hasCols && !hasRows) opts.autoFit = { target: termW, by: "cols" };
451
+ if (opts.autoFit) {
452
+ opts.cols = void 0;
453
+ opts.rows = void 0;
454
+ }
455
+ let result;
456
+ if (polygonsJson) result = compilePolygons(normalizePolygons(JSON.parse(polygonsJson)), opts);
457
+ else if (polygonsFile) result = compilePolygons(normalizePolygons(JSON.parse(await (0, import_promises3.readFile)(polygonsFile, "utf8"))), opts);
458
+ else if (shapeName) result = compilePolygons((0, import_core2.resolveGeometry)(shapeName, { size: 1 }), { ...opts, autoCenter: true });
459
+ else result = await compileFile(meshFile, opts);
460
+ const output = format === "text" ? result.inner : format === "ansi" ? (0, import_glyphcss3.encodeGlyphAnsi)(result.inner) : format === "full" ? wrapHtml(result.html) : result.html;
461
+ if (out) {
462
+ await (0, import_promises3.writeFile)(out, output, "utf8");
463
+ process.stderr.write(`glyphcss: wrote ${out} (${result.cols}\xD7${result.rows}, ${format})
464
+ `);
465
+ } else {
466
+ process.stdout.write(output + "\n");
467
+ }
468
+ }
469
+ main().catch((err) => {
470
+ process.stderr.write(`glyphcss: ${err instanceof Error ? err.message : String(err)}
471
+ `);
472
+ process.exit(1);
473
+ });
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node