lecodes-assets 0.2.1 → 2.5.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 +203 -26
- package/dist/cli.js +18157 -11441
- package/dist/index.js +16603 -9878
- package/package.json +3 -1
- package/src/cli-kit/command.ts +100 -0
- package/src/cli-kit/help.ts +72 -0
- package/src/cli-kit/index.ts +4 -0
- package/src/cli-kit/parse.ts +72 -0
- package/src/cli-kit/run.ts +52 -0
- package/src/cli.ts +24 -1039
- package/src/commands/avatar.ts +77 -0
- package/src/commands/bvh.ts +26 -0
- package/src/commands/convert.ts +156 -0
- package/src/commands/doctor.ts +79 -0
- package/src/commands/geometry.ts +45 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/marks.ts +38 -0
- package/src/commands/mirror.ts +35 -0
- package/src/commands/probe.ts +43 -0
- package/src/commands/retarget/donor.ts +63 -0
- package/src/commands/retarget/index.ts +169 -0
- package/src/commands/retarget/map.ts +81 -0
- package/src/commands/retarget/report.ts +77 -0
- package/src/commands/scene.ts +108 -0
- package/src/commands/shared.ts +140 -0
- package/src/commands/sky.ts +35 -0
- package/src/commands/terrainPack.ts +40 -0
- package/src/commands/unpack.ts +43 -0
- package/src/commands/unreal.ts +109 -0
- package/src/commands/unwrap.ts +56 -0
- package/src/doctor/cardTrim.ts +288 -0
- package/src/doctor/doctor.ts +94 -30
- package/src/doctor/impostor.ts +614 -0
- package/src/doctor/merge/accessors.ts +30 -0
- package/src/doctor/merge/atlasLayout.ts +94 -0
- package/src/doctor/merge/atlasPixels.ts +114 -0
- package/src/doctor/merge/bindSpace.ts +59 -0
- package/src/doctor/merge/classify.ts +136 -0
- package/src/doctor/merge/geometry.ts +167 -0
- package/src/doctor/merge/index.ts +98 -0
- package/src/doctor/merge/matrix.ts +60 -0
- package/src/doctor/merge/plan.ts +88 -0
- package/src/doctor/merge/textures.ts +60 -0
- package/src/doctor/merge/types.ts +77 -0
- package/src/doctor/transcodeKtx2.ts +186 -31
- package/src/doctor/transcodeTextures.ts +6 -1
- package/src/doctor/validateGlb.ts +2 -2
- package/src/doctor/vertexAo.ts +333 -0
- package/src/fbx/read.ts +2 -0
- package/src/fbx/toDocument.ts +227 -209
- package/src/geometry.ts +222 -0
- package/src/index.ts +2 -0
- package/src/retarget/heading.ts +238 -171
- package/src/retarget/humanoid.ts +572 -561
- package/src/retarget/retarget.ts +2375 -2272
- package/src/retarget/rig.ts +4 -1
- package/src/retarget/writeMarks.ts +134 -132
- package/src/sky/cmgen.ts +3 -3
- package/src/sky/index.ts +57 -8
- package/src/terrainPack.ts +61 -22
- package/src/unity/colliderFit.ts +287 -0
- package/src/unity/fbxModel.ts +194 -0
- package/src/unity/foliage.ts +63 -0
- package/src/unity/gameObjects.ts +90 -0
- package/src/unity/guid.ts +104 -0
- package/src/unity/image.ts +58 -0
- package/src/unity/kindGlb.ts +364 -0
- package/src/unity/lecodesLevel.ts +414 -0
- package/src/unity/levelTerrain.ts +125 -0
- package/src/unity/matFile.ts +179 -0
- package/src/unity/materialSlots.ts +53 -0
- package/src/unity/materialWriter.ts +316 -0
- package/src/unity/materials.ts +34 -429
- package/src/unity/math.ts +82 -0
- package/src/unity/prefabTree.ts +285 -0
- package/src/unity/prefabs.ts +10 -36
- package/src/unity/propertyPath.ts +43 -0
- package/src/unity/scene.ts +339 -304
- package/src/unity/serialized.ts +242 -0
- package/src/unity/terrain.ts +221 -0
- package/src/unity/triBudget.ts +77 -0
- package/src/unity/triplanar.ts +122 -0
- package/src/unity/unityMesh.ts +151 -0
- package/src/unity/unpack.ts +24 -5
- package/src/unity/yaml.ts +131 -0
- package/src/unreal/index.ts +6 -5
- package/src/unreal/materialGraph.ts +224 -0
- package/src/unreal/materials.ts +117 -9
- package/src/unreal/package.ts +30 -12
- package/src/unreal/resolve.ts +6 -4
- package/src/unreal/sound.ts +128 -0
- package/src/unreal/texture.ts +70 -3
- package/src/unreal/toDocument.ts +7 -0
- package/src/unreal/versions.ts +8 -0
- package/src/unwrap/index.ts +167 -0
- package/src/unwrap/lightmapUv.ts +761 -0
- package/src/wasm/fbxReader.js +1 -1
- package/src/wasm/lightmapUv.js +1 -1
- package/src/doctor/lightmapUv.ts +0 -452
package/src/doctor/doctor.ts
CHANGED
|
@@ -6,26 +6,27 @@
|
|
|
6
6
|
// unbound joints / re-encoding webp→png) vs LOSSY fixes (bound-joint removal,
|
|
7
7
|
// webp→jpg, downscaling, heavy-png→jpg).
|
|
8
8
|
// Fix : applies everything; the caller writes <name>.fixed.glb and re-validates.
|
|
9
|
-
// KTX2 : textures → KTX2 (ETC1S; --ktx2-hq keeps normals UASTC) via KTX-Software
|
|
9
|
+
// KTX2 : textures → KTX2 (ETC1S; --ktx2-hq keeps normals UASTC) via KTX-Software (toktx, or `ktx create` where toktx is gone).
|
|
10
10
|
import { gatherSkinPrims, planSkin, applySkinPlan, DEFAULT_TARGET, type SkinPlan } from './reduceGlbBones'
|
|
11
11
|
import { planTexture, applyTexturePlan, getTextureRoles, DEFAULT_MAX_SIZE, type TexturePlan } from './transcodeTextures'
|
|
12
|
-
import { planKtx2, applyKtx2,
|
|
12
|
+
import { planKtx2, applyKtx2, findKtxEncoder, KTX_ENCODER_MISSING, type Ktx2Plan, type KtxEncoder } from './transcodeKtx2'
|
|
13
13
|
import { measureScale, planScale, applyScale, type ScaleInfo, type ScalePlan, type ScaleOptions } from './normalizeScale'
|
|
14
14
|
import { analyzeShading, applyShading, type ShadingPlan, type ShadingOptions } from './shading'
|
|
15
|
-
import {
|
|
15
|
+
import { planCardTrim, applyCardTrim, type CardTrimPlan, type CardTrimOptions, type CardTrimResult } from './cardTrim'
|
|
16
16
|
import { planLod, applyLod, type LodPlan, type LodOptions, type LodResult } from './lod'
|
|
17
|
+
import { planMerge, applyMerge, describeScale, type MergePlan, type MergeOptions, type MergeResult } from './merge'
|
|
17
18
|
import { duplicateNodeNames } from '../mergeSkins'
|
|
18
19
|
import { KHRTextureBasisu } from '@gltf-transform/extensions'
|
|
19
20
|
import type { Document, Material } from '@gltf-transform/core'
|
|
20
21
|
import { mkdirSync, existsSync, statSync, writeFileSync } from 'node:fs'
|
|
21
22
|
import { join } from 'node:path'
|
|
22
23
|
|
|
23
|
-
export { DEFAULT_TARGET, DEFAULT_MAX_SIZE,
|
|
24
|
+
export { DEFAULT_TARGET, DEFAULT_MAX_SIZE, findKtxEncoder, KTX_ENCODER_MISSING }
|
|
24
25
|
export { sceneBounds, measureScale, applyScale } from './normalizeScale'
|
|
25
26
|
|
|
26
27
|
interface MetallicWarn { mat: Material; name: string; metal: number }
|
|
27
|
-
export interface DoctorPlan { skinPlans: SkinPlan[]; texPlans: TexturePlan[]; ktx2Plans: Ktx2Plan[]; metallicWarn: MetallicWarn[];
|
|
28
|
-
export interface DoctorOptions extends ScaleOptions, ShadingOptions,
|
|
28
|
+
export interface DoctorPlan { skinPlans: SkinPlan[]; texPlans: TexturePlan[]; ktx2Plans: Ktx2Plan[]; metallicWarn: MetallicWarn[]; bareCount: number; shadingPlan: ShadingPlan; scaleInfo: ScaleInfo | null; scalePlan: ScalePlan | null; lodPlan: LodPlan | null; cardTrimPlan: CardTrimPlan; mergePlan: MergePlan; conflicts: number; target: number; dupNames: { name: string; count: number; shadowsJoint: boolean }[] }
|
|
29
|
+
export interface DoctorOptions extends ScaleOptions, ShadingOptions, LodOptions, CardTrimOptions, MergeOptions { target?: number; maxSize?: number; ktx2?: boolean; ktx2Hq?: boolean; ktx2Uastc?: boolean }
|
|
29
30
|
|
|
30
31
|
export async function analyzeDoc(doc: Document, opts: DoctorOptions = {}): Promise<DoctorPlan> {
|
|
31
32
|
const target = opts.target ?? DEFAULT_TARGET
|
|
@@ -50,9 +51,12 @@ export async function analyzeDoc(doc: Document, opts: DoctorOptions = {}): Promi
|
|
|
50
51
|
const metallicWarn: MetallicWarn[] = root.listMaterials()
|
|
51
52
|
.filter((m) => m.getMetallicFactor() > 0.01 && !m.getMetallicRoughnessTexture())
|
|
52
53
|
.map((m) => ({ mat: m, name: m.getName() || '?', metal: m.getMetallicFactor() }))
|
|
54
|
+
// Primitives with NO material at all: glTF's default material is metallic 1 / roughness 1 — a white ROUGH METAL,
|
|
55
|
+
// which under an environment (or a lightmap's reflection probe) is a dark featureless blob (the school's
|
|
56
|
+
// cabinets and sinks, 2026-09-17). --fix-metallic gives them a plain white dielectric.
|
|
57
|
+
let bareCount = 0
|
|
58
|
+
for (const mesh of root.listMeshes()) for (const prim of mesh.listPrimitives()) if (!prim.getMaterial()) bareCount++
|
|
53
59
|
const shadingPlan = analyzeShading(doc, opts)
|
|
54
|
-
// Lightmap UVs are only ever ADDED on request (--lightmap-uv); the analysis just says what would be unwrapped.
|
|
55
|
-
const lightmapPlan = analyzeLightmapUv(doc, opts)
|
|
56
60
|
// Scale is always MEASURED (probe/report it even with no flag); it is only APPLIED when asked.
|
|
57
61
|
const scaleInfo = measureScale(doc)
|
|
58
62
|
const scalePlan = planScale(scaleInfo, opts)
|
|
@@ -61,7 +65,11 @@ export async function analyzeDoc(doc: Document, opts: DoctorOptions = {}): Promi
|
|
|
61
65
|
const dupNames = duplicateNodeNames(doc)
|
|
62
66
|
// LOD levels are only ever ADDED on request (--lod); see doctor/lod.ts.
|
|
63
67
|
const lodPlan = planLod(doc, opts)
|
|
64
|
-
|
|
68
|
+
// Card trimming only on request (--trim-cards); the analysis lists the alpha-MASK primitives it would cut.
|
|
69
|
+
const cardTrimPlan = planCardTrim(doc, opts)
|
|
70
|
+
// Mesh merge + atlas only on request (--merge); the analysis says how many draw calls it would fold.
|
|
71
|
+
const mergePlan = await planMerge(doc, opts)
|
|
72
|
+
return { skinPlans, texPlans, ktx2Plans, metallicWarn, bareCount, shadingPlan, scaleInfo, scalePlan, lodPlan, cardTrimPlan, mergePlan, conflicts, target, dupNames }
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
export const kb = (n?: number) => (n === undefined ? '?' : `${Math.round(n / 1024)}KB`)
|
|
@@ -137,15 +145,16 @@ export function reportPlan(plan: DoctorPlan): { text: string; lossless: number;
|
|
|
137
145
|
if (lossless.length) { lines.push(' ── LOSSLESS (no quality loss) ──'); lossless.forEach((l) => lines.push(' • ' + l)) }
|
|
138
146
|
if (lossy.length) { lines.push(' ── LOSSY (some degradation) ──'); lossy.forEach((l) => lines.push(' • ' + l)) }
|
|
139
147
|
if (blocking.length) { lines.push(' ── BLOCKING / manual ──'); blocking.forEach((l) => lines.push(' ✗ ' + l)) }
|
|
148
|
+
if (plan.metallicWarn.length || plan.bareCount) lines.push(` ── MATERIALS ──`)
|
|
140
149
|
if (plan.metallicWarn.length) {
|
|
141
|
-
lines.push(` ── MATERIALS ──`)
|
|
142
150
|
lines.push(` ℹ ${plan.metallicWarn.length} material(s) have metallic without an MR texture → may render BLACK without IBL (--fix-metallic zeroes them):`)
|
|
143
151
|
lines.push(` ${plan.metallicWarn.map((w) => `${w.name}(${w.metal.toFixed(2)})`).join(', ')}`)
|
|
144
152
|
}
|
|
153
|
+
if (plan.bareCount) lines.push(` ℹ ${plan.bareCount} primitive(s) have NO material → glTF's default is a white ROUGH METAL (a dark blob under any environment; --fix-metallic gives them a white dielectric)`)
|
|
145
154
|
const selfLit = plan.shadingPlan.issues.filter((i) => i.selfLit)
|
|
146
155
|
const boosted = plan.shadingPlan.issues.filter((i) => i.specularBoost > 0)
|
|
147
156
|
if (selfLit.length || boosted.length) {
|
|
148
|
-
if (!plan.metallicWarn.length) lines.push(` ── MATERIALS ──`)
|
|
157
|
+
if (!plan.metallicWarn.length && !plan.bareCount) lines.push(` ── MATERIALS ──`)
|
|
149
158
|
if (selfLit.length) {
|
|
150
159
|
lines.push(` ⚠ ${selfLit.length} material(s) emit their own base colour (emissive = albedo) → render ~2× too bright, washed out (--fix-emissive drops it, --unlit goes flat):`)
|
|
151
160
|
lines.push(` ${selfLit.map((i) => i.name).join(', ')}`)
|
|
@@ -163,14 +172,24 @@ export function reportPlan(plan: DoctorPlan): { text: string; lossless: number;
|
|
|
163
172
|
else if (sc && sc.suggested !== 1) lines.push(` --fix-scale applies ×${fmtFactor(sc.suggested)} → ${dims3(sc.dims.map((d) => d * sc.suggested))}; --scale-to <m> / --scale <n> to decide yourself`)
|
|
164
173
|
else lines.push(` --scale-to <m> sets the largest dimension explicitly`)
|
|
165
174
|
}
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
168
|
-
lines.push(` ──
|
|
169
|
-
const tris =
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
else lines.push(` ℹ ${
|
|
173
|
-
|
|
175
|
+
const ct = plan.cardTrimPlan
|
|
176
|
+
if (ct.prims.length) {
|
|
177
|
+
lines.push(` ── CARDS ──`)
|
|
178
|
+
const tris = ct.prims.reduce((n, p) => n + p.triangles, 0)
|
|
179
|
+
const ready = ct.prims.filter((p) => p.texture === 'ok').length
|
|
180
|
+
if (ct.enabled) lines.push(` • trim ${ready} alpha-MASK primitive(s) (${tris} triangles) to their opaque texels, margin ${ct.margin} texel(s)${ready < ct.prims.length ? `; ${ct.prims.length - ready} skipped (${ct.prims.filter((p) => p.texture !== 'ok').map((p) => `${p.name}: ${p.texture}`).slice(0, 4).join(', ')})` : ''}`)
|
|
181
|
+
else lines.push(` ℹ ${ct.prims.length} alpha-MASK primitive(s), ${tris} triangles — foliage cards? --trim-cards cuts each quad to its opaque texels (less discarded fill)${ready < ct.prims.length ? `; ${ct.prims.length - ready} carry KTX2 / transformed / no textures and would be skipped` : ''}`)
|
|
182
|
+
}
|
|
183
|
+
const mg = plan.mergePlan
|
|
184
|
+
if (mg.groups.length || mg.skipped.length) {
|
|
185
|
+
lines.push(` ── MERGE ──`)
|
|
186
|
+
const fold = `${mg.primitivesBefore} primitive(s) over ${mg.materialsBefore} material(s) → ${mg.primitivesAfter} draw call(s)`
|
|
187
|
+
if (mg.enabled) {
|
|
188
|
+
lines.push(` • merge ${fold}`)
|
|
189
|
+
for (const g of mg.groups) lines.push(` ${g.label}: ${g.sources.length} primitives, ${g.materials.length} material(s)${g.atlas ? ` → 1 (atlas ${g.atlas.size}² ${g.atlas.maps.join('+')}, ${g.atlas.tiles.length} tiles${describeScale(g.atlas)}, pad ${g.atlas.pad})` : g.materials.length === 1 ? ` (kept: ${g.materials[0].getName() || 'material'}${g.note ? ` — ${g.note}` : ''})` : ''}${g.family === 'skinned' ? `, ${g.joints} joints in one skin` : ', transforms baked'}`)
|
|
190
|
+
if (mg.joints > plan.target) blocking.push(`bones merged skin has ${mg.joints} joints > ${plan.target} — exclude parts (--merge-exclude) or raise --target`)
|
|
191
|
+
} else if (mg.groups.length) lines.push(` ℹ ${fold} with --merge (one atlas material per alpha mode / sidedness)`)
|
|
192
|
+
if (mg.skipped.length) lines.push(` ${mg.enabled ? 'skipped' : 'would skip'}: ${mg.skipped.slice(0, 6).map((s) => `${s.name} (${s.reason})`).join(', ')}${mg.skipped.length > 6 ? '…' : ''}`)
|
|
174
193
|
}
|
|
175
194
|
const plainDups = plan.dupNames.filter((d) => !d.shadowsJoint)
|
|
176
195
|
if (plainDups.length) {
|
|
@@ -181,10 +200,10 @@ export function reportPlan(plan: DoctorPlan): { text: string; lossless: number;
|
|
|
181
200
|
return { text: lines.join('\n'), lossless: lossless.length, lossy: lossy.length, blocking: blocking.length }
|
|
182
201
|
}
|
|
183
202
|
|
|
184
|
-
export interface ApplyOptions extends DoctorOptions { fixMetallic?: boolean;
|
|
185
|
-
export interface ApplyResult { metallicZeroed: number; ktx2Encoded: number; scaled: number; unlit: number; emissiveCleared: number;
|
|
203
|
+
export interface ApplyOptions extends DoctorOptions { fixMetallic?: boolean; ktxEncoder?: KtxEncoder; progress?: (done: number, total: number) => void }
|
|
204
|
+
export interface ApplyResult { metallicZeroed: number; bareFixed: number; ktx2Encoded: number; scaled: number; unlit: number; emissiveCleared: number; lod: LodResult | null; cards: CardTrimResult | null; merge: MergeResult | null; emptied: number }
|
|
186
205
|
|
|
187
|
-
/** Apply a plan to the document (mutates). Resolves
|
|
206
|
+
/** Apply a plan to the document (mutates). Resolves the KTX encoder itself when `ktx2` and none is given. */
|
|
188
207
|
export async function applyPlan(doc: Document, plan: DoctorPlan, opts: ApplyOptions = {}): Promise<ApplyResult> {
|
|
189
208
|
const maxSize = opts.maxSize ?? DEFAULT_MAX_SIZE
|
|
190
209
|
for (const s of plan.skinPlans) applySkinPlan(doc, s)
|
|
@@ -196,19 +215,48 @@ export async function applyPlan(doc: Document, plan: DoctorPlan, opts: ApplyOpti
|
|
|
196
215
|
for (const w of plan.metallicWarn) w.mat.setMetallicFactor(0)
|
|
197
216
|
metallicZeroed = plan.metallicWarn.length
|
|
198
217
|
}
|
|
218
|
+
let bareFixed = 0
|
|
219
|
+
if (opts.fixMetallic && plan.bareCount) {
|
|
220
|
+
const plain = doc.createMaterial('Default').setBaseColorFactor([1, 1, 1, 1]).setMetallicFactor(0).setRoughnessFactor(1)
|
|
221
|
+
for (const mesh of doc.getRoot().listMeshes()) for (const prim of mesh.listPrimitives()) if (!prim.getMaterial()) { prim.setMaterial(plain); bareFixed++ }
|
|
222
|
+
}
|
|
199
223
|
const shading = applyShading(doc, plan.shadingPlan)
|
|
200
|
-
//
|
|
201
|
-
|
|
224
|
+
// Merge + atlas before every other geometry pass (they see the merged primitive) and before the texture
|
|
225
|
+
// passes: the atlases are new PNG textures, planned below like any other — capped by --atlas-size, not
|
|
226
|
+
// --max-size — while the plans of the textures the atlases replaced are dropped with them.
|
|
227
|
+
const merge = plan.mergePlan.enabled ? await applyMerge(doc, plan.mergePlan) : null
|
|
228
|
+
if (merge) {
|
|
229
|
+
const root = doc.getRoot()
|
|
230
|
+
const alive = new Set(root.listTextures())
|
|
231
|
+
plan.ktx2Plans = plan.ktx2Plans.filter((t) => alive.has(t.tex))
|
|
232
|
+
plan.texPlans = plan.texPlans.filter((t) => alive.has(t.tex))
|
|
233
|
+
const codecPolicy = opts.ktx2Uastc ? 'uastc' : opts.ktx2Hq ? 'hq' : 'default'
|
|
234
|
+
for (const tex of merge.textures) {
|
|
235
|
+
if (opts.ktx2) { const p = await planKtx2(tex, root, plan.mergePlan.atlasSize, codecPolicy); if (p) plan.ktx2Plans.push(p) }
|
|
236
|
+
else { const p = await planTexture(tex, root, plan.mergePlan.atlasSize); if (p) plan.texPlans.push(p) }
|
|
237
|
+
}
|
|
238
|
+
// …and the geometry passes are planned AGAIN: their plans were made in `analyzeDoc`, against primitives
|
|
239
|
+
// the merge has just disposed (a plan that kept one died on it: `pos.getCount` of null).
|
|
240
|
+
plan.lodPlan = planLod(doc, opts)
|
|
241
|
+
plan.cardTrimPlan = planCardTrim(doc, opts)
|
|
242
|
+
}
|
|
202
243
|
// LOD levels after every geometry pass (they copy the final base mesh) and before textures.
|
|
203
244
|
const lod = plan.lodPlan ? await applyLod(doc, plan.lodPlan) : null
|
|
245
|
+
// Card trimming before the texture passes: it decodes the base colour (PNG / JPEG), which a KTX2
|
|
246
|
+
// encode below would take away.
|
|
247
|
+
const cards = plan.cardTrimPlan.enabled ? await applyCardTrim(doc, plan.cardTrimPlan) : null
|
|
204
248
|
let ktx2Encoded = 0
|
|
205
249
|
if (opts.ktx2) {
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
208
|
-
|
|
209
|
-
|
|
250
|
+
const enc = opts.ktxEncoder ?? findKtxEncoder()
|
|
251
|
+
if (!enc) throw new Error(KTX_ENCODER_MISSING)
|
|
252
|
+
// Launched together, not one after another: `applyKtx2` bounds the real concurrency with its own
|
|
253
|
+
// pool of encoder processes (LECODES_KTX_JOBS, default one per core less one). Encoding used to be
|
|
254
|
+
// strictly sequential — a `spawnSync` per texture blocked the event loop — so a project's whole
|
|
255
|
+
// texture set went through one core however many were idle.
|
|
256
|
+
await Promise.all(plan.ktx2Plans.map(async (t) => {
|
|
257
|
+
await applyKtx2(t, enc, maxSize)
|
|
210
258
|
opts.progress?.(++ktx2Encoded, plan.ktx2Plans.length)
|
|
211
|
-
}
|
|
259
|
+
}))
|
|
212
260
|
// KTX2 textures must be written via KHR_texture_basisu (core texture.source allows only PNG/JPEG);
|
|
213
261
|
// gltf-transform only emits that form when the extension is attached to the document.
|
|
214
262
|
if (plan.ktx2Plans.length && !doc.getRoot().listExtensionsUsed().some((e) => e.extensionName === 'KHR_texture_basisu')) {
|
|
@@ -224,7 +272,23 @@ export async function applyPlan(doc: Document, plan: DoctorPlan, opts: ApplyOpti
|
|
|
224
272
|
if (ext.extensionName === 'EXT_texture_webp') ext.dispose()
|
|
225
273
|
}
|
|
226
274
|
}
|
|
227
|
-
|
|
275
|
+
// LAST, after every pass that can eat geometry: a primitive with no vertices left is not a
|
|
276
|
+
// primitive. Card trimming produces them — it cuts an alpha-MASK quad down to its opaque texels,
|
|
277
|
+
// and the Modular Shooting Range's floor stripes have LOD levels whose texels are all clear, so
|
|
278
|
+
// the level came out carrying five empty meshes. That is not a cosmetic problem: filament's
|
|
279
|
+
// gltfio walks the primitive in `ResourceLoader::loadResources` and takes the host down with an
|
|
280
|
+
// access violation before the first frame.
|
|
281
|
+
let emptied = 0
|
|
282
|
+
for (const mesh of doc.getRoot().listMeshes()) {
|
|
283
|
+
for (const prim of mesh.listPrimitives()) {
|
|
284
|
+
if ((prim.getAttribute("POSITION")?.getCount() ?? 0) > 0) continue
|
|
285
|
+
mesh.removePrimitive(prim)
|
|
286
|
+
prim.dispose()
|
|
287
|
+
emptied++
|
|
288
|
+
}
|
|
289
|
+
if (!mesh.listPrimitives().length) mesh.dispose()
|
|
290
|
+
}
|
|
291
|
+
return { metallicZeroed, bareFixed, ktx2Encoded, scaled: plan.scalePlan?.factor ?? 1, unlit: shading.unlit, emissiveCleared: shading.emissiveCleared, lod, cards, merge, emptied }
|
|
228
292
|
}
|
|
229
293
|
|
|
230
294
|
/** Output path for a doctored file: single file + -o → exact path; -o as dir (or batch) → derived name inside it. */
|