@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 +21 -0
- package/README.md +125 -0
- package/SKILL.md +60 -0
- package/dist/cli.cjs +473 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +458 -0
- package/dist/index.cjs +315 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +282 -0
- package/dist/vite-zkmogiPw.d.cts +69 -0
- package/dist/vite-zkmogiPw.d.ts +69 -0
- package/dist/vite.cjs +299 -0
- package/dist/vite.d.cts +3 -0
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +267 -0
- package/package.json +72 -0
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
|