@rig-c/core 2.71.0 → 2.73.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 +10 -0
- package/docs/AUTHORING.md +172 -11
- package/docs/FACE.md +19 -0
- package/package.json +1 -1
- package/src/alpha_clip.ts +255 -0
- package/src/cli/core_commands.ts +2 -1
- package/src/cli/shared.ts +14 -1
- package/src/compile.ts +364 -27
- package/src/core/clipping.ts +5 -1
- package/src/core/events.ts +17 -5
- package/src/deformgen.ts +2 -2
- package/src/mesh.ts +3 -1
- package/src/meshcompare.ts +146 -22
- package/src/rig.ts +32 -7
- package/src/runtime/index.ts +8 -4
- package/src/runtime/model.ts +37 -20
- package/src/runtime/pose.ts +41 -1
- package/src/types.ts +26 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/** Alpha-outline geometry and UV carriers, independent of emitters and posers. */
|
|
2
|
+
import { CompileError } from './errors.ts';
|
|
3
|
+
import {
|
|
4
|
+
earClip, findSelfIntersection, traceAlphaOutline, simplifyClosedPolygon,
|
|
5
|
+
measureAuthoredMeshFit, rasteriseTriangles, MeshError, type AlphaMask,
|
|
6
|
+
} from './mesh.ts';
|
|
7
|
+
import { uvCarriers, CONTAINS, type Carrier } from './meshcarriers.ts';
|
|
8
|
+
import { hypot2 } from './numeric.ts';
|
|
9
|
+
import type { RigAlphaClip } from './rig.ts';
|
|
10
|
+
import type { AlphaClipReport } from './types.ts';
|
|
11
|
+
|
|
12
|
+
export type ClipPoint = [number, number];
|
|
13
|
+
const cross = (a: ClipPoint, b: ClipPoint): number => a[0] * b[1] - a[1] * b[0];
|
|
14
|
+
const sub = (a: ClipPoint, b: ClipPoint): ClipPoint => [a[0] - b[0], a[1] - b[1]];
|
|
15
|
+
|
|
16
|
+
export function clipDistance(p: readonly number[], a: readonly number[], b: readonly number[]): number {
|
|
17
|
+
const dx = b[0] - a[0];
|
|
18
|
+
const dy = b[1] - a[1];
|
|
19
|
+
const length2 = dx * dx + dy * dy;
|
|
20
|
+
const projection = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / (length2 || 1);
|
|
21
|
+
const t = Math.max(0, Math.min(1, projection));
|
|
22
|
+
return hypot2(p[0] - a[0] - t * dx, p[1] - a[1] - t * dy);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Neighbour degeneracies are not covered by the nonadjacent intersection check. */
|
|
26
|
+
export function refuseClipDegeneracy(points: ClipPoint[], where: string): void {
|
|
27
|
+
if (points.length < 3) {
|
|
28
|
+
throw new CompileError(
|
|
29
|
+
`${where}: clipping has ${points.length} vertices; required >=3 after final coordinate rounding.`,
|
|
30
|
+
'CE_COMPILE_ALPHA_CLIP_TOPOLOGY',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
let area2 = 0;
|
|
34
|
+
for (let i = 0; i < points.length; i++) {
|
|
35
|
+
const a = points[i];
|
|
36
|
+
const b = points[(i + 1) % points.length];
|
|
37
|
+
const c = points[(i + 2) % points.length];
|
|
38
|
+
if (!a.every(Number.isFinite) || (a[0] === b[0] && a[1] === b[1])) {
|
|
39
|
+
throw new CompileError(
|
|
40
|
+
`${where}: adjacent clipping vertices ${i} and ${(i + 1) % points.length} are ` +
|
|
41
|
+
`[${a}] and [${b}]; required finite distinct neighbours after final coordinate rounding. ` +
|
|
42
|
+
'Lower the source coordinate magnitude or simplify the outline.',
|
|
43
|
+
'CE_COMPILE_ALPHA_CLIP_TOPOLOGY',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const incoming = sub(b, a);
|
|
47
|
+
const outgoing = sub(c, b);
|
|
48
|
+
if (cross(incoming, outgoing) === 0 && incoming[0] * outgoing[0] + incoming[1] * outgoing[1] < 0) {
|
|
49
|
+
throw new CompileError(
|
|
50
|
+
`${where}: adjacent edges ${i} and ${(i + 1) % points.length} fold back after rounding; ` +
|
|
51
|
+
'required a nondegenerate simple clipping boundary.',
|
|
52
|
+
'CE_COMPILE_ALPHA_CLIP_TOPOLOGY',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
area2 += cross(a, b);
|
|
56
|
+
}
|
|
57
|
+
if (area2 === 0) {
|
|
58
|
+
throw new CompileError(
|
|
59
|
+
`${where}: clipping doubled area is ${area2} after coordinate rounding; required nonzero area.`,
|
|
60
|
+
'CE_COMPILE_ALPHA_CLIP_TOPOLOGY',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Check simplifier output and mapped source-triangle crossings before emission. */
|
|
66
|
+
export function refuseClipCrossing(points: ClipPoint[], where: string): void {
|
|
67
|
+
const crossing = findSelfIntersection(points);
|
|
68
|
+
if (crossing) {
|
|
69
|
+
throw new CompileError(
|
|
70
|
+
`${where}: selfintersectionaftersimplification: edges ${crossing[0]} and ${crossing[1]} ` +
|
|
71
|
+
'intersect; required one simple outline. Lower tolerance or repair the source topology.',
|
|
72
|
+
'CE_COMPILE_ALPHA_CLIP_SELF_INTERSECTION',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type OutlineReport = Omit<AlphaClipReport, 'target' | 'source' | 'vertices' | 'uvs' | 'maxBindingDeviationWorld'>;
|
|
78
|
+
|
|
79
|
+
export function alphaClipOutline(
|
|
80
|
+
mask: AlphaMask,
|
|
81
|
+
spec: RigAlphaClip,
|
|
82
|
+
where: string,
|
|
83
|
+
pageScale = 1,
|
|
84
|
+
): { points: ClipPoint[]; report: OutlineReport } {
|
|
85
|
+
if (!Number.isInteger(spec.alpha) || spec.alpha < 1 || spec.alpha > 255) {
|
|
86
|
+
throw new CompileError(
|
|
87
|
+
`${where}: alphaClip.alpha is ${String(spec.alpha)}; required an integer in 1..255. Declare the threshold.`,
|
|
88
|
+
'CE_COMPILE_ALPHA_CLIP_PARAMETERS',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (!Number.isFinite(spec.tolerance) || spec.tolerance <= 0 || !Number.isFinite(pageScale) || pageScale <= 0) {
|
|
92
|
+
throw new CompileError(
|
|
93
|
+
`${where}: alphaClip.tolerance=${String(spec.tolerance)}, page scale=${pageScale}; ` +
|
|
94
|
+
'required finite positive drawing-pixel tolerance and page scale.',
|
|
95
|
+
'CE_COMPILE_ALPHA_CLIP_PARAMETERS',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isInteger(spec.maxVertices) || spec.maxVertices < 3) {
|
|
99
|
+
throw new CompileError(
|
|
100
|
+
`${where}: alphaClip.maxVertices is ${String(spec.maxVertices)}; required an integer >=3. ` +
|
|
101
|
+
'Declare the complete mapped polygon budget.',
|
|
102
|
+
'CE_COMPILE_ALPHA_CLIP_PARAMETERS',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let below = 0;
|
|
106
|
+
let above = 0;
|
|
107
|
+
for (const alpha of mask.alpha) {
|
|
108
|
+
if (alpha < spec.alpha) below++;
|
|
109
|
+
else above++;
|
|
110
|
+
}
|
|
111
|
+
if (above === 0 || below === 0) {
|
|
112
|
+
throw new CompileError(
|
|
113
|
+
`${where}: slotwithnoalpha: ${above} of ${mask.width * mask.height} pixels reach alpha ${spec.alpha}; ` +
|
|
114
|
+
'required a nonempty silhouette with transparent exterior. Supply alpha art or declare a threshold that measures a silhouette.',
|
|
115
|
+
'CE_COMPILE_ALPHA_CLIP_NO_ALPHA',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
let traced: ReturnType<typeof traceAlphaOutline>;
|
|
119
|
+
try {
|
|
120
|
+
traced = traceAlphaOutline(mask, spec.alpha);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof MeshError)) throw error;
|
|
123
|
+
throw new CompileError(
|
|
124
|
+
`${where}: alpha outline cannot be represented: ${error.message}; required one simple connected outline.`,
|
|
125
|
+
'CE_COMPILE_ALPHA_CLIP_TOPOLOGY',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (traced.islands !== 1 || traced.holePixels !== 0) {
|
|
129
|
+
throw new CompileError(
|
|
130
|
+
`${where}: alpha ${spec.alpha} has ${traced.islands} islands and ${traced.holePixels} hole pixels; ` +
|
|
131
|
+
'required one island without holes. Separate the source or author clipping geometry.',
|
|
132
|
+
'CE_COMPILE_ALPHA_CLIP_TOPOLOGY',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const points = simplifyClosedPolygon(traced.outline, spec.tolerance * pageScale);
|
|
136
|
+
refuseClipCrossing(points, where);
|
|
137
|
+
refuseClipDegeneracy(points, where);
|
|
138
|
+
const triangles = clipTrianglesOf(points);
|
|
139
|
+
const fit = measureAuthoredMeshFit(mask, spec.alpha, points, triangles);
|
|
140
|
+
let maxTraceDeviation = 0;
|
|
141
|
+
for (const point of traced.outline) {
|
|
142
|
+
const distance = Math.min(...points.map((a, i) => clipDistance(point, a, points[(i + 1) % points.length])));
|
|
143
|
+
maxTraceDeviation = Math.max(maxTraceDeviation, distance);
|
|
144
|
+
}
|
|
145
|
+
const filled = rasteriseTriangles(points, triangles, mask.width, mask.height).reduce((sum, bit) => sum + bit, 0);
|
|
146
|
+
return {
|
|
147
|
+
points,
|
|
148
|
+
report: {
|
|
149
|
+
alpha: spec.alpha,
|
|
150
|
+
tolerance: spec.tolerance,
|
|
151
|
+
maxVertices: spec.maxVertices,
|
|
152
|
+
tracedVertices: traced.outline.length,
|
|
153
|
+
simplifiedVertices: points.length,
|
|
154
|
+
artPixels: fit.artPixels,
|
|
155
|
+
coveredArt: fit.coveredArt,
|
|
156
|
+
coverage: fit.coverage,
|
|
157
|
+
falsePositive: filled - fit.coveredArt,
|
|
158
|
+
falseNegative: fit.artPixels - fit.coveredArt,
|
|
159
|
+
maxTraceDeviation: maxTraceDeviation / pageScale,
|
|
160
|
+
overshoot: fit.overshoot / pageScale,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function clipTrianglesOf(points: ClipPoint[]): number[] {
|
|
166
|
+
return earClip(points);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Offline cell/reference reading, not a runtime alpha-mask feature. */
|
|
170
|
+
export function clipContains(points: readonly (readonly number[])[], x: number, y: number): boolean {
|
|
171
|
+
let inside = false;
|
|
172
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
173
|
+
const a = points[i];
|
|
174
|
+
const b = points[j];
|
|
175
|
+
if ((a[1] > y) !== (b[1] > y) && x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0]) inside = !inside;
|
|
176
|
+
}
|
|
177
|
+
return inside;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function carriersOf(
|
|
181
|
+
points: ClipPoint[], uvs: readonly number[], triangles: readonly number[], where: string,
|
|
182
|
+
): Array<Carrier | null> {
|
|
183
|
+
try {
|
|
184
|
+
return uvCarriers(uvs, triangles, points.map((uv) => ({ uv })), where);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new CompileError(
|
|
187
|
+
`${where}: source UV carrier is ambiguous: ${error instanceof Error ? error.message : String(error)}; ` +
|
|
188
|
+
'required unique source geometry. Repair overlapping UV triangles.',
|
|
189
|
+
'CE_COMPILE_ALPHA_CLIP_CARRIER',
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Split BEFORE mapping: an edge across source triangles is piecewise affine, not one chord. */
|
|
195
|
+
export function clipCarriers(
|
|
196
|
+
ring: ClipPoint[], uvs: readonly number[], triangles: readonly number[], where: string,
|
|
197
|
+
): { points: ClipPoint[]; carriers: Carrier[] } {
|
|
198
|
+
const edges = new Map<string, [number, number]>();
|
|
199
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
200
|
+
const triplet = triangles.slice(i, i + 3);
|
|
201
|
+
for (const [a, b] of [[triplet[0], triplet[1]], [triplet[1], triplet[2]], [triplet[2], triplet[0]]]) {
|
|
202
|
+
edges.set(`${Math.min(a, b)}/${Math.max(a, b)}`, [a, b]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const points: ClipPoint[] = [];
|
|
206
|
+
for (let i = 0; i < ring.length; i++) {
|
|
207
|
+
const a = ring[i];
|
|
208
|
+
const b = ring[(i + 1) % ring.length];
|
|
209
|
+
const direction = sub(b, a);
|
|
210
|
+
const cuts = [0];
|
|
211
|
+
for (const [v, w] of edges.values()) {
|
|
212
|
+
const c: ClipPoint = [uvs[2 * v], uvs[2 * v + 1]];
|
|
213
|
+
const e: ClipPoint = [uvs[2 * w], uvs[2 * w + 1]];
|
|
214
|
+
const edge = sub(e, c);
|
|
215
|
+
const denominator = cross(direction, edge);
|
|
216
|
+
if (denominator === 0) continue;
|
|
217
|
+
const t = cross(sub(c, a), edge) / denominator;
|
|
218
|
+
const u = cross(sub(c, a), direction) / denominator;
|
|
219
|
+
if (t > CONTAINS && t < 1 - CONTAINS && u >= -CONTAINS && u <= 1 + CONTAINS) cuts.push(t);
|
|
220
|
+
}
|
|
221
|
+
cuts.sort((x, y) => x - y);
|
|
222
|
+
for (let k = 0; k < cuts.length; k++) {
|
|
223
|
+
if (k === 0 || cuts[k] - cuts[k - 1] > CONTAINS) {
|
|
224
|
+
points.push([a[0] + cuts[k] * direction[0], a[1] + cuts[k] * direction[1]]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const carriers = carriersOf(points, uvs, triangles, where);
|
|
229
|
+
const missing = carriers.findIndex((carrier) => carrier === null);
|
|
230
|
+
if (missing >= 0) {
|
|
231
|
+
throw new CompileError(
|
|
232
|
+
`${where}: source has no UV carrier for outline vertex ${missing} at [${points[missing]}]; ` +
|
|
233
|
+
'required coverage of the complete clipping outline. Extend the source geometry or lower tolerance.',
|
|
234
|
+
'CE_COMPILE_ALPHA_CLIP_CARRIER',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
// Endpoints can all be carried while an edge bridges a missing strip. Since all
|
|
238
|
+
// triangle-edge crossings partition the ring, a unique open midpoint identifies
|
|
239
|
+
// the actual affine source map throughout that entire open segment.
|
|
240
|
+
const midpoints = points.map((a, i): ClipPoint => {
|
|
241
|
+
const b = points[(i + 1) % points.length];
|
|
242
|
+
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
243
|
+
});
|
|
244
|
+
const intervals = carriersOf(midpoints, uvs, triangles, `${where} open outline intervals`);
|
|
245
|
+
const gap = intervals.findIndex((carrier) => carrier === null);
|
|
246
|
+
if (gap >= 0) {
|
|
247
|
+
throw new CompileError(
|
|
248
|
+
`${where}: outline interval ${gap} from [${points[gap]}] to [${points[(gap + 1) % points.length]}] ` +
|
|
249
|
+
`has no source UV carrier at open midpoint [${midpoints[gap]}]; required complete interval coverage. ` +
|
|
250
|
+
'Fill the source UV gap; carried endpoints cannot authorize a bridge.',
|
|
251
|
+
'CE_COMPILE_ALPHA_CLIP_CARRIER',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return { points, carriers: carriers as Carrier[] };
|
|
255
|
+
}
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -50,7 +50,7 @@ import { attachmentRegionJoins } from '../region_joins.ts';
|
|
|
50
50
|
import { cmdRepack } from './repack.ts';
|
|
51
51
|
import { writeWhole } from './write_whole.ts';
|
|
52
52
|
import { GUIDE_FILE, GUIDE_RULE_TABLES_ELSEWHERE, guideAnswer, refusalLine } from '../guide.ts';
|
|
53
|
-
import { ATLAS_ABSENT, COMMANDS, type CommandRun, DelegateError, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPageEdges, readPoserFlag, readSheetFormat, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
53
|
+
import { ATLAS_ABSENT, COMMANDS, type CommandRun, DelegateError, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, printAlphaClips, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPageEdges, readPoserFlag, readSheetFormat, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
54
54
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
55
55
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
56
56
|
import { spawnSync } from 'node:child_process';
|
|
@@ -1886,6 +1886,7 @@ export function cmdExplain(flags: Record<string, string>): void {
|
|
|
1886
1886
|
}
|
|
1887
1887
|
}
|
|
1888
1888
|
|
|
1889
|
+
printAlphaClips(result);
|
|
1889
1890
|
if (result.meshes.length) {
|
|
1890
1891
|
console.log('\nmeshes');
|
|
1891
1892
|
for (const kind of new Set(result.meshes.map((m) => m.kind))) console.log(` ${MESH_KIND_NOTES[kind]}`);
|
package/src/cli/shared.ts
CHANGED
|
@@ -2400,6 +2400,18 @@ export function readBuildInvocation(flags: Record<string, string>): { label: str
|
|
|
2400
2400
|
return { label, opts, profile, packing };
|
|
2401
2401
|
}
|
|
2402
2402
|
|
|
2403
|
+
/** Six decimal places: display error <=0.5e-6 per scalar; cells/counts remain integers. */
|
|
2404
|
+
export function printAlphaClips(result: Pick<CompileResult, 'alphaClips'>): void {
|
|
2405
|
+
for (const clip of result.alphaClips ?? []) console.log(
|
|
2406
|
+
` CLIP ${clip.target} from=${clip.source} alpha=${clip.alpha} tolerance=${clip.tolerance} drawing-px ` +
|
|
2407
|
+
`vertices=${clip.vertices}/${clip.maxVertices} traced=${clip.tracedVertices} simplified=${clip.simplifiedVertices} ` +
|
|
2408
|
+
`coverage=${clip.coverage.toFixed(6)} (${clip.coveredArt}/${clip.artPixels}) ` +
|
|
2409
|
+
`falsePositive=${clip.falsePositive} falseNegative=${clip.falseNegative} ` +
|
|
2410
|
+
`maxTraceDeviation=${clip.maxTraceDeviation.toFixed(6)}drawing-px ` +
|
|
2411
|
+
`maxBindingDeviation=${clip.maxBindingDeviationWorld.toFixed(6)}world-units overshoot=${clip.overshoot.toFixed(6)}drawing-px`,
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2403
2415
|
/** The lines a build opens on: the cut, then its two input files. */
|
|
2404
2416
|
export function printBuildHeader(label: string, opts: CompileOptions): void {
|
|
2405
2417
|
console.log(`rigc build ${label}`);
|
|
@@ -2412,7 +2424,7 @@ export function printBuildHeader(label: string, opts: CompileOptions): void {
|
|
|
2412
2424
|
}
|
|
2413
2425
|
|
|
2414
2426
|
/** What a compile measured, as `build` reports it before its gate: the parts, the dropped states, the absent parts, the meshes, the physics. */
|
|
2415
|
-
export function printCompiled(opts: CompileOptions, result: Pick<CompileResult, 'images' | 'droppedStates' | 'absentParts' | 'meshes' | 'rig' | 'physics'>): void {
|
|
2427
|
+
export function printCompiled(opts: CompileOptions, result: Pick<CompileResult, 'images' | 'droppedStates' | 'absentParts' | 'meshes' | 'rig' | 'physics' | 'alphaClips'>): void {
|
|
2416
2428
|
if (opts.atlasInPath !== undefined) console.log(` .. atlas-in ${opts.atlasInPath}`);
|
|
2417
2429
|
console.log(` .. ${result.images.length} part page(s):`);
|
|
2418
2430
|
for (const img of result.images) {
|
|
@@ -2447,6 +2459,7 @@ export function printCompiled(opts: CompileOptions, result: Pick<CompileResult,
|
|
|
2447
2459
|
for (const a of result.absentParts) {
|
|
2448
2460
|
console.log(` ABSENT ${a.slot}: ${a.why} — slot not emitted`);
|
|
2449
2461
|
}
|
|
2462
|
+
printAlphaClips(result);
|
|
2450
2463
|
for (const m of result.meshes) {
|
|
2451
2464
|
console.log(
|
|
2452
2465
|
` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|