@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/index.cjs
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
compileFile: () => compileFile,
|
|
34
|
+
compileInteractive: () => compileInteractive,
|
|
35
|
+
compilePolygons: () => compilePolygons,
|
|
36
|
+
compileScene: () => import_glyphcss3.compileScene,
|
|
37
|
+
glyphcssCompile: () => glyphcssCompile,
|
|
38
|
+
loadMeshFromFile: () => loadMeshFromFile,
|
|
39
|
+
toCodepenPrefill: () => import_glyphcss2.glyphCodepenPrefill
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(src_exports);
|
|
42
|
+
var import_glyphcss3 = require("glyphcss");
|
|
43
|
+
|
|
44
|
+
// src/loadMeshFromFile.ts
|
|
45
|
+
var import_promises2 = require("fs/promises");
|
|
46
|
+
var import_core = require("@glyphcss/core");
|
|
47
|
+
|
|
48
|
+
// src/textureBakeNode.ts
|
|
49
|
+
var import_promises = require("fs/promises");
|
|
50
|
+
var import_pngjs = require("pngjs");
|
|
51
|
+
var import_jpeg_js = __toESM(require("jpeg-js"), 1);
|
|
52
|
+
function stripQuery(url) {
|
|
53
|
+
return url.split("?")[0].split("#")[0];
|
|
54
|
+
}
|
|
55
|
+
function toFilePath(url) {
|
|
56
|
+
const clean = stripQuery(url);
|
|
57
|
+
if (/^https?:\/\//i.test(clean)) {
|
|
58
|
+
try {
|
|
59
|
+
return decodeURIComponent(new URL(clean).pathname);
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (clean.startsWith("file://")) {
|
|
64
|
+
try {
|
|
65
|
+
return decodeURIComponent(new URL(clean).pathname);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return clean;
|
|
70
|
+
}
|
|
71
|
+
async function decode(path) {
|
|
72
|
+
const fp = toFilePath(path);
|
|
73
|
+
try {
|
|
74
|
+
const buf = await (0, import_promises.readFile)(fp);
|
|
75
|
+
if (/\.png$/i.test(fp)) {
|
|
76
|
+
const p = import_pngjs.PNG.sync.read(buf);
|
|
77
|
+
return { w: p.width, h: p.height, data: p.data };
|
|
78
|
+
}
|
|
79
|
+
if (/\.jpe?g$/i.test(fp)) {
|
|
80
|
+
const j = import_jpeg_js.default.decode(buf, { useTArray: true });
|
|
81
|
+
return { w: j.width, h: j.height, data: j.data };
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function texelAt(img, u, v) {
|
|
88
|
+
const x = Math.min(img.w - 1, Math.max(0, Math.round(u * (img.w - 1))));
|
|
89
|
+
const y = Math.min(img.h - 1, Math.max(0, Math.round((1 - v) * (img.h - 1))));
|
|
90
|
+
const i = (y * img.w + x) * 4;
|
|
91
|
+
return [img.data[i], img.data[i + 1], img.data[i + 2]];
|
|
92
|
+
}
|
|
93
|
+
function sampleFace(img, uvs) {
|
|
94
|
+
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]];
|
|
95
|
+
let r = 0, g = 0, b = 0;
|
|
96
|
+
for (const [u, v] of pts) {
|
|
97
|
+
const c = texelAt(img, u, v);
|
|
98
|
+
r += c[0];
|
|
99
|
+
g += c[1];
|
|
100
|
+
b += c[2];
|
|
101
|
+
}
|
|
102
|
+
const n = pts.length;
|
|
103
|
+
const h = (x) => Math.round(x / n).toString(16).padStart(2, "0");
|
|
104
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
105
|
+
}
|
|
106
|
+
async function bakeTexturesNode(polygons) {
|
|
107
|
+
const cache = /* @__PURE__ */ new Map();
|
|
108
|
+
const get = async (url) => {
|
|
109
|
+
if (!cache.has(url)) cache.set(url, await decode(url));
|
|
110
|
+
return cache.get(url) ?? null;
|
|
111
|
+
};
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const p of polygons) {
|
|
114
|
+
const tt = p.textureTriangles?.[0];
|
|
115
|
+
const tex = p.texture ?? p.material?.texture ?? tt?.texture;
|
|
116
|
+
const uvs = tt?.uvs ?? (p.uvs && p.uvs.length >= 3 ? [p.uvs[0], p.uvs[1], p.uvs[2]] : void 0);
|
|
117
|
+
if (tex && uvs) {
|
|
118
|
+
const img = await get(tex);
|
|
119
|
+
if (img) {
|
|
120
|
+
out.push({ ...p, color: sampleFace(img, uvs), texture: void 0, textureTriangles: void 0, uvs: void 0 });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
out.push(p);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function hasTextures(polygons) {
|
|
129
|
+
return polygons.some((p) => p.texture || p.material?.texture || p.textureTriangles?.[0]?.texture);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/loadMeshFromFile.ts
|
|
133
|
+
function stripQuery2(url) {
|
|
134
|
+
return url.split("?")[0].split("#")[0];
|
|
135
|
+
}
|
|
136
|
+
async function siblingMtl(objPath) {
|
|
137
|
+
const clean = stripQuery2(objPath);
|
|
138
|
+
const dir = clean.replace(/[^/\\]+$/, "");
|
|
139
|
+
let candidate;
|
|
140
|
+
try {
|
|
141
|
+
const m = (await (0, import_promises2.readFile)(clean, "utf8")).match(/^\s*mtllib\s+(.+?)\s*$/im);
|
|
142
|
+
candidate = m ? dir + m[1].trim() : clean.replace(/\.obj$/i, ".mtl");
|
|
143
|
+
} catch {
|
|
144
|
+
candidate = clean.replace(/\.obj$/i, ".mtl");
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await (0, import_promises2.readFile)(candidate);
|
|
148
|
+
return candidate;
|
|
149
|
+
} catch {
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function fileFetch(url) {
|
|
154
|
+
const path = stripQuery2(url);
|
|
155
|
+
try {
|
|
156
|
+
const buf = await (0, import_promises2.readFile)(path);
|
|
157
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
status: 200,
|
|
161
|
+
text: async () => buf.toString("utf8"),
|
|
162
|
+
arrayBuffer: async () => ab
|
|
163
|
+
};
|
|
164
|
+
} catch {
|
|
165
|
+
return { ok: false, status: 404, text: async () => "", arrayBuffer: async () => new ArrayBuffer(0) };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function loadMeshFromFile(path, options) {
|
|
169
|
+
const g = globalThis;
|
|
170
|
+
const prev = g.fetch;
|
|
171
|
+
g.fetch = fileFetch;
|
|
172
|
+
let result;
|
|
173
|
+
try {
|
|
174
|
+
const mtlUrl = options?.mtlUrl ?? (/\.obj(\?|$)/i.test(path) ? await siblingMtl(path) : void 0);
|
|
175
|
+
result = await (0, import_core.loadMesh)(path, { solidTextureSamples: false, ...options, mtlUrl });
|
|
176
|
+
} finally {
|
|
177
|
+
g.fetch = prev;
|
|
178
|
+
}
|
|
179
|
+
if (hasTextures(result.polygons)) {
|
|
180
|
+
return { ...result, polygons: await bakeTexturesNode(result.polygons) };
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/compileFile.ts
|
|
186
|
+
var import_glyphcss = require("glyphcss");
|
|
187
|
+
function worldMaxDim(polys) {
|
|
188
|
+
let mnx = Infinity, mxx = -Infinity, mny = Infinity, mxy = -Infinity, mnz = Infinity, mxz = -Infinity;
|
|
189
|
+
for (const p of polys) for (const v of p.vertices) {
|
|
190
|
+
if (v[0] < mnx) mnx = v[0];
|
|
191
|
+
if (v[0] > mxx) mxx = v[0];
|
|
192
|
+
if (v[1] < mny) mny = v[1];
|
|
193
|
+
if (v[1] > mxy) mxy = v[1];
|
|
194
|
+
if (v[2] < mnz) mnz = v[2];
|
|
195
|
+
if (v[2] > mxz) mxz = v[2];
|
|
196
|
+
}
|
|
197
|
+
if (!isFinite(mnx)) return 1;
|
|
198
|
+
return Math.max(mxx - mnx, mxy - mny, mxz - mnz, 1e-6);
|
|
199
|
+
}
|
|
200
|
+
function measureContent(inner) {
|
|
201
|
+
const lines = inner.replace(/<[^>]*>/g, "").split("\n");
|
|
202
|
+
let minC = Infinity, maxC = -1, minR = Infinity, maxR = -1;
|
|
203
|
+
lines.forEach((l, r) => {
|
|
204
|
+
for (let c = 0; c < l.length; c++) if (l[c] !== " ") {
|
|
205
|
+
if (c < minC) minC = c;
|
|
206
|
+
if (c > maxC) maxC = c;
|
|
207
|
+
if (r < minR) minR = r;
|
|
208
|
+
if (r > maxR) maxR = r;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return maxC < 0 ? { w: 0, h: 0 } : { w: maxC - minC + 1, h: maxR - minR + 1 };
|
|
212
|
+
}
|
|
213
|
+
function compilePolygons(polygons, options = {}) {
|
|
214
|
+
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 });
|
|
215
|
+
const shared = {
|
|
216
|
+
autoCenter: options.autoCenter,
|
|
217
|
+
cellAspect: options.cellAspect,
|
|
218
|
+
mode: options.mode,
|
|
219
|
+
glyphPalette: options.glyphPalette,
|
|
220
|
+
useColors: options.useColors,
|
|
221
|
+
smoothShading: options.smoothShading,
|
|
222
|
+
creaseAngle: options.creaseAngle,
|
|
223
|
+
doubleSided: options.doubleSided,
|
|
224
|
+
supersample: options.supersample
|
|
225
|
+
};
|
|
226
|
+
if (options.autoFit && options.autoFit.target > 0) {
|
|
227
|
+
const { target, by } = options.autoFit;
|
|
228
|
+
const probeZoom = 40 / worldMaxDim(polygons);
|
|
229
|
+
const probe = (0, import_glyphcss.compileScene)({ polygons, camera: buildCam(probeZoom), cols: 200, rows: 120, ...shared, autoCenter: true });
|
|
230
|
+
const m = measureContent(probe.inner);
|
|
231
|
+
if (m.w > 0 && m.h > 0) {
|
|
232
|
+
const scale = by === "rows" ? target / m.h : target / m.w;
|
|
233
|
+
const zoom = probeZoom * scale;
|
|
234
|
+
const cols = Math.ceil(m.w * scale * 1.4) + 6;
|
|
235
|
+
const rows = Math.ceil(m.h * scale * 1.4) + 6;
|
|
236
|
+
const full = (0, import_glyphcss.compileScene)({ polygons, camera: buildCam(zoom), cols, rows, ...shared, autoCenter: true });
|
|
237
|
+
const inner = (0, import_glyphcss.cropGlyphInner)(full.inner);
|
|
238
|
+
const lines = inner.split("\n");
|
|
239
|
+
const w = lines.reduce((a, l) => Math.max(a, l.replace(/<[^>]*>/g, "").length), 0);
|
|
240
|
+
return { html: `<pre class="glyph-output">${inner}</pre>`, inner, cols: w, rows: lines.length, cellAspect: options.cellAspect ?? 2 };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return (0, import_glyphcss.compileScene)({
|
|
244
|
+
polygons,
|
|
245
|
+
camera: buildCam(options.zoom),
|
|
246
|
+
cols: options.cols,
|
|
247
|
+
rows: options.rows,
|
|
248
|
+
...shared
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function compileFile(path, options = {}) {
|
|
252
|
+
const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
|
|
253
|
+
return compilePolygons(polygons, options);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/compileInteractive.ts
|
|
257
|
+
var import_glyphcss2 = require("glyphcss");
|
|
258
|
+
async function compileInteractive(path, options = {}) {
|
|
259
|
+
const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
|
|
260
|
+
return (0, import_glyphcss2.buildGlyphInteractiveExport)(polygons, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/vite.ts
|
|
264
|
+
var MESH_RE = /\.(glb|gltf|obj|vox|stl)(\?|$)/i;
|
|
265
|
+
function hasGlyphFlag(id) {
|
|
266
|
+
const q = id.split("?")[1];
|
|
267
|
+
return q != null && new URLSearchParams(q).has("glyph");
|
|
268
|
+
}
|
|
269
|
+
function parseQuery(id) {
|
|
270
|
+
const q = id.split("?")[1] ?? "";
|
|
271
|
+
const p = new URLSearchParams(q);
|
|
272
|
+
const num = (k) => p.has(k) ? Number(p.get(k)) : void 0;
|
|
273
|
+
const out = {};
|
|
274
|
+
for (const k of ["rotX", "rotY", "zoom", "distance", "perspective", "cols", "rows", "cellAspect", "creaseAngle", "supersample"]) {
|
|
275
|
+
const v = num(k);
|
|
276
|
+
if (v !== void 0 && Number.isFinite(v)) out[k] = v;
|
|
277
|
+
}
|
|
278
|
+
if (p.has("mode")) out.mode = p.get("mode");
|
|
279
|
+
if (p.has("palette")) out.glyphPalette = p.get("palette") ?? void 0;
|
|
280
|
+
if (p.has("projection")) out.projection = p.get("projection") === "orthographic" ? "orthographic" : "perspective";
|
|
281
|
+
if (p.has("meshResolution")) out.meshResolution = p.get("meshResolution");
|
|
282
|
+
if (p.has("colors")) out.useColors = p.get("colors") !== "0" && p.get("colors") !== "false";
|
|
283
|
+
if (p.has("autoCenter")) out.autoCenter = p.get("autoCenter") !== "0" && p.get("autoCenter") !== "false";
|
|
284
|
+
if (p.has("smoothShading")) out.smoothShading = p.get("smoothShading") !== "0" && p.get("smoothShading") !== "false";
|
|
285
|
+
if (p.has("doubleSided")) out.doubleSided = p.get("doubleSided") !== "0" && p.get("doubleSided") !== "false";
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
function glyphcssCompile(globalOptions = {}) {
|
|
289
|
+
return {
|
|
290
|
+
name: "glyphcss-compile",
|
|
291
|
+
enforce: "pre",
|
|
292
|
+
async load(id) {
|
|
293
|
+
if (!MESH_RE.test(id) || !hasGlyphFlag(id)) return null;
|
|
294
|
+
const path = id.split("?")[0];
|
|
295
|
+
const opts = { ...globalOptions, ...parseQuery(id) };
|
|
296
|
+
const result = await compileFile(path, opts);
|
|
297
|
+
const meta = { cols: result.cols, rows: result.rows, cellAspect: result.cellAspect };
|
|
298
|
+
return [
|
|
299
|
+
`export default ${JSON.stringify(result.html)};`,
|
|
300
|
+
`export const inner = ${JSON.stringify(result.inner)};`,
|
|
301
|
+
`export const meta = ${JSON.stringify(meta)};`
|
|
302
|
+
].join("\n");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
307
|
+
0 && (module.exports = {
|
|
308
|
+
compileFile,
|
|
309
|
+
compileInteractive,
|
|
310
|
+
compilePolygons,
|
|
311
|
+
compileScene,
|
|
312
|
+
glyphcssCompile,
|
|
313
|
+
loadMeshFromFile,
|
|
314
|
+
toCodepenPrefill
|
|
315
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GlyphInteractiveExportOptions, GlyphInteractiveExportResult } from 'glyphcss';
|
|
2
|
+
export { CompileSceneOptions, CompileSceneResult, GlyphInteraction, compileScene, glyphCodepenPrefill as toCodepenPrefill } from 'glyphcss';
|
|
3
|
+
import { LoadMeshOptions, ParseResult, MeshResolution } from '@glyphcss/core';
|
|
4
|
+
export { C as CompileFileOptions, G as GlyphCompileOptions, c as compileFile, a as compilePolygons, g as glyphcssCompile } from './vite-zkmogiPw.cjs';
|
|
5
|
+
|
|
6
|
+
declare function loadMeshFromFile(path: string, options?: LoadMeshOptions): Promise<ParseResult>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* compileInteractive — load a mesh file and compile it to a **self-contained
|
|
10
|
+
* interactive** scene, shipping only what the declared interactions need.
|
|
11
|
+
*
|
|
12
|
+
* Thin Node wrapper: read the file, then hand the polygons to glyphcss's
|
|
13
|
+
* (pure, browser-safe) `buildGlyphInteractiveExport`. The same builder powers
|
|
14
|
+
* the in-browser "export to CodePen" button, so file pipeline and gallery emit
|
|
15
|
+
* identical snippets.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type CompileInteractiveResult = GlyphInteractiveExportResult;
|
|
19
|
+
interface CompileInteractiveOptions extends GlyphInteractiveExportOptions {
|
|
20
|
+
/** Mesh-optimization quality passed to `loadMesh`. Default: the loadMesh default ("lossy"). */
|
|
21
|
+
meshResolution?: MeshResolution;
|
|
22
|
+
/** Explicit companion `.mtl` path for OBJ (overrides sibling auto-detection). */
|
|
23
|
+
mtlUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare function compileInteractive(path: string, options?: CompileInteractiveOptions): Promise<CompileInteractiveResult>;
|
|
27
|
+
|
|
28
|
+
export { type CompileInteractiveOptions, type CompileInteractiveResult, compileInteractive, loadMeshFromFile };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GlyphInteractiveExportOptions, GlyphInteractiveExportResult } from 'glyphcss';
|
|
2
|
+
export { CompileSceneOptions, CompileSceneResult, GlyphInteraction, compileScene, glyphCodepenPrefill as toCodepenPrefill } from 'glyphcss';
|
|
3
|
+
import { LoadMeshOptions, ParseResult, MeshResolution } from '@glyphcss/core';
|
|
4
|
+
export { C as CompileFileOptions, G as GlyphCompileOptions, c as compileFile, a as compilePolygons, g as glyphcssCompile } from './vite-zkmogiPw.js';
|
|
5
|
+
|
|
6
|
+
declare function loadMeshFromFile(path: string, options?: LoadMeshOptions): Promise<ParseResult>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* compileInteractive — load a mesh file and compile it to a **self-contained
|
|
10
|
+
* interactive** scene, shipping only what the declared interactions need.
|
|
11
|
+
*
|
|
12
|
+
* Thin Node wrapper: read the file, then hand the polygons to glyphcss's
|
|
13
|
+
* (pure, browser-safe) `buildGlyphInteractiveExport`. The same builder powers
|
|
14
|
+
* the in-browser "export to CodePen" button, so file pipeline and gallery emit
|
|
15
|
+
* identical snippets.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type CompileInteractiveResult = GlyphInteractiveExportResult;
|
|
19
|
+
interface CompileInteractiveOptions extends GlyphInteractiveExportOptions {
|
|
20
|
+
/** Mesh-optimization quality passed to `loadMesh`. Default: the loadMesh default ("lossy"). */
|
|
21
|
+
meshResolution?: MeshResolution;
|
|
22
|
+
/** Explicit companion `.mtl` path for OBJ (overrides sibling auto-detection). */
|
|
23
|
+
mtlUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare function compileInteractive(path: string, options?: CompileInteractiveOptions): Promise<CompileInteractiveResult>;
|
|
27
|
+
|
|
28
|
+
export { type CompileInteractiveOptions, type CompileInteractiveResult, compileInteractive, loadMeshFromFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { compileScene as compileScene2 } from "glyphcss";
|
|
3
|
+
|
|
4
|
+
// src/loadMeshFromFile.ts
|
|
5
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
6
|
+
import { loadMesh } from "@glyphcss/core";
|
|
7
|
+
|
|
8
|
+
// src/textureBakeNode.ts
|
|
9
|
+
import { readFile } from "fs/promises";
|
|
10
|
+
import { PNG } from "pngjs";
|
|
11
|
+
import jpeg from "jpeg-js";
|
|
12
|
+
function stripQuery(url) {
|
|
13
|
+
return url.split("?")[0].split("#")[0];
|
|
14
|
+
}
|
|
15
|
+
function toFilePath(url) {
|
|
16
|
+
const clean = stripQuery(url);
|
|
17
|
+
if (/^https?:\/\//i.test(clean)) {
|
|
18
|
+
try {
|
|
19
|
+
return decodeURIComponent(new URL(clean).pathname);
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (clean.startsWith("file://")) {
|
|
24
|
+
try {
|
|
25
|
+
return decodeURIComponent(new URL(clean).pathname);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return clean;
|
|
30
|
+
}
|
|
31
|
+
async function decode(path) {
|
|
32
|
+
const fp = toFilePath(path);
|
|
33
|
+
try {
|
|
34
|
+
const buf = await readFile(fp);
|
|
35
|
+
if (/\.png$/i.test(fp)) {
|
|
36
|
+
const p = PNG.sync.read(buf);
|
|
37
|
+
return { w: p.width, h: p.height, data: p.data };
|
|
38
|
+
}
|
|
39
|
+
if (/\.jpe?g$/i.test(fp)) {
|
|
40
|
+
const j = jpeg.decode(buf, { useTArray: true });
|
|
41
|
+
return { w: j.width, h: j.height, data: j.data };
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function texelAt(img, u, v) {
|
|
48
|
+
const x = Math.min(img.w - 1, Math.max(0, Math.round(u * (img.w - 1))));
|
|
49
|
+
const y = Math.min(img.h - 1, Math.max(0, Math.round((1 - v) * (img.h - 1))));
|
|
50
|
+
const i = (y * img.w + x) * 4;
|
|
51
|
+
return [img.data[i], img.data[i + 1], img.data[i + 2]];
|
|
52
|
+
}
|
|
53
|
+
function sampleFace(img, uvs) {
|
|
54
|
+
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]];
|
|
55
|
+
let r = 0, g = 0, b = 0;
|
|
56
|
+
for (const [u, v] of pts) {
|
|
57
|
+
const c = texelAt(img, u, v);
|
|
58
|
+
r += c[0];
|
|
59
|
+
g += c[1];
|
|
60
|
+
b += c[2];
|
|
61
|
+
}
|
|
62
|
+
const n = pts.length;
|
|
63
|
+
const h = (x) => Math.round(x / n).toString(16).padStart(2, "0");
|
|
64
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
65
|
+
}
|
|
66
|
+
async function bakeTexturesNode(polygons) {
|
|
67
|
+
const cache = /* @__PURE__ */ new Map();
|
|
68
|
+
const get = async (url) => {
|
|
69
|
+
if (!cache.has(url)) cache.set(url, await decode(url));
|
|
70
|
+
return cache.get(url) ?? null;
|
|
71
|
+
};
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const p of polygons) {
|
|
74
|
+
const tt = p.textureTriangles?.[0];
|
|
75
|
+
const tex = p.texture ?? p.material?.texture ?? tt?.texture;
|
|
76
|
+
const uvs = tt?.uvs ?? (p.uvs && p.uvs.length >= 3 ? [p.uvs[0], p.uvs[1], p.uvs[2]] : void 0);
|
|
77
|
+
if (tex && uvs) {
|
|
78
|
+
const img = await get(tex);
|
|
79
|
+
if (img) {
|
|
80
|
+
out.push({ ...p, color: sampleFace(img, uvs), texture: void 0, textureTriangles: void 0, uvs: void 0 });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
out.push(p);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function hasTextures(polygons) {
|
|
89
|
+
return polygons.some((p) => p.texture || p.material?.texture || p.textureTriangles?.[0]?.texture);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/loadMeshFromFile.ts
|
|
93
|
+
function stripQuery2(url) {
|
|
94
|
+
return url.split("?")[0].split("#")[0];
|
|
95
|
+
}
|
|
96
|
+
async function siblingMtl(objPath) {
|
|
97
|
+
const clean = stripQuery2(objPath);
|
|
98
|
+
const dir = clean.replace(/[^/\\]+$/, "");
|
|
99
|
+
let candidate;
|
|
100
|
+
try {
|
|
101
|
+
const m = (await readFile2(clean, "utf8")).match(/^\s*mtllib\s+(.+?)\s*$/im);
|
|
102
|
+
candidate = m ? dir + m[1].trim() : clean.replace(/\.obj$/i, ".mtl");
|
|
103
|
+
} catch {
|
|
104
|
+
candidate = clean.replace(/\.obj$/i, ".mtl");
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await readFile2(candidate);
|
|
108
|
+
return candidate;
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function fileFetch(url) {
|
|
114
|
+
const path = stripQuery2(url);
|
|
115
|
+
try {
|
|
116
|
+
const buf = await readFile2(path);
|
|
117
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
status: 200,
|
|
121
|
+
text: async () => buf.toString("utf8"),
|
|
122
|
+
arrayBuffer: async () => ab
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return { ok: false, status: 404, text: async () => "", arrayBuffer: async () => new ArrayBuffer(0) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function loadMeshFromFile(path, options) {
|
|
129
|
+
const g = globalThis;
|
|
130
|
+
const prev = g.fetch;
|
|
131
|
+
g.fetch = fileFetch;
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
const mtlUrl = options?.mtlUrl ?? (/\.obj(\?|$)/i.test(path) ? await siblingMtl(path) : void 0);
|
|
135
|
+
result = await loadMesh(path, { solidTextureSamples: false, ...options, mtlUrl });
|
|
136
|
+
} finally {
|
|
137
|
+
g.fetch = prev;
|
|
138
|
+
}
|
|
139
|
+
if (hasTextures(result.polygons)) {
|
|
140
|
+
return { ...result, polygons: await bakeTexturesNode(result.polygons) };
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/compileFile.ts
|
|
146
|
+
import {
|
|
147
|
+
compileScene,
|
|
148
|
+
createGlyphPerspectiveCamera,
|
|
149
|
+
createGlyphOrthographicCamera,
|
|
150
|
+
cropGlyphInner
|
|
151
|
+
} from "glyphcss";
|
|
152
|
+
function worldMaxDim(polys) {
|
|
153
|
+
let mnx = Infinity, mxx = -Infinity, mny = Infinity, mxy = -Infinity, mnz = Infinity, mxz = -Infinity;
|
|
154
|
+
for (const p of polys) for (const v of p.vertices) {
|
|
155
|
+
if (v[0] < mnx) mnx = v[0];
|
|
156
|
+
if (v[0] > mxx) mxx = v[0];
|
|
157
|
+
if (v[1] < mny) mny = v[1];
|
|
158
|
+
if (v[1] > mxy) mxy = v[1];
|
|
159
|
+
if (v[2] < mnz) mnz = v[2];
|
|
160
|
+
if (v[2] > mxz) mxz = v[2];
|
|
161
|
+
}
|
|
162
|
+
if (!isFinite(mnx)) return 1;
|
|
163
|
+
return Math.max(mxx - mnx, mxy - mny, mxz - mnz, 1e-6);
|
|
164
|
+
}
|
|
165
|
+
function measureContent(inner) {
|
|
166
|
+
const lines = inner.replace(/<[^>]*>/g, "").split("\n");
|
|
167
|
+
let minC = Infinity, maxC = -1, minR = Infinity, maxR = -1;
|
|
168
|
+
lines.forEach((l, r) => {
|
|
169
|
+
for (let c = 0; c < l.length; c++) if (l[c] !== " ") {
|
|
170
|
+
if (c < minC) minC = c;
|
|
171
|
+
if (c > maxC) maxC = c;
|
|
172
|
+
if (r < minR) minR = r;
|
|
173
|
+
if (r > maxR) maxR = r;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return maxC < 0 ? { w: 0, h: 0 } : { w: maxC - minC + 1, h: maxR - minR + 1 };
|
|
177
|
+
}
|
|
178
|
+
function compilePolygons(polygons, options = {}) {
|
|
179
|
+
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 });
|
|
180
|
+
const shared = {
|
|
181
|
+
autoCenter: options.autoCenter,
|
|
182
|
+
cellAspect: options.cellAspect,
|
|
183
|
+
mode: options.mode,
|
|
184
|
+
glyphPalette: options.glyphPalette,
|
|
185
|
+
useColors: options.useColors,
|
|
186
|
+
smoothShading: options.smoothShading,
|
|
187
|
+
creaseAngle: options.creaseAngle,
|
|
188
|
+
doubleSided: options.doubleSided,
|
|
189
|
+
supersample: options.supersample
|
|
190
|
+
};
|
|
191
|
+
if (options.autoFit && options.autoFit.target > 0) {
|
|
192
|
+
const { target, by } = options.autoFit;
|
|
193
|
+
const probeZoom = 40 / worldMaxDim(polygons);
|
|
194
|
+
const probe = compileScene({ polygons, camera: buildCam(probeZoom), cols: 200, rows: 120, ...shared, autoCenter: true });
|
|
195
|
+
const m = measureContent(probe.inner);
|
|
196
|
+
if (m.w > 0 && m.h > 0) {
|
|
197
|
+
const scale = by === "rows" ? target / m.h : target / m.w;
|
|
198
|
+
const zoom = probeZoom * scale;
|
|
199
|
+
const cols = Math.ceil(m.w * scale * 1.4) + 6;
|
|
200
|
+
const rows = Math.ceil(m.h * scale * 1.4) + 6;
|
|
201
|
+
const full = compileScene({ polygons, camera: buildCam(zoom), cols, rows, ...shared, autoCenter: true });
|
|
202
|
+
const inner = cropGlyphInner(full.inner);
|
|
203
|
+
const lines = inner.split("\n");
|
|
204
|
+
const w = lines.reduce((a, l) => Math.max(a, l.replace(/<[^>]*>/g, "").length), 0);
|
|
205
|
+
return { html: `<pre class="glyph-output">${inner}</pre>`, inner, cols: w, rows: lines.length, cellAspect: options.cellAspect ?? 2 };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return compileScene({
|
|
209
|
+
polygons,
|
|
210
|
+
camera: buildCam(options.zoom),
|
|
211
|
+
cols: options.cols,
|
|
212
|
+
rows: options.rows,
|
|
213
|
+
...shared
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function compileFile(path, options = {}) {
|
|
217
|
+
const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
|
|
218
|
+
return compilePolygons(polygons, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/compileInteractive.ts
|
|
222
|
+
import {
|
|
223
|
+
buildGlyphInteractiveExport,
|
|
224
|
+
glyphCodepenPrefill
|
|
225
|
+
} from "glyphcss";
|
|
226
|
+
async function compileInteractive(path, options = {}) {
|
|
227
|
+
const { polygons } = await loadMeshFromFile(path, { meshResolution: options.meshResolution, mtlUrl: options.mtlUrl });
|
|
228
|
+
return buildGlyphInteractiveExport(polygons, options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/vite.ts
|
|
232
|
+
var MESH_RE = /\.(glb|gltf|obj|vox|stl)(\?|$)/i;
|
|
233
|
+
function hasGlyphFlag(id) {
|
|
234
|
+
const q = id.split("?")[1];
|
|
235
|
+
return q != null && new URLSearchParams(q).has("glyph");
|
|
236
|
+
}
|
|
237
|
+
function parseQuery(id) {
|
|
238
|
+
const q = id.split("?")[1] ?? "";
|
|
239
|
+
const p = new URLSearchParams(q);
|
|
240
|
+
const num = (k) => p.has(k) ? Number(p.get(k)) : void 0;
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const k of ["rotX", "rotY", "zoom", "distance", "perspective", "cols", "rows", "cellAspect", "creaseAngle", "supersample"]) {
|
|
243
|
+
const v = num(k);
|
|
244
|
+
if (v !== void 0 && Number.isFinite(v)) out[k] = v;
|
|
245
|
+
}
|
|
246
|
+
if (p.has("mode")) out.mode = p.get("mode");
|
|
247
|
+
if (p.has("palette")) out.glyphPalette = p.get("palette") ?? void 0;
|
|
248
|
+
if (p.has("projection")) out.projection = p.get("projection") === "orthographic" ? "orthographic" : "perspective";
|
|
249
|
+
if (p.has("meshResolution")) out.meshResolution = p.get("meshResolution");
|
|
250
|
+
if (p.has("colors")) out.useColors = p.get("colors") !== "0" && p.get("colors") !== "false";
|
|
251
|
+
if (p.has("autoCenter")) out.autoCenter = p.get("autoCenter") !== "0" && p.get("autoCenter") !== "false";
|
|
252
|
+
if (p.has("smoothShading")) out.smoothShading = p.get("smoothShading") !== "0" && p.get("smoothShading") !== "false";
|
|
253
|
+
if (p.has("doubleSided")) out.doubleSided = p.get("doubleSided") !== "0" && p.get("doubleSided") !== "false";
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function glyphcssCompile(globalOptions = {}) {
|
|
257
|
+
return {
|
|
258
|
+
name: "glyphcss-compile",
|
|
259
|
+
enforce: "pre",
|
|
260
|
+
async load(id) {
|
|
261
|
+
if (!MESH_RE.test(id) || !hasGlyphFlag(id)) return null;
|
|
262
|
+
const path = id.split("?")[0];
|
|
263
|
+
const opts = { ...globalOptions, ...parseQuery(id) };
|
|
264
|
+
const result = await compileFile(path, opts);
|
|
265
|
+
const meta = { cols: result.cols, rows: result.rows, cellAspect: result.cellAspect };
|
|
266
|
+
return [
|
|
267
|
+
`export default ${JSON.stringify(result.html)};`,
|
|
268
|
+
`export const inner = ${JSON.stringify(result.inner)};`,
|
|
269
|
+
`export const meta = ${JSON.stringify(meta)};`
|
|
270
|
+
].join("\n");
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
export {
|
|
275
|
+
compileFile,
|
|
276
|
+
compileInteractive,
|
|
277
|
+
compilePolygons,
|
|
278
|
+
compileScene2 as compileScene,
|
|
279
|
+
glyphcssCompile,
|
|
280
|
+
loadMeshFromFile,
|
|
281
|
+
glyphCodepenPrefill as toCodepenPrefill
|
|
282
|
+
};
|