Enhance usability and localization across components: Update USABILITY_CONCEPT.md with new focus areas, improve user feedback in AppFooter and FamilyView components, and refine text in various UI elements for better clarity and consistency. Replace console logs with user-friendly messages, correct German translations, and streamline interaction logic in multiple components.

This commit is contained in:
Torsten Schulz (local)
2026-03-20 09:41:03 +01:00
parent 1774d7df88
commit c7d33525ff
48 changed files with 1161 additions and 481 deletions

View File

@@ -17,23 +17,33 @@ import { getApiBaseURL } from '@/utils/axios.js';
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
let threeRuntimePromise = null;
let threeLoadersPromise = null;
let threeModelRuntimePromise = null;
async function loadThreeRuntime() {
if (!threeRuntimePromise) {
threeRuntimePromise = Promise.all([
import('three'),
import('three/addons/loaders/GLTFLoader.js'),
import('three/addons/loaders/DRACOLoader.js')
]).then(([THREE, { GLTFLoader }, { DRACOLoader }]) => ({
THREE,
GLTFLoader,
DRACOLoader
}));
threeRuntimePromise = import('@/utils/threeRuntime.js');
}
return threeRuntimePromise;
}
async function loadThreeLoaders() {
if (!threeLoadersPromise) {
threeLoadersPromise = import('@/utils/threeLoaders.js');
}
return threeLoadersPromise;
}
async function loadThreeModelRuntime() {
if (!threeModelRuntimePromise) {
threeModelRuntimePromise = import('@/utils/threeModelRuntime.js');
}
return threeModelRuntimePromise;
}
export default {
name: 'Character3D',
props: {
@@ -65,7 +75,9 @@ export default {
clock: null,
baseYPosition: 0,
showFallback: false,
threeRuntime: null
threeRuntime: null,
threeLoaders: null,
threeModelRuntime: null
};
},
computed: {
@@ -149,49 +161,65 @@ export default {
return this.threeRuntime;
},
async ensureThreeLoaders() {
if (!this.threeLoaders) {
this.threeLoaders = markRaw(await loadThreeLoaders());
}
return this.threeLoaders;
},
async ensureThreeModelRuntime() {
if (!this.threeModelRuntime) {
this.threeModelRuntime = markRaw(await loadThreeModelRuntime());
}
return this.threeModelRuntime;
},
async init3D() {
const container = this.$refs.container;
if (!container) return;
this.showFallback = false;
const { THREE } = await this.ensureThreeRuntime();
this.clock = markRaw(new THREE.Clock());
const runtime = await this.ensureThreeRuntime();
this.clock = markRaw(new runtime.Clock());
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
this.scene = markRaw(new THREE.Scene());
this.scene = markRaw(new runtime.Scene());
if (!this.noBackground) {
this.scene.background = new THREE.Color(0xf0f0f0);
this.scene.background = new runtime.Color(0xf0f0f0);
await this.loadBackground();
}
// Camera erstellen
const aspect = container.clientWidth / container.clientHeight;
this.camera = markRaw(new THREE.PerspectiveCamera(50, aspect, 0.1, 1000));
this.camera = markRaw(new runtime.PerspectiveCamera(50, aspect, 0.1, 1000));
this.camera.position.set(0, 1.5, 3);
this.camera.lookAt(0, 1, 0);
// Renderer erstellen
this.renderer = markRaw(new THREE.WebGLRenderer({ antialias: true, alpha: true }));
this.renderer = markRaw(new runtime.WebGLRenderer({ antialias: true, alpha: true }));
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(this.renderer.domElement);
// Verbesserte Beleuchtung für hellere Modelle
// Mehr ambient light für gleichmäßigere Ausleuchtung
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
const ambientLight = new runtime.AmbientLight(0xffffff, 1.0);
this.scene.add(ambientLight);
// Hauptlicht von vorne oben - stärker
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
const directionalLight = new runtime.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(5, 10, 5);
this.scene.add(directionalLight);
// Zusätzliches Licht von hinten - heller
const backLight = new THREE.DirectionalLight(0xffffff, 0.75);
const backLight = new runtime.DirectionalLight(0xffffff, 0.75);
backLight.position.set(-5, 5, -5);
this.scene.add(backLight);
// Zusätzliches Seitenlicht für mehr Tiefe
const sideLight = new THREE.DirectionalLight(0xffffff, 0.5);
const sideLight = new runtime.DirectionalLight(0xffffff, 0.5);
sideLight.position.set(-5, 5, 5);
this.scene.add(sideLight);
@@ -200,13 +228,13 @@ export default {
},
async loadBackground() {
const { THREE } = await this.ensureThreeRuntime();
const runtime = await this.ensureThreeRuntime();
// Optimierte Versionen (512×341, ~130 KB); Originale ~3 MB
const backgrounds = ['bg1_opt.png', 'bg2_opt.png'];
const randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
const bgPath = `/images/falukant/backgrounds/${randomBg}`;
const loader = new THREE.TextureLoader();
const loader = new runtime.TextureLoader();
loader.load(
bgPath,
(texture) => {
@@ -220,7 +248,7 @@ export default {
console.warn('Fehler beim Laden des Hintergrunds:', error);
// Fallback auf Standardfarbe bei Fehler
if (this.scene) {
this.scene.background = new THREE.Color(0xf0f0f0);
this.scene.background = new runtime.Color(0xf0f0f0);
}
}
);
@@ -228,7 +256,8 @@ export default {
async loadModel() {
if (!this.scene) return;
const { THREE, GLTFLoader, DRACOLoader } = await this.ensureThreeRuntime();
const modelRuntime = await this.ensureThreeModelRuntime();
const loaders = await this.ensureThreeLoaders();
// Altes Modell entfernen
if (this.model) {
@@ -252,9 +281,9 @@ export default {
}
try {
const dracoLoader = new DRACOLoader();
const dracoLoader = new loaders.DRACOLoader();
dracoLoader.setDecoderPath('/draco/gltf/');
const loader = new GLTFLoader();
const loader = new loaders.GLTFLoader();
loader.setDRACOLoader(dracoLoader);
const base = getApiBaseURL();
@@ -273,12 +302,12 @@ export default {
// Versuche zuerst genaues Alter
try {
gltf = await loader.loadAsync(exactAgePath);
console.log(`Loaded exact age model: ${exactAgePath}`);
console.debug(`Loaded exact age model: ${exactAgePath}`);
} catch (exactAgeError) {
// Falls genaues Alter nicht existiert, versuche Altersbereich
try {
gltf = await loader.loadAsync(ageGroupPath);
console.log(`Loaded age group model: ${ageGroupPath}`);
console.debug(`Loaded age group model: ${ageGroupPath}`);
} catch (ageGroupError) {
// Falls Altersbereich nicht existiert, verwende Basis-Modell
console.warn(`Could not load ${ageGroupPath}, trying fallback model`);
@@ -293,8 +322,8 @@ export default {
this.model = markRaw(gltf.scene);
// Initiale Bounding Box für Größenberechnung (vor Skalierung)
const initialBox = new THREE.Box3().setFromObject(this.model);
const initialSize = initialBox.getSize(new THREE.Vector3());
const initialBox = new modelRuntime.Box3().setFromObject(this.model);
const initialSize = initialBox.getSize(new modelRuntime.Vector3());
// Skalierung basierend auf Alter
const age = this.actualAge;
@@ -318,8 +347,8 @@ export default {
this.model.scale.set(modelScale, modelScale, modelScale);
// Bounding Box NACH dem Skalieren neu berechnen
const scaledBox = new THREE.Box3().setFromObject(this.model);
const scaledCenter = scaledBox.getCenter(new THREE.Vector3());
const scaledBox = new modelRuntime.Box3().setFromObject(this.model);
const scaledCenter = scaledBox.getCenter(new modelRuntime.Vector3());
// Modell zentrieren basierend auf der skalierten Bounding Box
// Position direkt setzen statt zu subtrahieren, um Proxy-Probleme zu vermeiden
@@ -331,7 +360,7 @@ export default {
// Animationen laden falls vorhanden
if (gltf.animations && gltf.animations.length > 0) {
this.mixer = markRaw(new THREE.AnimationMixer(this.model));
this.mixer = markRaw(new modelRuntime.AnimationMixer(this.model));
gltf.animations.forEach((clip) => {
this.mixer.clipAction(clip).play();
});