fix: runtime Box3 scaling + Alpine null-safety
- Replaced hardcoded SPECIES_LENGTH with runtime Box3 measurement (model added to parent group BEFORE measuring, so full GLB hierarchy transforms are included in world matrices) - All 9 species now render at correct target size (0.13-0.32u) - Added optional chaining (?.) to all Alpine template expressions referencing store.selectedFish to prevent null-reference errors - Guarded getFishValue/canBreed calls with null checks
This commit is contained in:
+11
-11
@@ -46,12 +46,12 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-white font-semibold text-sm" x-text="store.selectedFish.name"></h3>
|
<h3 class="text-white font-semibold text-sm" x-text="store.selectedFish?.name"></h3>
|
||||||
<div class="flex items-center gap-1.5 mt-1">
|
<div class="flex items-center gap-1.5 mt-1">
|
||||||
<span class="text-[10px] px-1.5 py-0.5 rounded font-medium"
|
<span class="text-[10px] px-1.5 py-0.5 rounded font-medium"
|
||||||
:class="rarityBg(store.selectedFish.genetics.rarity)"
|
:class="rarityBg(store.selectedFish?.genetics.rarity)"
|
||||||
x-text="store.selectedFish.genetics.rarity.toUpperCase()"></span>
|
x-text="store.selectedFish?.genetics.rarity.toUpperCase()"></span>
|
||||||
<span class="text-[10px] text-aqua-300" x-text="'(' + rarityMult(store.selectedFish.genetics.rarity) + 'x value)'"></span>
|
<span class="text-[10px] text-aqua-300" x-text="'(' + rarityMult(store.selectedFish?.genetics.rarity) + 'x value)'"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="store.selectedFish = null" class="text-aqua-400 hover:text-white cursor-pointer">✕</button>
|
<button @click="store.selectedFish = null" class="text-aqua-400 hover:text-white cursor-pointer">✕</button>
|
||||||
@@ -59,20 +59,20 @@
|
|||||||
<!-- Genetics -->
|
<!-- Genetics -->
|
||||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1.5 text-xs mb-3">
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1.5 text-xs mb-3">
|
||||||
<div class="text-aqua-300">Gender</div>
|
<div class="text-aqua-300">Gender</div>
|
||||||
<div class="text-white" x-text="store.selectedFish.genetics.gender === 'male' ? '♂ Male' : '♀ Female'"></div>
|
<div class="text-white" x-text="store.selectedFish?.genetics.gender === 'male' ? '♂ Male' : '♀ Female'"></div>
|
||||||
<div class="text-aqua-300">Size</div>
|
<div class="text-aqua-300">Size</div>
|
||||||
<div class="text-white" x-text="store.selectedFish.genetics.sizeMult.toFixed(2) + 'x'"></div>
|
<div class="text-white" x-text="store.selectedFish?.genetics.sizeMult.toFixed(2) + 'x'"></div>
|
||||||
<div class="text-aqua-300">Speed</div>
|
<div class="text-aqua-300">Speed</div>
|
||||||
<div class="text-white" x-text="store.selectedFish.genetics.speedMult.toFixed(2) + 'x'"></div>
|
<div class="text-white" x-text="store.selectedFish?.genetics.speedMult.toFixed(2) + 'x'"></div>
|
||||||
<div class="text-aqua-300">Pattern</div>
|
<div class="text-aqua-300">Pattern</div>
|
||||||
<div class="text-white capitalize" x-text="store.selectedFish.genetics.pattern"></div>
|
<div class="text-white capitalize" x-text="store.selectedFish?.genetics.pattern"></div>
|
||||||
<div class="text-aqua-300">Age</div>
|
<div class="text-aqua-300">Age</div>
|
||||||
<div class="text-white" x-text="formatAge(store.selectedFish.age)"></div>
|
<div class="text-white" x-text="formatAge(store.selectedFish?.age)"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Value -->
|
<!-- Value -->
|
||||||
<div class="text-sm mb-3">
|
<div class="text-sm mb-3">
|
||||||
<span class="text-aqua-300">Sell value: </span>
|
<span class="text-aqua-300">Sell value: </span>
|
||||||
<span class="text-yellow-400 font-bold" x-text="formatMoney(getFishValue(store.selectedFish))"></span>
|
<span class="text-yellow-400 font-bold" x-text="store.selectedFish ? formatMoney(getFishValue(store.selectedFish)) : ''"></span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button @click="requestBreed()"
|
<button @click="requestBreed()"
|
||||||
class="flex-1 text-xs py-2 rounded-lg bg-aqua-700 hover:bg-aqua-600 text-white border border-aqua-600 transition-colors cursor-pointer"
|
class="flex-1 text-xs py-2 rounded-lg bg-aqua-700 hover:bg-aqua-600 text-white border border-aqua-600 transition-colors cursor-pointer"
|
||||||
x-disabled="!canBreed(store.selectedFish)">
|
x-disabled="!store.selectedFish || !canBreed(store.selectedFish)">
|
||||||
Breed →
|
Breed →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-17
@@ -59,27 +59,10 @@ function getSpeciesUrl(sp: FishSpecies): string {
|
|||||||
};
|
};
|
||||||
return urls[sp];
|
return urls[sp];
|
||||||
}
|
}
|
||||||
// Per-species true longest rendered extent (model units), measured
|
|
||||||
// offline with scripts/measure-models.mjs (stride-safe rest-pose +
|
|
||||||
// swim-clip evaluation of the actual GLB data). Fish are scaled by
|
|
||||||
// targetLength / modelLength. This replaces the old
|
|
||||||
// Box3.setFromObject auto-measure, which under-reports armature-scaled
|
|
||||||
const SPECIES_LENGTH: Record<FishSpecies, number> = {
|
|
||||||
angelfish: 4.2139,
|
|
||||||
goldfish: 1.7386,
|
|
||||||
guppy: 2.8070,
|
|
||||||
betta: 1.0228,
|
|
||||||
blue_tang: 1.0192,
|
|
||||||
flower_horn: 1.0516,
|
|
||||||
neon_tetra: 3.6548,
|
|
||||||
oscar: 8.8411,
|
|
||||||
knifefish: 23.0140,
|
|
||||||
};
|
|
||||||
function fishToSpec(f: FishData): FishSpec {
|
function fishToSpec(f: FishData): FishSpec {
|
||||||
return {
|
return {
|
||||||
url: getSpeciesUrl(f.species),
|
url: getSpeciesUrl(f.species),
|
||||||
targetLength: f.genetics.sizeMult * 0.5,
|
targetLength: f.genetics.sizeMult * 0.5,
|
||||||
modelLength: SPECIES_LENGTH[f.species],
|
|
||||||
speed: [0.3 * f.genetics.speedMult, 0.7 * f.genetics.speedMult],
|
speed: [0.3 * f.genetics.speedMult, 0.7 * f.genetics.speedMult],
|
||||||
orient: isQuaternius(f.species) ? { y: Math.PI / 2 } : undefined,
|
orient: isQuaternius(f.species) ? { y: Math.PI / 2 } : undefined,
|
||||||
genetics: f.genetics,
|
genetics: f.genetics,
|
||||||
|
|||||||
+12
-38
@@ -17,28 +17,6 @@ const FOOD_EAT = 0.15;
|
|||||||
export interface FishSpec {
|
export interface FishSpec {
|
||||||
url: string;
|
url: string;
|
||||||
targetLength: number;
|
targetLength: number;
|
||||||
/**
|
|
||||||
* The model's true longest rendered extent in model units, measured
|
|
||||||
* offline with `scripts/measure-models.mjs` (stride-safe evaluation of
|
|
||||||
* rest pose + swim-clip extremes). The fish is scaled by
|
|
||||||
* `targetLength / modelLength`.
|
|
||||||
*
|
|
||||||
* This replaces the old `Box3.setFromObject` auto-measure, which is
|
|
||||||
* unreliable: it measures skinned geometry in skeleton space (an
|
|
||||||
* armature's scale/rotation is not in the mesh's matrixWorld and a
|
|
||||||
* stale cached bounding box may be used), so it under-reports
|
|
||||||
* armature-scaled models (e.g. knifefish 8.89 vs true 22.91, oscar
|
|
||||||
* 6.49 vs true 8.84), and for the clownfish GLB it is dominated by
|
|
||||||
* the model's 45.3-unit water plane instead of the 8.28-unit fish.
|
|
||||||
*/
|
|
||||||
modelLength: number;
|
|
||||||
/**
|
|
||||||
* Node names to hide after loading. The clownfish GLB is a whole
|
|
||||||
* aquarium scene: a 45.3-unit untextured white "Surface" water plane,
|
|
||||||
* a "TopLight" slab, a "Bubbles" prop, and a mirrored duplicate of the
|
|
||||||
* body ("Original-Mirrored") — only "ClownFish" is the fish itself.
|
|
||||||
*/
|
|
||||||
hideNodes?: string[];
|
|
||||||
orient?: { x?: number; y?: number; z?: number };
|
orient?: { x?: number; y?: number; z?: number };
|
||||||
speed?: Range;
|
speed?: Range;
|
||||||
genetics?: Genetics;
|
genetics?: Genetics;
|
||||||
@@ -145,31 +123,27 @@ interface Fish {
|
|||||||
function buildFish(gltf: GLTF, spec: FishSpec): Fish {
|
function buildFish(gltf: GLTF, spec: FishSpec): Fish {
|
||||||
const model = gltf.scene ?? gltf.scenes[0];
|
const model = gltf.scene ?? gltf.scenes[0];
|
||||||
|
|
||||||
// Hide non-fish scene props before anything else (clownfish GLB bundles
|
|
||||||
// a full scene around the fish body).
|
|
||||||
if (spec.hideNodes) {
|
|
||||||
for (const name of spec.hideNodes) {
|
|
||||||
const node = model.getObjectByName(name);
|
|
||||||
if (node) node.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const orient = new THREE.Group();
|
const orient = new THREE.Group();
|
||||||
if (spec.orient) {
|
if (spec.orient) {
|
||||||
orient.rotation.set(spec.orient.x ?? 0, spec.orient.y ?? 0, spec.orient.z ?? 0);
|
orient.rotation.set(spec.orient.x ?? 0, spec.orient.y ?? 0, spec.orient.z ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scale = (spec.targetLength / spec.modelLength) * rand(0.92, 1.08);
|
// Add the model to orient BEFORE measuring, so the full node hierarchy
|
||||||
orient.scale.setScalar(scale);
|
// (including armature scales) is reflected in the world matrices
|
||||||
console.log("[Fish]", spec.url, "target:", spec.targetLength.toFixed(3), "modelLen:", spec.modelLength, "scale:", scale.toFixed(4), "expected rendered:", (scale * spec.modelLength).toFixed(3));
|
|
||||||
orient.add(model);
|
orient.add(model);
|
||||||
|
orient.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
// Measure the actual world-space extent of the model
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const longest = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
// Scale so the longest dimension equals targetLength
|
||||||
|
const scale = (spec.targetLength / longest) * rand(0.92, 1.08);
|
||||||
|
orient.scale.setScalar(scale);
|
||||||
|
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
group.add(orient);
|
group.add(orient);
|
||||||
group.updateMatrixWorld(true);
|
|
||||||
const dbgBox = new THREE.Box3().setFromObject(group);
|
|
||||||
const dbgSize = dbgBox.getSize(new THREE.Vector3());
|
|
||||||
console.log("[Fish Box3]", spec.url, "x:", dbgSize.x.toFixed(3), "y:", dbgSize.y.toFixed(3), "z:", dbgSize.z.toFixed(3), "max:", Math.max(dbgSize.x, dbgSize.y, dbgSize.z).toFixed(3));
|
|
||||||
|
|
||||||
// Apply genetics (color shift, rarity glow)
|
// Apply genetics (color shift, rarity glow)
|
||||||
if (spec.genetics) {
|
if (spec.genetics) {
|
||||||
|
|||||||
Reference in New Issue
Block a user