fix(falukant): combine overview house and character scene
All checks were successful
Deploy to production / deploy (push) Successful in 2m2s
All checks were successful
Deploy to production / deploy (push) Successful in 2m2s
This commit is contained in:
@@ -8,12 +8,20 @@
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
const MODEL_BASE_PATH = '/models/3d/falukant/houses';
|
||||
const CHARACTER_MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
||||
const CHARACTER_MODEL_AGES = Object.freeze([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 25, 30, 40, 50, 60]);
|
||||
const CHARACTER_MODEL_ASSET_REVISION = '20260930-2';
|
||||
|
||||
export default {
|
||||
name: 'House3D',
|
||||
props: {
|
||||
model: { type: String, required: true },
|
||||
label: { type: String, default: '' },
|
||||
// When supplied, the character is loaded into the same Three.js scene as
|
||||
// the house. This keeps their depth relationship intact while rotating or
|
||||
// zooming the overview scene.
|
||||
characterGender: { type: String, default: null },
|
||||
characterAge: { type: Number, default: null },
|
||||
// List previews must not keep a WebGL context while they are outside the
|
||||
// scroll viewport. A page with several houses otherwise exhausts the
|
||||
// browser's per-site context limit after navigating around Falukant.
|
||||
@@ -25,6 +33,7 @@ export default {
|
||||
camera: null,
|
||||
renderer: null,
|
||||
renderedModel: null,
|
||||
renderedCharacter: null,
|
||||
controls: null,
|
||||
onControlsChange: null,
|
||||
resizeObserver: null,
|
||||
@@ -38,6 +47,12 @@ export default {
|
||||
model() {
|
||||
if (this.isVisible) this.loadModel();
|
||||
},
|
||||
characterGender() {
|
||||
if (this.isVisible) this.loadModel();
|
||||
},
|
||||
characterAge() {
|
||||
if (this.isVisible) this.loadModel();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.lazy) {
|
||||
@@ -110,18 +125,18 @@ export default {
|
||||
const lifecycleToken = this.lifecycleToken;
|
||||
this.loadFailed = false;
|
||||
this.removeModel();
|
||||
let dracoLoader = null;
|
||||
|
||||
try {
|
||||
const [loaders, modelRuntime] = await Promise.all([
|
||||
import('@/utils/threeLoaders.js'),
|
||||
import('@/utils/threeModelRuntime.js'),
|
||||
]);
|
||||
const dracoLoader = new loaders.DRACOLoader();
|
||||
dracoLoader = new loaders.DRACOLoader();
|
||||
dracoLoader.setDecoderPath('/draco/gltf/');
|
||||
const loader = new loaders.GLTFLoader();
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
const gltf = await loader.loadAsync(`${MODEL_BASE_PATH}/${this.model}_opt.glb`);
|
||||
dracoLoader.dispose();
|
||||
|
||||
if (!this.isVisible || lifecycleToken !== this.lifecycleToken || !this.scene) {
|
||||
this.disposeObject(gltf.scene);
|
||||
@@ -141,13 +156,62 @@ export default {
|
||||
const center = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||
this.renderedModel.position.set(-center.x, -scaledBox.min.y, -center.z);
|
||||
this.scene.add(this.renderedModel);
|
||||
|
||||
await this.loadForegroundCharacter(loader, modelRuntime, lifecycleToken);
|
||||
this.controls?.update();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error(`Unable to load 3D house model '${this.model}'`, error);
|
||||
this.loadFailed = true;
|
||||
} finally {
|
||||
dracoLoader?.dispose();
|
||||
}
|
||||
},
|
||||
async loadForegroundCharacter(loader, modelRuntime, lifecycleToken) {
|
||||
const gender = this.characterGender === 'female' ? 'female' : this.characterGender === 'male' ? 'male' : null;
|
||||
const requestedAge = Number(this.characterAge);
|
||||
if (!gender || !Number.isFinite(requestedAge)) return;
|
||||
|
||||
const candidateAges = CHARACTER_MODEL_AGES
|
||||
.filter((age) => age <= requestedAge)
|
||||
.sort((left, right) => right - left);
|
||||
let gltf = null;
|
||||
for (const age of candidateAges) {
|
||||
try {
|
||||
gltf = await loader.loadAsync(
|
||||
`${CHARACTER_MODELS_API_PATH}/${gender}_${age}y_opt.glb?v=${CHARACTER_MODEL_ASSET_REVISION}`
|
||||
);
|
||||
break;
|
||||
} catch (_) {
|
||||
// The next lower concrete age is an intentional fallback.
|
||||
}
|
||||
}
|
||||
if (!gltf) return;
|
||||
|
||||
if (!this.isVisible || lifecycleToken !== this.lifecycleToken || !this.scene) {
|
||||
this.disposeObject(gltf.scene);
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderedCharacter = markRaw(gltf.scene);
|
||||
const box = new modelRuntime.Box3().setFromObject(this.renderedCharacter);
|
||||
const size = box.getSize(new modelRuntime.Vector3());
|
||||
const maxDimension = Math.max(size.x, size.y, size.z);
|
||||
if (!Number.isFinite(maxDimension) || maxDimension <= 0) {
|
||||
this.disposeObject(this.renderedCharacter);
|
||||
this.renderedCharacter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const ageScale = requestedAge <= 3 ? 0.6 : requestedAge <= 7 ? 0.8 : requestedAge <= 12 ? 0.9 : 0.98;
|
||||
this.renderedCharacter.scale.setScalar((1.7 / maxDimension) * ageScale);
|
||||
const scaledBox = new modelRuntime.Box3().setFromObject(this.renderedCharacter);
|
||||
const center = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||
// The house model is centered at z=0. Place the figure a little in
|
||||
// front of it, rather than overlaying an unrelated canvas.
|
||||
this.renderedCharacter.position.set(-center.x, -scaledBox.min.y, 0.62 - center.z);
|
||||
this.scene.add(this.renderedCharacter);
|
||||
},
|
||||
render() {
|
||||
const container = this.$refs.container;
|
||||
if (!container || !this.renderer || !this.camera || !this.scene) return;
|
||||
@@ -159,10 +223,12 @@ export default {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
},
|
||||
removeModel() {
|
||||
if (!this.renderedModel) return;
|
||||
this.scene.remove(this.renderedModel);
|
||||
this.disposeObject(this.renderedModel);
|
||||
[this.renderedModel, this.renderedCharacter].filter(Boolean).forEach((model) => {
|
||||
this.scene?.remove(model);
|
||||
this.disposeObject(model);
|
||||
});
|
||||
this.renderedModel = null;
|
||||
this.renderedCharacter = null;
|
||||
},
|
||||
disposeObject(object3d) {
|
||||
object3d?.traverse((object) => {
|
||||
|
||||
@@ -34,14 +34,9 @@
|
||||
class="house"
|
||||
:model="houseModel"
|
||||
:label="$t(`falukant.house.type.${falukantUser.userHouse.houseType.labelTr}`)"
|
||||
:character-gender="falukantUser.character.gender"
|
||||
:character-age="falukantUser.character.age"
|
||||
/>
|
||||
<div class="character-foreground">
|
||||
<Character3D
|
||||
:gender="falukantUser.character.gender"
|
||||
:age="falukantUser.character.age"
|
||||
:no-background="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -369,7 +364,6 @@
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import Character3D from '@/components/Character3D.vue';
|
||||
import House3D from '@/components/falukant/House3D.vue';
|
||||
import { getFalukantHouseModel } from '@/utils/falukantHouseModels.js';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
@@ -432,7 +426,6 @@ export default {
|
||||
name: 'FalukantOverviewView',
|
||||
components: {
|
||||
StatusBar,
|
||||
Character3D,
|
||||
House3D,
|
||||
},
|
||||
data() {
|
||||
@@ -1392,23 +1385,10 @@ export default {
|
||||
}
|
||||
|
||||
.house {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background-repeat: no-repeat;
|
||||
image-rendering: crisp-edges;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.character-foreground {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 55%;
|
||||
height: 55%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.heir-selection-container {
|
||||
|
||||
Reference in New Issue
Block a user