@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/dist/vite.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/compileFile.ts
|
|
2
|
+
import {
|
|
3
|
+
compileScene,
|
|
4
|
+
createGlyphPerspectiveCamera,
|
|
5
|
+
createGlyphOrthographicCamera,
|
|
6
|
+
cropGlyphInner
|
|
7
|
+
} from "glyphcss";
|
|
8
|
+
|
|
9
|
+
// src/loadMeshFromFile.ts
|
|
10
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
11
|
+
import { loadMesh } from "@glyphcss/core";
|
|
12
|
+
|
|
13
|
+
// src/textureBakeNode.ts
|
|
14
|
+
import { readFile } from "fs/promises";
|
|
15
|
+
import { PNG } from "pngjs";
|
|
16
|
+
import jpeg from "jpeg-js";
|
|
17
|
+
function stripQuery(url) {
|
|
18
|
+
return url.split("?")[0].split("#")[0];
|
|
19
|
+
}
|
|
20
|
+
function toFilePath(url) {
|
|
21
|
+
const clean = stripQuery(url);
|
|
22
|
+
if (/^https?:\/\//i.test(clean)) {
|
|
23
|
+
try {
|
|
24
|
+
return decodeURIComponent(new URL(clean).pathname);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (clean.startsWith("file://")) {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(new URL(clean).pathname);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return clean;
|
|
35
|
+
}
|
|
36
|
+
async function decode(path) {
|
|
37
|
+
const fp = toFilePath(path);
|
|
38
|
+
try {
|
|
39
|
+
const buf = await readFile(fp);
|
|
40
|
+
if (/\.png$/i.test(fp)) {
|
|
41
|
+
const p = PNG.sync.read(buf);
|
|
42
|
+
return { w: p.width, h: p.height, data: p.data };
|
|
43
|
+
}
|
|
44
|
+
if (/\.jpe?g$/i.test(fp)) {
|
|
45
|
+
const j = jpeg.decode(buf, { useTArray: true });
|
|
46
|
+
return { w: j.width, h: j.height, data: j.data };
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function texelAt(img, u, v) {
|
|
53
|
+
const x = Math.min(img.w - 1, Math.max(0, Math.round(u * (img.w - 1))));
|
|
54
|
+
const y = Math.min(img.h - 1, Math.max(0, Math.round((1 - v) * (img.h - 1))));
|
|
55
|
+
const i = (y * img.w + x) * 4;
|
|
56
|
+
return [img.data[i], img.data[i + 1], img.data[i + 2]];
|
|
57
|
+
}
|
|
58
|
+
function sampleFace(img, uvs) {
|
|
59
|
+
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]];
|
|
60
|
+
let r = 0, g = 0, b = 0;
|
|
61
|
+
for (const [u, v] of pts) {
|
|
62
|
+
const c = texelAt(img, u, v);
|
|
63
|
+
r += c[0];
|
|
64
|
+
g += c[1];
|
|
65
|
+
b += c[2];
|
|
66
|
+
}
|
|
67
|
+
const n = pts.length;
|
|
68
|
+
const h = (x) => Math.round(x / n).toString(16).padStart(2, "0");
|
|
69
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
70
|
+
}
|
|
71
|
+
async function bakeTexturesNode(polygons) {
|
|
72
|
+
const cache = /* @__PURE__ */ new Map();
|
|
73
|
+
const get = async (url) => {
|
|
74
|
+
if (!cache.has(url)) cache.set(url, await decode(url));
|
|
75
|
+
return cache.get(url) ?? null;
|
|
76
|
+
};
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const p of polygons) {
|
|
79
|
+
const tt = p.textureTriangles?.[0];
|
|
80
|
+
const tex = p.texture ?? p.material?.texture ?? tt?.texture;
|
|
81
|
+
const uvs = tt?.uvs ?? (p.uvs && p.uvs.length >= 3 ? [p.uvs[0], p.uvs[1], p.uvs[2]] : void 0);
|
|
82
|
+
if (tex && uvs) {
|
|
83
|
+
const img = await get(tex);
|
|
84
|
+
if (img) {
|
|
85
|
+
out.push({ ...p, color: sampleFace(img, uvs), texture: void 0, textureTriangles: void 0, uvs: void 0 });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
out.push(p);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function hasTextures(polygons) {
|
|
94
|
+
return polygons.some((p) => p.texture || p.material?.texture || p.textureTriangles?.[0]?.texture);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/loadMeshFromFile.ts
|
|
98
|
+
function stripQuery2(url) {
|
|
99
|
+
return url.split("?")[0].split("#")[0];
|
|
100
|
+
}
|
|
101
|
+
async function siblingMtl(objPath) {
|
|
102
|
+
const clean = stripQuery2(objPath);
|
|
103
|
+
const dir = clean.replace(/[^/\\]+$/, "");
|
|
104
|
+
let candidate;
|
|
105
|
+
try {
|
|
106
|
+
const m = (await readFile2(clean, "utf8")).match(/^\s*mtllib\s+(.+?)\s*$/im);
|
|
107
|
+
candidate = m ? dir + m[1].trim() : clean.replace(/\.obj$/i, ".mtl");
|
|
108
|
+
} catch {
|
|
109
|
+
candidate = clean.replace(/\.obj$/i, ".mtl");
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await readFile2(candidate);
|
|
113
|
+
return candidate;
|
|
114
|
+
} catch {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function fileFetch(url) {
|
|
119
|
+
const path = stripQuery2(url);
|
|
120
|
+
try {
|
|
121
|
+
const buf = await readFile2(path);
|
|
122
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
status: 200,
|
|
126
|
+
text: async () => buf.toString("utf8"),
|
|
127
|
+
arrayBuffer: async () => ab
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { ok: false, status: 404, text: async () => "", arrayBuffer: async () => new ArrayBuffer(0) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function loadMeshFromFile(path, options) {
|
|
134
|
+
const g = globalThis;
|
|
135
|
+
const prev = g.fetch;
|
|
136
|
+
g.fetch = fileFetch;
|
|
137
|
+
let result;
|
|
138
|
+
try {
|
|
139
|
+
const mtlUrl = options?.mtlUrl ?? (/\.obj(\?|$)/i.test(path) ? await siblingMtl(path) : void 0);
|
|
140
|
+
result = await loadMesh(path, { solidTextureSamples: false, ...options, mtlUrl });
|
|
141
|
+
} finally {
|
|
142
|
+
g.fetch = prev;
|
|
143
|
+
}
|
|
144
|
+
if (hasTextures(result.polygons)) {
|
|
145
|
+
return { ...result, polygons: await bakeTexturesNode(result.polygons) };
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/compileFile.ts
|
|
151
|
+
function worldMaxDim(polys) {
|
|
152
|
+
let mnx = Infinity, mxx = -Infinity, mny = Infinity, mxy = -Infinity, mnz = Infinity, mxz = -Infinity;
|
|
153
|
+
for (const p of polys) for (const v of p.vertices) {
|
|
154
|
+
if (v[0] < mnx) mnx = v[0];
|
|
155
|
+
if (v[0] > mxx) mxx = v[0];
|
|
156
|
+
if (v[1] < mny) mny = v[1];
|
|
157
|
+
if (v[1] > mxy) mxy = v[1];
|
|
158
|
+
if (v[2] < mnz) mnz = v[2];
|
|
159
|
+
if (v[2] > mxz) mxz = v[2];
|
|
160
|
+
}
|
|
161
|
+
if (!isFinite(mnx)) return 1;
|
|
162
|
+
return Math.max(mxx - mnx, mxy - mny, mxz - mnz, 1e-6);
|
|
163
|
+
}
|
|
164
|
+
function measureContent(inner) {
|
|
165
|
+
const lines = inner.replace(/<[^>]*>/g, "").split("\n");
|
|
166
|
+
let minC = Infinity, maxC = -1, minR = Infinity, maxR = -1;
|
|
167
|
+
lines.forEach((l, r) => {
|
|
168
|
+
for (let c = 0; c < l.length; c++) if (l[c] !== " ") {
|
|
169
|
+
if (c < minC) minC = c;
|
|
170
|
+
if (c > maxC) maxC = c;
|
|
171
|
+
if (r < minR) minR = r;
|
|
172
|
+
if (r > maxR) maxR = r;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return maxC < 0 ? { w: 0, h: 0 } : { w: maxC - minC + 1, h: maxR - minR + 1 };
|
|
176
|
+
}
|
|
177
|
+
function compilePolygons(polygons, options = {}) {
|
|
178
|
+
const buildCam = (zoom) => options.projection === "orthographic" ? createGlyphOrthographicCamera({ rotX: options.rotX, rotY: options.rotY, zoom }) : createGlyphPerspectiveCamera({ rotX: options.rotX, rotY: options.rotY, zoom, distance: options.distance, perspective: options.perspective });
|
|
179
|
+
const shared = {
|
|
180
|
+
autoCenter: options.autoCenter,
|
|
181
|
+
cellAspect: options.cellAspect,
|
|
182
|
+
mode: options.mode,
|
|
183
|
+
glyphPalette: options.glyphPalette,
|
|
184
|
+
useColors: options.useColors,
|
|
185
|
+
smoothShading: options.smoothShading,
|
|
186
|
+
creaseAngle: options.creaseAngle,
|
|
187
|
+
doubleSided: options.doubleSided,
|
|
188
|
+
supersample: options.supersample
|
|
189
|
+
};
|
|
190
|
+
if (options.autoFit && options.autoFit.target > 0) {
|
|
191
|
+
const { target, by } = options.autoFit;
|
|
192
|
+
const probeZoom = 40 / worldMaxDim(polygons);
|
|
193
|
+
const probe = compileScene({ polygons, camera: buildCam(probeZoom), cols: 200, rows: 120, ...shared, autoCenter: true });
|
|
194
|
+
const m = measureContent(probe.inner);
|
|
195
|
+
if (m.w > 0 && m.h > 0) {
|
|
196
|
+
const scale = by === "rows" ? target / m.h : target / m.w;
|
|
197
|
+
const zoom = probeZoom * scale;
|
|
198
|
+
const cols = Math.ceil(m.w * scale * 1.4) + 6;
|
|
199
|
+
const rows = Math.ceil(m.h * scale * 1.4) + 6;
|
|
200
|
+
const full = compileScene({ polygons, camera: buildCam(zoom), cols, rows, ...shared, autoCenter: true });
|
|
201
|
+
const inner = cropGlyphInner(full.inner);
|
|
202
|
+
const lines = inner.split("\n");
|
|
203
|
+
const w = lines.reduce((a, l) => Math.max(a, l.replace(/<[^>]*>/g, "").length), 0);
|
|
204
|
+
return { html: `<pre class="glyph-output">${inner}</pre>`, inner, cols: w, rows: lines.length, cellAspect: options.cellAspect ?? 2 };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return compileScene({
|
|
208
|
+
polygons,
|
|
209
|
+
camera: buildCam(options.zoom),
|
|
210
|
+
cols: options.cols,
|
|
211
|
+
rows: options.rows,
|
|
212
|
+
...shared
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function compileFile(path, options = {}) {
|
|
216
|
+
const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
|
|
217
|
+
return compilePolygons(polygons, options);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/vite.ts
|
|
221
|
+
var MESH_RE = /\.(glb|gltf|obj|vox|stl)(\?|$)/i;
|
|
222
|
+
function hasGlyphFlag(id) {
|
|
223
|
+
const q = id.split("?")[1];
|
|
224
|
+
return q != null && new URLSearchParams(q).has("glyph");
|
|
225
|
+
}
|
|
226
|
+
function parseQuery(id) {
|
|
227
|
+
const q = id.split("?")[1] ?? "";
|
|
228
|
+
const p = new URLSearchParams(q);
|
|
229
|
+
const num = (k) => p.has(k) ? Number(p.get(k)) : void 0;
|
|
230
|
+
const out = {};
|
|
231
|
+
for (const k of ["rotX", "rotY", "zoom", "distance", "perspective", "cols", "rows", "cellAspect", "creaseAngle", "supersample"]) {
|
|
232
|
+
const v = num(k);
|
|
233
|
+
if (v !== void 0 && Number.isFinite(v)) out[k] = v;
|
|
234
|
+
}
|
|
235
|
+
if (p.has("mode")) out.mode = p.get("mode");
|
|
236
|
+
if (p.has("palette")) out.glyphPalette = p.get("palette") ?? void 0;
|
|
237
|
+
if (p.has("projection")) out.projection = p.get("projection") === "orthographic" ? "orthographic" : "perspective";
|
|
238
|
+
if (p.has("meshResolution")) out.meshResolution = p.get("meshResolution");
|
|
239
|
+
if (p.has("colors")) out.useColors = p.get("colors") !== "0" && p.get("colors") !== "false";
|
|
240
|
+
if (p.has("autoCenter")) out.autoCenter = p.get("autoCenter") !== "0" && p.get("autoCenter") !== "false";
|
|
241
|
+
if (p.has("smoothShading")) out.smoothShading = p.get("smoothShading") !== "0" && p.get("smoothShading") !== "false";
|
|
242
|
+
if (p.has("doubleSided")) out.doubleSided = p.get("doubleSided") !== "0" && p.get("doubleSided") !== "false";
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
function glyphcssCompile(globalOptions = {}) {
|
|
246
|
+
return {
|
|
247
|
+
name: "glyphcss-compile",
|
|
248
|
+
enforce: "pre",
|
|
249
|
+
async load(id) {
|
|
250
|
+
if (!MESH_RE.test(id) || !hasGlyphFlag(id)) return null;
|
|
251
|
+
const path = id.split("?")[0];
|
|
252
|
+
const opts = { ...globalOptions, ...parseQuery(id) };
|
|
253
|
+
const result = await compileFile(path, opts);
|
|
254
|
+
const meta = { cols: result.cols, rows: result.rows, cellAspect: result.cellAspect };
|
|
255
|
+
return [
|
|
256
|
+
`export default ${JSON.stringify(result.html)};`,
|
|
257
|
+
`export const inner = ${JSON.stringify(result.inner)};`,
|
|
258
|
+
`export const meta = ${JSON.stringify(meta)};`
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
var vite_default = glyphcssCompile;
|
|
264
|
+
export {
|
|
265
|
+
vite_default as default,
|
|
266
|
+
glyphcssCompile
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glyphcss/compile",
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "Compile 3D meshes to static glyphcss ASCII at build time — a Vite plugin, a CLI, and a Node API. Zero-runtime <pre> output for any static pipeline.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"glyphcss",
|
|
11
|
+
"ascii",
|
|
12
|
+
"3d",
|
|
13
|
+
"compile",
|
|
14
|
+
"vite",
|
|
15
|
+
"vite-plugin",
|
|
16
|
+
"static",
|
|
17
|
+
"ssg",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/apresmoi/glyphcss.git",
|
|
24
|
+
"directory": "packages/compile"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/apresmoi/glyphcss/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/apresmoi/glyphcss#readme",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"SKILL.md"
|
|
33
|
+
],
|
|
34
|
+
"bin": {
|
|
35
|
+
"glyphcss": "dist/cli.js"
|
|
36
|
+
},
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"require": "./dist/index.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./vite": {
|
|
44
|
+
"types": "./dist/vite.d.ts",
|
|
45
|
+
"import": "./dist/vite.js",
|
|
46
|
+
"require": "./dist/vite.cjs"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"pngjs": "^7.0.0",
|
|
54
|
+
"jpeg-js": "^0.4.4",
|
|
55
|
+
"@glyphcss/core": "^0.0.9",
|
|
56
|
+
"glyphcss": "^0.0.9"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^20.11.0",
|
|
60
|
+
"@types/pngjs": "^6.0.5",
|
|
61
|
+
"happy-dom": "^20.7.0",
|
|
62
|
+
"tsup": "^8.0.1",
|
|
63
|
+
"typescript": "^5.3.3",
|
|
64
|
+
"vitest": "^3.1.1",
|
|
65
|
+
"@vitest/coverage-v8": "^3.1.1"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsup",
|
|
69
|
+
"test": "vitest run --passWithNoTests",
|
|
70
|
+
"test:coverage": "vitest run --coverage --passWithNoTests"
|
|
71
|
+
}
|
|
72
|
+
}
|