Skip to content

Commit c85eeef

Browse files
authored
Feature/vehicle part editor (#14)
* Add vehicle part color editor to save editor - Add Vehicles tab with 3D preview of customizable vehicle parts - Support all 43 colorable parts across 4 vehicles (dune buggy, helicopter, jetski, race car) - Implement shared LOD resolution for proper rendering of all parts - Extract reusable NavButton and ResetButton components - Remove debug console.log statements from ScoreCube * Reserve space for reset button in vehicle editor to prevent layout shift * Add tooltip to vehicle editor explaining click-to-cycle interaction * Add scroll-into-view behavior for name slots on mobile When name slots overflow on narrow screens, focusing a partially visible slot now smoothly scrolls it into view. Scrollbar is hidden for cleaner UI. * Extract EditorTooltip component for consistent tooltip positioning Refactored tooltip markup into reusable EditorTooltip component used by both ScoreCube and VehicleEditor. Tooltip now consistently appears in top right corner of the editor section. * Add transparency support to vehicle part rendering Apply mesh alpha values from WDB to Three.js materials. In the original game, alpha=0 means opaque while alpha>0 enables transparency. Disable depthWrite for transparent meshes to prevent z-fighting. * Use MeshLambertMaterial for original game-like rendering Switch from MeshStandardMaterial (PBR) to MeshLambertMaterial for flat, vibrant colors matching the original game. Simplify lighting setup for solid colors without visible shadows. * Fix WDB texture parsing for parts vs models Parts and models have different texture info formats - models include a skipTextures field that parts don't have. Add isModel parameter to parseTextureInfo to handle this difference correctly. Also remove silent catch blocks and overly defensive checks.
1 parent 36a6e0f commit c85eeef

File tree

13 files changed

+1008
-155
lines changed

13 files changed

+1008
-155
lines changed

src/core/formats/WdbParser.js

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ export class WdbParser {
3737
const nameLen = this.reader.readS32();
3838
const name = this.reader.readString(nameLen).replace(/\0/g, '');
3939

40-
// Parse parts (skip for now)
40+
// Parse parts
4141
const numParts = this.reader.readS32();
42+
const parts = [];
4243
for (let i = 0; i < numParts; i++) {
43-
this.skipPartReference();
44+
parts.push(this.parsePartReference());
4445
}
4546

4647
// Parse models
@@ -50,14 +51,15 @@ export class WdbParser {
5051
models.push(this.parseModelEntry());
5152
}
5253

53-
return { name, numParts, models };
54+
return { name, numParts, parts, models };
5455
}
5556

56-
skipPartReference() {
57+
parsePartReference() {
5758
const nameLen = this.reader.readU32();
58-
this.reader.skip(nameLen); // name
59-
this.reader.skip(4); // data_length
60-
this.reader.skip(4); // data_offset
59+
const name = this.reader.readString(nameLen).replace(/\0/g, '');
60+
const dataLength = this.reader.readU32();
61+
const dataOffset = this.reader.readU32();
62+
return { name, dataLength, dataOffset };
6163
}
6264

6365
parseModelEntry() {
@@ -90,6 +92,39 @@ export class WdbParser {
9092
return this.reader.readString(length).replace(/\0/g, '');
9193
}
9294

95+
/**
96+
* Parse part data blob at specified offset
97+
* Parts have a simpler structure than models - no animation, direct LOD data
98+
* @param {number} offset - Absolute file offset
99+
* @returns {{ parts: Array, textures: Array }}
100+
*/
101+
parsePartData(offset) {
102+
this.reader.seek(offset);
103+
104+
const textureInfoOffset = this.reader.readU32();
105+
const numRois = this.reader.readU32();
106+
const parts = [];
107+
108+
for (let i = 0; i < numRois; i++) {
109+
const nameLen = this.reader.readU32();
110+
const name = this.readCleanString(nameLen);
111+
const numLods = this.reader.readU32();
112+
const roiInfoOffset = this.reader.readU32();
113+
114+
const lods = [];
115+
for (let j = 0; j < numLods; j++) {
116+
lods.push(this.parseLod());
117+
}
118+
119+
parts.push({ name, lods });
120+
}
121+
122+
this.reader.seek(offset + textureInfoOffset);
123+
const textures = this.parseTextureInfo();
124+
125+
return { parts, textures };
126+
}
127+
93128
/**
94129
* Parse model_data blob at specified offset
95130
* @param {number} offset - Absolute file offset
@@ -112,9 +147,8 @@ export class WdbParser {
112147
// Parse ROI hierarchy
113148
const roi = this.parseRoi();
114149

115-
// Parse textures at textureInfoOffset
116150
this.reader.seek(offset + textureInfoOffset);
117-
const textures = this.parseTextureInfo();
151+
const textures = this.parseTextureInfo(true); // Models have skipTextures field
118152

119153
return { version, anim, roi, textures };
120154
}
@@ -264,7 +298,7 @@ export class WdbParser {
264298
children.push(this.parseRoi());
265299
}
266300

267-
return { name, boundingSphere, boundingBox, textureName, lods, children };
301+
return { name, boundingSphere, boundingBox, textureName, sharedLodList: sharedLodList !== 0, lods, children };
268302
}
269303

270304
parseLod() {
@@ -366,9 +400,19 @@ export class WdbParser {
366400
return { color, alpha, shading, useAlias, textureName, materialName };
367401
}
368402

369-
parseTextureInfo() {
403+
/**
404+
* Parse texture info block
405+
* @param {boolean} isModel - If true, read skipTextures field (models have it, parts don't)
406+
*/
407+
parseTextureInfo(isModel = false) {
370408
const numTextures = this.reader.readU32();
371-
const skipTextures = this.reader.readU32();
409+
410+
// Models have an extra skipTextures field that parts don't have
411+
// See legomodelpresenter.cpp vs legopartpresenter.cpp in LEGO1 source
412+
if (isModel) {
413+
this.reader.readU32(); // skipTextures - skip over this field
414+
}
415+
372416
const textures = [];
373417

374418
for (let i = 0; i < numTextures; i++) {
@@ -437,3 +481,48 @@ export function findRoi(roi, name) {
437481
}
438482
return null;
439483
}
484+
485+
/**
486+
* Resolve LODs for an ROI, handling shared LOD lists
487+
* This mirrors how the game's ViewLODListManager resolves shared parts
488+
* @param {object} roi - ROI data with lods and sharedLodList flag
489+
* @param {Map} partsMap - Map of part name (lowercase) -> part data with lods
490+
* @returns {Array} - Array of LODs (may be empty)
491+
*/
492+
export function resolveLods(roi, partsMap) {
493+
// If ROI has its own LODs, use them
494+
if (roi.lods && roi.lods.length > 0) {
495+
return roi.lods;
496+
}
497+
498+
// If ROI uses shared LOD list, look up by name (strip trailing digits)
499+
// This matches the game's logic in LegoROI::Read
500+
if (roi.sharedLodList && roi.name && partsMap) {
501+
const baseName = roi.name.replace(/\d+$/, '').toLowerCase();
502+
const part = partsMap.get(baseName);
503+
if (part && part.lods && part.lods.length > 0) {
504+
return part.lods;
505+
}
506+
}
507+
508+
return [];
509+
}
510+
511+
/**
512+
* Build a parts lookup map from a world's parts array
513+
* @param {WdbParser} parser - Parser instance for reading part data
514+
* @param {Array} worldParts - Array of part references from world entry
515+
* @returns {Map} - Map of part name (lowercase) -> part data
516+
*/
517+
export function buildPartsMap(parser, worldParts) {
518+
const partsMap = new Map();
519+
if (!worldParts || worldParts.length === 0) return partsMap;
520+
521+
for (const partRef of worldParts) {
522+
const partData = parser.parsePartData(partRef.dataOffset);
523+
for (const part of partData.parts) {
524+
partsMap.set(part.name.toLowerCase(), part);
525+
}
526+
}
527+
return partsMap;
528+
}

0 commit comments

Comments
 (0)