| 123 | // it up in their own material table. |
| 124 | const groups = []; |
| 125 | const startGroup = (materialName) => { |
| 126 | // close the previous group if it has any indices |
| 127 | const prev = groups[groups.length - 1]; |
| 128 | if (prev) { |
| 129 | prev.count = indices.length - prev.start; |
| 130 | } else if (indices.length > 0) { |
| 131 | // pre-usemtl indices belong to an anonymous group |
| 132 | groups.push({ |
| 133 | materialName: null, |
| 134 | start: 0, |
| 135 | count: indices.length, |
| 136 | }); |
| 137 | } |
| 138 | groups.push({ materialName, start: indices.length, count: 0 }); |
| 139 | // Swap to this material's vertex dedup scope. Vertices shared |
| 140 | // across materials get separate slots (required for per-vertex |
| 141 | // color baking in `Mesh`), but vertices reused within the same |
| 142 | // material — even across non-contiguous `usemtl` blocks — hit |
| 143 | // the cache and don't get duplicated. Lazily allocated per |
| 144 | // material name on first switch. |
| 145 | let cached = materialMaps.get(materialName); |
| 146 | if (cached === undefined) { |
| 147 | cached = new Map(); |
| 148 | materialMaps.set(materialName, cached); |
| 149 | } |
| 150 | vertexMap = cached; |
| 151 | }; |
| 152 | |
| 153 | // parse lines and build geometry in a single pass |
| 154 | const lines = text.split("\n"); |