@zkmake/three-audit 0.3.1 → 0.4.0
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/README.md +101 -4
- package/dist/audit-scene-DHTFd-zs.js +69 -0
- package/dist/audit-scene-DHTFd-zs.js.map +1 -0
- package/dist/{audit-scene-Da-8SA1x.js → budgets-DQggDhEu.js} +80 -69
- package/dist/budgets-DQggDhEu.js.map +1 -0
- package/dist/budgets-Dl-AOd9T.d.ts +220 -0
- package/dist/cli.js +84 -43
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -173
- package/dist/index.js +9 -14
- package/dist/index.js.map +1 -1
- package/dist/{load-nqfCU3-k.js → load-D4bV0-2t.js} +3 -5
- package/dist/{load-nqfCU3-k.js.map → load-D4bV0-2t.js.map} +1 -1
- package/dist/node.d.ts +0 -4
- package/dist/node.js +1 -2
- package/dist/report-lines-Myjl7bcq.js +23 -0
- package/dist/report-lines-Myjl7bcq.js.map +1 -0
- package/dist/vitest.d.ts +54 -0
- package/dist/vitest.js +90 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +15 -6
- package/dist/audit-scene-Da-8SA1x.js.map +0 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { BatchedMesh, BufferGeometry, Mesh, Object3D } from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Any three.js object, from whichever copy of three's types the app has. The checks take this, so
|
|
4
|
+
* an app on a newer `@types/three` than this package was built against passes its scene as is.
|
|
5
|
+
*/
|
|
6
|
+
type AnyObject3D = {
|
|
7
|
+
readonly isObject3D: true;
|
|
8
|
+
readonly children: readonly object[];
|
|
9
|
+
};
|
|
10
|
+
/** Leave an object and everything under it out of a check. */
|
|
11
|
+
type Skip = (object: Object3D) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* What a check sees of a bake (`@zkmake/three-batch`'s `bake` and `<Baked>`), which hides its
|
|
14
|
+
* parts and draws one merged mesh per material instead. `sources`: the parts, as if shown, and not
|
|
15
|
+
* the merge, so rows name real parts. `merged`: what the renderer draws.
|
|
16
|
+
*/
|
|
17
|
+
type Bakes = "sources" | "merged";
|
|
18
|
+
/** Triangles one draw of this geometry makes: indexed or not, within its draw range. */
|
|
19
|
+
declare const triangleCount: (geometry: BufferGeometry) => number;
|
|
20
|
+
type BadGeometryOptions = {
|
|
21
|
+
skip?: Skip | undefined;
|
|
22
|
+
/** `userData` key naming the part a mesh belongs to, for the `under` column. */
|
|
23
|
+
tagKey?: string;
|
|
24
|
+
/** A bake's parts (default), or its merged meshes: see `Bakes`. Not both, or each counts twice. */
|
|
25
|
+
bakes?: Bakes | undefined;
|
|
26
|
+
};
|
|
27
|
+
type BadGeometryRow = {
|
|
28
|
+
mesh: string;
|
|
29
|
+
/** The tagged part it sits in, or its parent's name. */
|
|
30
|
+
under: string;
|
|
31
|
+
/** Vertices with a NaN or infinite position or normal component. */
|
|
32
|
+
nonFinite: number;
|
|
33
|
+
/** Vertices of triangles with area whose normal has no length. */
|
|
34
|
+
zeroNormals: number;
|
|
35
|
+
/** Not enumerable: kept out of JSON and `console.table`. */
|
|
36
|
+
readonly object: Mesh;
|
|
37
|
+
};
|
|
38
|
+
/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */
|
|
39
|
+
declare const findBadGeometry: (root: AnyObject3D, options?: BadGeometryOptions) => BadGeometryRow[];
|
|
40
|
+
type EmptyMeshRow = {
|
|
41
|
+
/** `path/to/mesh [material]`. */
|
|
42
|
+
mesh: string;
|
|
43
|
+
/** Not enumerable: kept out of JSON and `console.table`. */
|
|
44
|
+
readonly object: Mesh;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw
|
|
48
|
+
* nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't
|
|
49
|
+
* count: that's usually a pool waiting to fill.
|
|
50
|
+
*/
|
|
51
|
+
declare const findEmptyMeshes: (root: AnyObject3D, options?: BadGeometryOptions) => EmptyMeshRow[];
|
|
52
|
+
type TriangleOptions = {
|
|
53
|
+
skip?: Skip | undefined;
|
|
54
|
+
};
|
|
55
|
+
type MeshRow = {
|
|
56
|
+
/** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */
|
|
57
|
+
name: string;
|
|
58
|
+
material: string;
|
|
59
|
+
/** Triangles in one copy. A batch's is an average: `geometryCensus` breaks it down. */
|
|
60
|
+
triangles: number;
|
|
61
|
+
instances: number;
|
|
62
|
+
total: number;
|
|
63
|
+
/** Not enumerable: kept out of JSON and `console.table`. */
|
|
64
|
+
readonly mesh: Mesh;
|
|
65
|
+
};
|
|
66
|
+
type GeometryRow = {
|
|
67
|
+
/**
|
|
68
|
+
* `geometry.name`, or its type and the start of its uuid. For a geometry in a batch, the batch's
|
|
69
|
+
* `label` for it, or `batch#id`.
|
|
70
|
+
*/
|
|
71
|
+
geometry: string;
|
|
72
|
+
/** Copies drawn: every mesh using it, instances counted. */
|
|
73
|
+
uses: number;
|
|
74
|
+
triangles: number;
|
|
75
|
+
total: number;
|
|
76
|
+
/** Its part of the census's total, 0–1. */
|
|
77
|
+
share: number;
|
|
78
|
+
/** With `budget`: whether `share` is over it. */
|
|
79
|
+
overBudget?: boolean;
|
|
80
|
+
};
|
|
81
|
+
type CensusOptions = TriangleOptions & {
|
|
82
|
+
/**
|
|
83
|
+
* Names a geometry inside a batch, e.g. from the app's own table of what it added. Default
|
|
84
|
+
* `batch#id`, the batch named by its path.
|
|
85
|
+
*/
|
|
86
|
+
label?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;
|
|
87
|
+
/** Flag rows whose share of the total is over this, 0–1: `0.2` for anything over 20%. */
|
|
88
|
+
budget?: number | undefined;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass
|
|
92
|
+
* adds roughly as much again for each shadow-casting light.
|
|
93
|
+
*/
|
|
94
|
+
declare const countTriangles: (root: AnyObject3D, options?: TriangleOptions) => number;
|
|
95
|
+
/**
|
|
96
|
+
* The triangles in a geometry (one draw, within its draw range: `triangleCount`) or that an object
|
|
97
|
+
* draws (visible meshes, instances counted: `countTriangles`). The one helper a test needs.
|
|
98
|
+
*/
|
|
99
|
+
declare const triangles: (target: AnyObject3D | {
|
|
100
|
+
readonly isBufferGeometry: true;
|
|
101
|
+
}, options?: TriangleOptions) => number;
|
|
102
|
+
/** Every visible mesh, most triangles drawn first. */
|
|
103
|
+
declare const listMeshes: (scene: AnyObject3D, options?: TriangleOptions) => MeshRow[];
|
|
104
|
+
/**
|
|
105
|
+
* Geometries by triangles drawn, hidden meshes included, most first. Name geometries
|
|
106
|
+
* (`geometry.name`) to make this readable. A batch counts each geometry in it separately, by its
|
|
107
|
+
* visible instances: the row a budget is really spent on.
|
|
108
|
+
*/
|
|
109
|
+
declare const geometryCensus: (root: AnyObject3D, options?: CensusOptions) => GeometryRow[];
|
|
110
|
+
/**
|
|
111
|
+
* Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material
|
|
112
|
+
* group for a mesh with several materials. Before frustum culling; each shadow-casting light adds
|
|
113
|
+
* one per caster. `recordDrawLedger` counts the real thing.
|
|
114
|
+
*/
|
|
115
|
+
declare const countDraws: (root: AnyObject3D, options?: TriangleOptions) => number;
|
|
116
|
+
type ZFightingOptions = {
|
|
117
|
+
/**
|
|
118
|
+
* How close two planes count as one, in world units. 0.004 (4 mm at 1 unit = 1 m) is about what
|
|
119
|
+
* depth precision loses with a camera ~50 m out. Scale it with your units.
|
|
120
|
+
*/
|
|
121
|
+
gap?: number;
|
|
122
|
+
/** Also compare triangles within one mesh: for merged geometry whose parts share a buffer. */
|
|
123
|
+
self?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Only meshes inside a tagged object (`userData[tagKey]`), so a studio's ground and props stay
|
|
126
|
+
* out. Defaults to true when anything under the root is tagged, false otherwise.
|
|
127
|
+
*/
|
|
128
|
+
tagged?: boolean;
|
|
129
|
+
tagKey?: string;
|
|
130
|
+
skip?: Skip | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* A bake's parts (default), or its merged meshes: see `Bakes`. The parts name what to fix; the
|
|
133
|
+
* merge is the same faces under one name.
|
|
134
|
+
*/
|
|
135
|
+
bakes?: Bakes | undefined;
|
|
136
|
+
};
|
|
137
|
+
type ZFightingRow = {
|
|
138
|
+
/** `path/to/mesh [material] @ (x, y, z)`: named ancestors, and the centre in world space. */
|
|
139
|
+
a: string;
|
|
140
|
+
b: string;
|
|
141
|
+
/** Overlapping triangle pairs between the two. */
|
|
142
|
+
triangles: number;
|
|
143
|
+
/**
|
|
144
|
+
* The planes they share, most overlap first: `z = -3.000, facing +z` when axis-aligned, else
|
|
145
|
+
* `(nx, ny, nz) · p = d`. Both faces point along the normal: opposite-facing faces never pair.
|
|
146
|
+
*/
|
|
147
|
+
planes: string[];
|
|
148
|
+
/** Where they overlap: the area in world units², and its centre in world space. */
|
|
149
|
+
overlap: {
|
|
150
|
+
area: number;
|
|
151
|
+
centre: [number, number, number];
|
|
152
|
+
};
|
|
153
|
+
/** Identical pairs (same labels, triangles and planes) folded into this row. */
|
|
154
|
+
count: number;
|
|
155
|
+
/** Not enumerable: kept out of JSON and `console.table`. The first pair folded in. */
|
|
156
|
+
readonly meshes: readonly [Mesh, Mesh];
|
|
157
|
+
/** Not enumerable: every pair folded in. */
|
|
158
|
+
readonly pairs: readonly (readonly [Mesh, Mesh])[];
|
|
159
|
+
};
|
|
160
|
+
/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */
|
|
161
|
+
declare const findZFighting: (scene: AnyObject3D, options?: ZFightingOptions) => ZFightingRow[];
|
|
162
|
+
/** What one frame may cost, all parts together. */
|
|
163
|
+
type FrameBudget = {
|
|
164
|
+
/** The frame rate to hold: three-meter's `targetFps`. */
|
|
165
|
+
fps?: number | undefined;
|
|
166
|
+
/** Draw calls a frame makes. */
|
|
167
|
+
calls?: number | undefined;
|
|
168
|
+
triangles?: number | undefined;
|
|
169
|
+
/** Texture memory in MB. No reader checks it yet; kept for a perf bench. */
|
|
170
|
+
textureMB?: number | undefined;
|
|
171
|
+
};
|
|
172
|
+
/** What one part (a model, a prop, a kart) may cost. */
|
|
173
|
+
type PartBudget = {
|
|
174
|
+
triangles?: number | undefined;
|
|
175
|
+
/** Draw calls one main pass makes for it: see `countDraws`. */
|
|
176
|
+
draws?: number | undefined;
|
|
177
|
+
/** Visible meshes in it. */
|
|
178
|
+
meshes?: number | undefined;
|
|
179
|
+
};
|
|
180
|
+
type BudgetsInput<Part extends string = string> = {
|
|
181
|
+
frame: FrameBudget;
|
|
182
|
+
parts?: Record<Part, PartBudget>;
|
|
183
|
+
};
|
|
184
|
+
/** `defineBudgets`'s result: frozen, every part named in its type. */
|
|
185
|
+
type Budgets<Part extends string = string> = {
|
|
186
|
+
readonly frame: Readonly<FrameBudget>;
|
|
187
|
+
readonly parts: { readonly [Name in Part]: Readonly<PartBudget>; };
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Checks budgets read from anywhere (a JSON file, a module) and returns them frozen. Throws a
|
|
191
|
+
* `TypeError` naming the first bad key.
|
|
192
|
+
*/
|
|
193
|
+
declare const parseBudgets: (value: unknown) => Budgets;
|
|
194
|
+
/**
|
|
195
|
+
* The app's budgets, typed and frozen: one file every reader imports.
|
|
196
|
+
*
|
|
197
|
+
* export default defineBudgets({
|
|
198
|
+
* frame: { fps: 60, calls: 150, triangles: 250_000 },
|
|
199
|
+
* parts: { kart: { triangles: 7_500, draws: 12 } },
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* `budgets.parts.kart` is typed, and a part not listed is a type error.
|
|
203
|
+
*/
|
|
204
|
+
declare const defineBudgets: <const Part extends string = never>(budgets: BudgetsInput<Part>) => Budgets<Part>;
|
|
205
|
+
type BudgetMetric = "triangles" | "draws" | "meshes";
|
|
206
|
+
type BudgetCheck = {
|
|
207
|
+
metric: BudgetMetric;
|
|
208
|
+
actual: number;
|
|
209
|
+
limit: number;
|
|
210
|
+
over: boolean;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* An object measured against a part budget (or the frame's `triangles` and `calls`), one row per
|
|
214
|
+
* limit the budget sets. Nothing over is a pass.
|
|
215
|
+
*/
|
|
216
|
+
declare const checkBudget: (root: AnyObject3D, budget: PartBudget | FrameBudget, options?: {
|
|
217
|
+
skip?: Skip | undefined;
|
|
218
|
+
}) => BudgetCheck[];
|
|
219
|
+
export { triangleCount as A, BadGeometryRow as C, AnyObject3D as D, findEmptyMeshes as E, Bakes as O, BadGeometryOptions as S, findBadGeometry as T, countDraws as _, FrameBudget as a, listMeshes as b, defineBudgets as c, ZFightingRow as d, findZFighting as f, TriangleOptions as g, MeshRow as h, BudgetsInput as i, Skip as k, parseBudgets as l, GeometryRow as m, BudgetMetric as n, PartBudget as o, CensusOptions as p, Budgets as r, checkBudget as s, BudgetCheck as t, ZFightingOptions as u, countTriangles as v, EmptyMeshRow as w, triangles as x, geometryCensus as y };
|
|
220
|
+
//# sourceMappingURL=budgets-Dl-AOd9T.d.ts.map
|
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as
|
|
3
|
-
import { t as
|
|
4
|
-
import {
|
|
2
|
+
import { r as parseBudgets, t as checkBudget } from "./budgets-DQggDhEu.js";
|
|
3
|
+
import { t as auditScene } from "./audit-scene-DHTFd-zs.js";
|
|
4
|
+
import { t as loadModel } from "./load-D4bV0-2t.js";
|
|
5
|
+
import { a as meshLines, i as emptyMeshLines, n as budgetLines, o as plural, r as count, s as zFightingLines, t as badGeometryLines } from "./report-lines-Myjl7bcq.js";
|
|
6
|
+
import { basename, extname, resolve } from "node:path";
|
|
5
7
|
import { readFileSync } from "node:fs";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
6
9
|
import { parseArgs } from "node:util";
|
|
7
|
-
|
|
10
|
+
// src/cli/main.ts
|
|
8
11
|
/**
|
|
9
12
|
* `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and
|
|
10
|
-
* NaN geometry. Exits 1 when a check fails, so it
|
|
13
|
+
* NaN geometry, and with `--budgets` against the app's budgets. Exits 1 when a check fails, so it
|
|
14
|
+
* can gate CI; `--json` for machines.
|
|
11
15
|
*/
|
|
12
16
|
const HELP = `Usage: three-audit <model.glb|model.gltf>… [options]
|
|
13
17
|
|
|
14
18
|
Checks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and
|
|
15
|
-
geometry that renders NaN (non-finite positions or normals, zero-length normals)
|
|
16
|
-
any model fails a check, 2 when a model
|
|
19
|
+
geometry that renders NaN (non-finite positions or normals, zero-length normals), and with
|
|
20
|
+
--budgets, that each fits its budget. Exits 1 when any model fails a check, 2 when a model or the
|
|
21
|
+
budgets can't be read.
|
|
17
22
|
|
|
18
23
|
Options:
|
|
19
|
-
--gap <m>
|
|
20
|
-
--self
|
|
21
|
-
--skip <regex>
|
|
22
|
-
--
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
--gap <m> How close two planes count as one, in metres (default 0.004)
|
|
25
|
+
--self Also check for z-fighting within each mesh (merged geometry)
|
|
26
|
+
--skip <regex> Leave out objects whose name matches, and everything under them (decals)
|
|
27
|
+
--budgets <file> The app's budgets: JSON, or a module whose default export is them
|
|
28
|
+
(defineBudgets). Each model is held to the part named like its file
|
|
29
|
+
(kart.glb: parts.kart) and to the frame's triangles and calls
|
|
30
|
+
--part <name> Hold every model to this part's budget instead
|
|
31
|
+
--top <n> List the n costliest meshes (default 5, 0 for none)
|
|
32
|
+
--json Print the reports as JSON
|
|
33
|
+
--no-fail Exit 0 even when a check fails
|
|
34
|
+
-h, --help Show this help
|
|
35
|
+
-v, --version Show the version
|
|
27
36
|
|
|
28
37
|
Draco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;
|
|
29
38
|
/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */
|
|
@@ -34,33 +43,45 @@ const version = () => {
|
|
|
34
43
|
} catch {}
|
|
35
44
|
return "unknown";
|
|
36
45
|
};
|
|
37
|
-
const
|
|
38
|
-
const printReport = (file, report, top) => {
|
|
46
|
+
const printReport = (file, report, top, budget) => {
|
|
39
47
|
const lines = [`${basename(file)} ${count(report.triangles)} triangles, ${count(report.draws)} draws`];
|
|
40
48
|
if (report.zFighting.length === 0) lines.push(" ✓ no z-fighting");
|
|
41
|
-
else {
|
|
42
|
-
lines.push(` ✗ ${report.zFighting.length} z-fighting pair${report.zFighting.length === 1 ? "" : "s"}`);
|
|
43
|
-
for (const row of report.zFighting) {
|
|
44
|
-
const more = row.planes.length > 1 ? ` (+${row.planes.length - 1} more)` : "";
|
|
45
|
-
const copies = row.count > 1 ? `, ×${row.count} identical pairs` : "";
|
|
46
|
-
lines.push(` ${row.a}`, ` ↔ ${row.b}`, ` ${row.triangles} triangles in ${row.planes[0]}${more}; overlap ${row.overlap.area} m² at (${row.overlap.centre.join(", ")})${copies}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
+
else lines.push(` ✗ ${report.zFighting.length} z-fighting ${plural(report.zFighting.length, "pair")}`, ...zFightingLines(report.zFighting));
|
|
49
50
|
if (report.badGeometry.length === 0) lines.push(" ✓ no NaN geometry");
|
|
50
|
-
else {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
lines.push(
|
|
56
|
-
for (const row of report.emptyMeshes) lines.push(` ${row.mesh}`);
|
|
57
|
-
}
|
|
58
|
-
if (top > 0 && report.meshes.length > 0) {
|
|
59
|
-
lines.push(" most triangles:");
|
|
60
|
-
for (const row of report.meshes) lines.push(` ${count(row.total).padStart(9)} ${row.name}${row.instances > 1 ? ` ×${row.instances}` : ""}`);
|
|
51
|
+
else lines.push(` ✗ ${report.badGeometry.length} ${plural(report.badGeometry.length, "mesh", "meshes")} with NaN geometry`, ...badGeometryLines(report.badGeometry));
|
|
52
|
+
if (report.emptyMeshes.length > 0) lines.push(` ! ${report.emptyMeshes.length} empty ${plural(report.emptyMeshes.length, "mesh", "meshes")} (no vertices)`, ...emptyMeshLines(report.emptyMeshes));
|
|
53
|
+
if (budget) {
|
|
54
|
+
const name = budget.part === null ? "the frame budget" : `budget "${budget.part}"`;
|
|
55
|
+
if (budget.checks.length === 0) lines.push(` ! no budget for ${basename(file)}${budget.part === null ? ": name a part with --part" : ` in part "${budget.part}"`}`);
|
|
56
|
+
else lines.push(budget.checks.some((check) => check.over) ? ` ✗ over ${name}` : ` ✓ within ${name}`, ...budgetLines(budget.checks));
|
|
61
57
|
}
|
|
58
|
+
if (top > 0 && report.meshes.length > 0) lines.push(" most triangles:", ...meshLines(report.meshes));
|
|
62
59
|
return lines.join("\n");
|
|
63
60
|
};
|
|
61
|
+
/** A budgets file: JSON, or a module whose default export (or the module itself) is the budgets. */
|
|
62
|
+
const readBudgets = async (file) => {
|
|
63
|
+
const path = resolve(file);
|
|
64
|
+
if (extname(path) === ".json") return parseBudgets(JSON.parse(readFileSync(path, "utf8")));
|
|
65
|
+
const module = await import(pathToFileURL(path).href);
|
|
66
|
+
return parseBudgets(module.default ?? module);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The model against its part's budget (`--part`, else the part named like the file) and the
|
|
70
|
+
* frame's triangles and calls, which no one model may pass either.
|
|
71
|
+
*/
|
|
72
|
+
const budgetOf = (scene, file, budgets, part, skip) => {
|
|
73
|
+
const stem = basename(file, extname(file));
|
|
74
|
+
const name = part ?? (Object.hasOwn(budgets.parts, stem) ? stem : null);
|
|
75
|
+
const own = name === null ? [] : checkBudget(scene, budgets.parts[name], { skip });
|
|
76
|
+
const frame = checkBudget(scene, {
|
|
77
|
+
triangles: budgets.frame.triangles,
|
|
78
|
+
calls: budgets.frame.calls
|
|
79
|
+
}, { skip });
|
|
80
|
+
return {
|
|
81
|
+
part: name,
|
|
82
|
+
checks: [...own, ...frame.filter((check) => !own.some((mine) => mine.metric === check.metric))]
|
|
83
|
+
};
|
|
84
|
+
};
|
|
64
85
|
const main = async (argv) => {
|
|
65
86
|
let parsed;
|
|
66
87
|
try {
|
|
@@ -68,6 +89,8 @@ const main = async (argv) => {
|
|
|
68
89
|
args: argv,
|
|
69
90
|
allowPositionals: true,
|
|
70
91
|
options: {
|
|
92
|
+
budgets: { type: "string" },
|
|
93
|
+
part: { type: "string" },
|
|
71
94
|
gap: { type: "string" },
|
|
72
95
|
self: {
|
|
73
96
|
type: "boolean",
|
|
@@ -118,7 +141,26 @@ const main = async (argv) => {
|
|
|
118
141
|
console.error("three-audit: --gap must be a positive number and --top zero or more");
|
|
119
142
|
return 2;
|
|
120
143
|
}
|
|
144
|
+
if (values.part !== void 0 && values.budgets === void 0) {
|
|
145
|
+
console.error("three-audit: --part needs --budgets");
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
let budgets;
|
|
149
|
+
if (values.budgets !== void 0) {
|
|
150
|
+
try {
|
|
151
|
+
budgets = await readBudgets(values.budgets);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(`three-audit: couldn't read ${values.budgets}: ${error.message}`);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
if (values.part !== void 0 && !Object.hasOwn(budgets.parts, values.part)) {
|
|
157
|
+
const known = Object.keys(budgets.parts);
|
|
158
|
+
console.error(`three-audit: no part "${values.part}" in ${values.budgets}${known.length > 0 ? ` (it has ${known.join(", ")})` : ""}`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
121
162
|
const skipPattern = values.skip === void 0 ? void 0 : new RegExp(values.skip);
|
|
163
|
+
const skip = skipPattern ? (object) => skipPattern.test(object.name) : void 0;
|
|
122
164
|
const results = [];
|
|
123
165
|
for (const file of positionals) try {
|
|
124
166
|
const scene = await loadModel(file);
|
|
@@ -126,11 +168,12 @@ const main = async (argv) => {
|
|
|
126
168
|
top,
|
|
127
169
|
...gap === void 0 ? {} : { gap },
|
|
128
170
|
...values.self ? { self: true } : {},
|
|
129
|
-
...
|
|
171
|
+
...skip ? { skip } : {}
|
|
130
172
|
});
|
|
131
173
|
results.push({
|
|
132
174
|
file,
|
|
133
|
-
report
|
|
175
|
+
report,
|
|
176
|
+
...budgets ? { budget: budgetOf(scene, file, budgets, values.part, skip) } : {}
|
|
134
177
|
});
|
|
135
178
|
} catch (error) {
|
|
136
179
|
results.push({
|
|
@@ -139,14 +182,12 @@ const main = async (argv) => {
|
|
|
139
182
|
});
|
|
140
183
|
}
|
|
141
184
|
if (values.json) console.log(JSON.stringify(results, null, 2));
|
|
142
|
-
else console.log(results.map(({ file, report, error }) => report ? printReport(file, report, top) : `${basename(file)}\n ✗ couldn't read: ${error}`).join("\n\n"));
|
|
185
|
+
else console.log(results.map(({ file, report, budget, error }) => report ? printReport(file, report, top, budget) : `${basename(file)}\n ✗ couldn't read: ${error}`).join("\n\n"));
|
|
143
186
|
if (results.some((result) => result.error !== void 0)) return 2;
|
|
144
|
-
return results.some(({ report }) => report.zFighting.length > 0 || report.badGeometry.length > 0) && !values["no-fail"] ? 1 : 0;
|
|
187
|
+
return results.some(({ report, budget }) => report.zFighting.length > 0 || report.badGeometry.length > 0 || budget?.checks.some((check) => check.over) === true) && !values["no-fail"] ? 1 : 0;
|
|
145
188
|
};
|
|
146
|
-
|
|
147
|
-
//#region src/cli.ts
|
|
189
|
+
// src/cli.ts
|
|
148
190
|
/** The `three-audit` bin. The work is in `cli/main.ts`, which the tests import. */
|
|
149
191
|
process.exitCode = await main(process.argv.slice(2));
|
|
150
|
-
//#endregion
|
|
151
192
|
|
|
152
193
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and\n * NaN geometry. Exits 1 when a check fails, so it can gate CI; `--json` for machines.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { type AuditReport, auditScene } from \"../audit-scene.ts\";\nimport { loadModel } from \"../node/load.ts\";\n\nconst HELP = `Usage: three-audit <model.glb|model.gltf>… [options]\n\nChecks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and\ngeometry that renders NaN (non-finite positions or normals, zero-length normals). Exits 1 when\nany model fails a check, 2 when a model can't be read.\n\nOptions:\n --gap <m> How close two planes count as one, in metres (default 0.004)\n --self Also check for z-fighting within each mesh (merged geometry)\n --skip <regex> Leave out objects whose name matches, and everything under them (decals)\n --top <n> List the n costliest meshes (default 5, 0 for none)\n --json Print the reports as JSON\n --no-fail Exit 0 even when a check fails\n -h, --help Show this help\n -v, --version Show the version\n\nDraco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-audit\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\nconst count = (value: number) => value.toLocaleString(\"en-US\");\n\nconst printReport = (file: string, report: AuditReport, top: number) => {\n const lines = [\n `${basename(file)} ${count(report.triangles)} triangles, ${count(report.draws)} draws`,\n ];\n\n if (report.zFighting.length === 0) {\n lines.push(\" ✓ no z-fighting\");\n } else {\n lines.push(\n ` ✗ ${report.zFighting.length} z-fighting pair${report.zFighting.length === 1 ? \"\" : \"s\"}`,\n );\n\n for (const row of report.zFighting) {\n const more = row.planes.length > 1 ? ` (+${row.planes.length - 1} more)` : \"\";\n const copies = row.count > 1 ? `, ×${row.count} identical pairs` : \"\";\n\n lines.push(\n ` ${row.a}`,\n ` ↔ ${row.b}`,\n ` ${row.triangles} triangles in ${row.planes[0]}${more}; overlap ${row.overlap.area} m² at (${row.overlap.centre.join(\", \")})${copies}`,\n );\n }\n }\n\n if (report.badGeometry.length === 0) {\n lines.push(\" ✓ no NaN geometry\");\n } else {\n lines.push(\n ` ✗ ${report.badGeometry.length} mesh${report.badGeometry.length === 1 ? \"\" : \"es\"} with NaN geometry`,\n );\n\n for (const row of report.badGeometry) {\n lines.push(\n ` ${row.mesh}${row.under ? ` (${row.under})` : \"\"}: ${row.nonFinite} non-finite, ${row.zeroNormals} zero normals`,\n );\n }\n }\n\n if (report.emptyMeshes.length > 0) {\n lines.push(\n ` ! ${report.emptyMeshes.length} empty mesh${report.emptyMeshes.length === 1 ? \"\" : \"es\"} (no vertices)`,\n );\n\n for (const row of report.emptyMeshes) {\n lines.push(` ${row.mesh}`);\n }\n }\n\n if (top > 0 && report.meshes.length > 0) {\n lines.push(\" most triangles:\");\n\n for (const row of report.meshes) {\n lines.push(\n ` ${count(row.total).padStart(9)} ${row.name}${row.instances > 1 ? ` ×${row.instances}` : \"\"}`,\n );\n }\n }\n\n return lines.join(\"\\n\");\n};\n\nexport const main = async (argv: string[]): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n gap: { type: \"string\" },\n self: { type: \"boolean\", default: false },\n skip: { type: \"string\" },\n top: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n \"no-fail\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-audit: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n if (positionals.length === 0) {\n console.error(HELP);\n\n return 2;\n }\n\n const gap = values.gap === undefined ? undefined : Number(values.gap);\n const top = values.top === undefined ? 5 : Number(values.top);\n\n if ((gap !== undefined && !(gap > 0)) || !(top >= 0)) {\n console.error(\"three-audit: --gap must be a positive number and --top zero or more\");\n\n return 2;\n }\n\n const skipPattern = values.skip === undefined ? undefined : new RegExp(values.skip);\n const results: { file: string; report?: AuditReport; error?: string }[] = [];\n\n for (const file of positionals) {\n try {\n const scene = await loadModel(file);\n const report = auditScene(scene, {\n top,\n ...(gap === undefined ? {} : { gap }),\n ...(values.self ? { self: true } : {}),\n ...(skipPattern ? { skip: (object) => skipPattern.test(object.name) } : {}),\n });\n\n results.push({ file, report });\n } catch (error) {\n results.push({ file, error: (error as Error).message });\n }\n }\n\n if (values.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n console.log(\n results\n .map(({ file, report, error }) =>\n report\n ? printReport(file, report, top)\n : `${basename(file)}\\n ✗ couldn't read: ${error}`,\n )\n .join(\"\\n\\n\"),\n );\n }\n\n if (results.some((result) => result.error !== undefined)) {\n return 2;\n }\n\n const failed = results.some(\n ({ report }) => report!.zFighting.length > 0 || report!.badGeometry.length > 0,\n );\n\n return failed && !values[\"no-fail\"] ? 1 : 0;\n};\n","#!/usr/bin/env node\n/** The `three-audit` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and\n * NaN geometry, and with `--budgets` against the app's budgets. Exits 1 when a check fails, so it\n * can gate CI; `--json` for machines.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync } from \"node:fs\";\nimport { basename, extname, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nimport { type AuditReport, auditScene } from \"../audit-scene.ts\";\nimport { type BudgetCheck, type Budgets, checkBudget, parseBudgets } from \"../budgets.ts\";\nimport { loadModel } from \"../node/load.ts\";\nimport {\n badGeometryLines,\n budgetLines,\n count,\n emptyMeshLines,\n meshLines,\n plural,\n zFightingLines,\n} from \"../report-lines.ts\";\nimport type { AnyObject3D, Skip } from \"../scene.ts\";\n\nconst HELP = `Usage: three-audit <model.glb|model.gltf>… [options]\n\nChecks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and\ngeometry that renders NaN (non-finite positions or normals, zero-length normals), and with\n--budgets, that each fits its budget. Exits 1 when any model fails a check, 2 when a model or the\nbudgets can't be read.\n\nOptions:\n --gap <m> How close two planes count as one, in metres (default 0.004)\n --self Also check for z-fighting within each mesh (merged geometry)\n --skip <regex> Leave out objects whose name matches, and everything under them (decals)\n --budgets <file> The app's budgets: JSON, or a module whose default export is them\n (defineBudgets). Each model is held to the part named like its file\n (kart.glb: parts.kart) and to the frame's triangles and calls\n --part <name> Hold every model to this part's budget instead\n --top <n> List the n costliest meshes (default 5, 0 for none)\n --json Print the reports as JSON\n --no-fail Exit 0 even when a check fails\n -h, --help Show this help\n -v, --version Show the version\n\nDraco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-audit\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\ntype Budgeted = {\n /** The part whose budget applied, `null` for the frame's alone. */\n part: string | null;\n checks: BudgetCheck[];\n};\n\ntype Result = { file: string; report?: AuditReport; budget?: Budgeted; error?: string };\n\nconst printReport = (file: string, report: AuditReport, top: number, budget?: Budgeted) => {\n const lines = [\n `${basename(file)} ${count(report.triangles)} triangles, ${count(report.draws)} draws`,\n ];\n\n if (report.zFighting.length === 0) {\n lines.push(\" ✓ no z-fighting\");\n } else {\n lines.push(\n ` ✗ ${report.zFighting.length} z-fighting ${plural(report.zFighting.length, \"pair\")}`,\n ...zFightingLines(report.zFighting),\n );\n }\n\n if (report.badGeometry.length === 0) {\n lines.push(\" ✓ no NaN geometry\");\n } else {\n lines.push(\n ` ✗ ${report.badGeometry.length} ${plural(report.badGeometry.length, \"mesh\", \"meshes\")} with NaN geometry`,\n ...badGeometryLines(report.badGeometry),\n );\n }\n\n if (report.emptyMeshes.length > 0) {\n lines.push(\n ` ! ${report.emptyMeshes.length} empty ${plural(report.emptyMeshes.length, \"mesh\", \"meshes\")} (no vertices)`,\n ...emptyMeshLines(report.emptyMeshes),\n );\n }\n\n if (budget) {\n const name = budget.part === null ? \"the frame budget\" : `budget \"${budget.part}\"`;\n\n if (budget.checks.length === 0) {\n lines.push(\n ` ! no budget for ${basename(file)}${budget.part === null ? \": name a part with --part\" : ` in part \"${budget.part}\"`}`,\n );\n } else {\n lines.push(\n budget.checks.some((check) => check.over) ? ` ✗ over ${name}` : ` ✓ within ${name}`,\n ...budgetLines(budget.checks),\n );\n }\n }\n\n if (top > 0 && report.meshes.length > 0) {\n lines.push(\" most triangles:\", ...meshLines(report.meshes));\n }\n\n return lines.join(\"\\n\");\n};\n\n/** A budgets file: JSON, or a module whose default export (or the module itself) is the budgets. */\nconst readBudgets = async (file: string) => {\n const path = resolve(file);\n\n if (extname(path) === \".json\") {\n return parseBudgets(JSON.parse(readFileSync(path, \"utf8\")));\n }\n\n const module = (await import(pathToFileURL(path).href)) as { default?: unknown };\n\n return parseBudgets(module.default ?? module);\n};\n\n/**\n * The model against its part's budget (`--part`, else the part named like the file) and the\n * frame's triangles and calls, which no one model may pass either.\n */\nconst budgetOf = (\n scene: AnyObject3D,\n file: string,\n budgets: Budgets,\n part: string | undefined,\n skip: Skip | undefined,\n) => {\n const stem = basename(file, extname(file));\n const name = part ?? (Object.hasOwn(budgets.parts, stem) ? stem : null);\n const own = name === null ? [] : checkBudget(scene, budgets.parts[name]!, { skip });\n const frame = checkBudget(\n scene,\n { triangles: budgets.frame.triangles, calls: budgets.frame.calls },\n { skip },\n );\n // A part's limit is the tighter one; the frame's still applies to what the part leaves unset.\n const checks = [\n ...own,\n ...frame.filter((check) => !own.some((mine) => mine.metric === check.metric)),\n ];\n\n return { part: name, checks };\n};\n\nexport const main = async (argv: string[]): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n budgets: { type: \"string\" },\n part: { type: \"string\" },\n gap: { type: \"string\" },\n self: { type: \"boolean\", default: false },\n skip: { type: \"string\" },\n top: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n \"no-fail\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-audit: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n if (positionals.length === 0) {\n console.error(HELP);\n\n return 2;\n }\n\n const gap = values.gap === undefined ? undefined : Number(values.gap);\n const top = values.top === undefined ? 5 : Number(values.top);\n\n if ((gap !== undefined && !(gap > 0)) || !(top >= 0)) {\n console.error(\"three-audit: --gap must be a positive number and --top zero or more\");\n\n return 2;\n }\n\n if (values.part !== undefined && values.budgets === undefined) {\n console.error(\"three-audit: --part needs --budgets\");\n\n return 2;\n }\n\n let budgets: Budgets | undefined;\n\n if (values.budgets !== undefined) {\n try {\n budgets = await readBudgets(values.budgets);\n } catch (error) {\n console.error(`three-audit: couldn't read ${values.budgets}: ${(error as Error).message}`);\n\n return 2;\n }\n\n if (values.part !== undefined && !Object.hasOwn(budgets.parts, values.part)) {\n const known = Object.keys(budgets.parts);\n\n console.error(\n `three-audit: no part \"${values.part}\" in ${values.budgets}${known.length > 0 ? ` (it has ${known.join(\", \")})` : \"\"}`,\n );\n\n return 2;\n }\n }\n\n const skipPattern = values.skip === undefined ? undefined : new RegExp(values.skip);\n const skip: Skip | undefined = skipPattern\n ? (object) => skipPattern.test(object.name)\n : undefined;\n const results: Result[] = [];\n\n for (const file of positionals) {\n try {\n const scene = await loadModel(file);\n const report = auditScene(scene, {\n top,\n ...(gap === undefined ? {} : { gap }),\n ...(values.self ? { self: true } : {}),\n ...(skip ? { skip } : {}),\n });\n\n results.push({\n file,\n report,\n ...(budgets ? { budget: budgetOf(scene, file, budgets, values.part, skip) } : {}),\n });\n } catch (error) {\n results.push({ file, error: (error as Error).message });\n }\n }\n\n if (values.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n console.log(\n results\n .map(({ file, report, budget, error }) =>\n report\n ? printReport(file, report, top, budget)\n : `${basename(file)}\\n ✗ couldn't read: ${error}`,\n )\n .join(\"\\n\\n\"),\n );\n }\n\n if (results.some((result) => result.error !== undefined)) {\n return 2;\n }\n\n const failed = results.some(\n ({ report, budget }) =>\n report!.zFighting.length > 0 ||\n report!.badGeometry.length > 0 ||\n budget?.checks.some((check) => check.over) === true,\n );\n\n return failed && !values[\"no-fail\"] ? 1 : 0;\n};\n","#!/usr/bin/env node\n/** The `three-audit` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
|