From 55d41c01fd39d31c67db0951e276a4b8314ed926 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 29 Sep 2026 10:50:41 +0200 Subject: [PATCH] perf(falukant): deploy prebuilt models and release WebGL contexts --- frontend/package.json | 1 + .../scripts/build-falukant-runtime-models.mjs | 45 +++++++++++++++++++ frontend/scripts/optimize-glb.mjs | 3 +- frontend/src/components/Character3D.vue | 7 ++- frontend/src/components/falukant/House3D.vue | 6 +++ update-frontend.sh | 34 +++++--------- 6 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 frontend/scripts/build-falukant-runtime-models.mjs diff --git a/frontend/package.json b/frontend/package.json index f761da7..8aff5d2 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "build": "vite build", "preview": "vite preview", "optimize-models": "node scripts/optimize-glb.mjs", + "build-runtime-models": "node scripts/build-falukant-runtime-models.mjs", "i18n:check-parity": "node scripts/check-i18n-locale-parity.mjs de fr", "i18n:translate-fr": "node scripts/generate-fr-locale-from-de.mjs", "i18n:smooth-falukant-fr": "node scripts/smooth-fr-falukant.mjs" diff --git a/frontend/scripts/build-falukant-runtime-models.mjs b/frontend/scripts/build-falukant-runtime-models.mjs new file mode 100644 index 0000000..b59fb5f --- /dev/null +++ b/frontend/scripts/build-falukant-runtime-models.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +/** + * Creates deployment-ready Falukant GLBs in the asset repository. + * Sources remain in models-src; only optimized *_opt.glb files are written + * to public/models and are later fetched by update-frontend.sh. + * + * Example: + * npm run build-runtime-models -- --assets-dir ../YourPart3Assets + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const frontendDir = path.resolve(scriptDir, '..'); +const args = process.argv.slice(2); +const optionIndex = args.indexOf('--assets-dir'); +const assetsDir = path.resolve(optionIndex >= 0 && args[optionIndex + 1] + ? args[optionIndex + 1] + : path.join(frontendDir, '..', '..', 'YourPart3Assets')); +const sourceRoot = path.join(assetsDir, 'models-src', '3d', 'falukant'); +const outputRoot = path.join(assetsDir, 'public', 'models', '3d', 'falukant'); + +for (const category of ['characters', 'houses']) { + const sourceDir = path.join(sourceRoot, category); + const outputDir = path.join(outputRoot, category); + if (!fs.existsSync(sourceDir)) { + throw new Error(`Missing model source directory: ${sourceDir}`); + } + + const result = spawnSync(process.execPath, [path.join(scriptDir, 'optimize-glb.mjs')], { + cwd: frontendDir, + stdio: 'inherit', + env: { + ...process.env, + MODEL_SOURCE_DIR: sourceDir, + MODEL_OUTPUT_DIR: outputDir, + CLEAN_MODEL_OUTPUT: 'true', + }, + }); + if (result.status !== 0) process.exit(result.status ?? 1); +} + +console.log(`Runtime models ready in ${outputRoot}`); diff --git a/frontend/scripts/optimize-glb.mjs b/frontend/scripts/optimize-glb.mjs index ae7c0c5..3f80ab8 100755 --- a/frontend/scripts/optimize-glb.mjs +++ b/frontend/scripts/optimize-glb.mjs @@ -35,8 +35,9 @@ function removeStaleOptimizedModels(outputDir) { if (!fs.existsSync(outputDir)) { return; } + const cleanAllModels = process.env.CLEAN_MODEL_OUTPUT === 'true'; for (const file of fs.readdirSync(outputDir)) { - if (file.endsWith('_opt.glb')) { + if (file.endsWith('_opt.glb') || (cleanAllModels && file.endsWith('.glb'))) { fs.unlinkSync(path.join(outputDir, file)); } } diff --git a/frontend/src/components/Character3D.vue b/frontend/src/components/Character3D.vue index ee222ce..6f72d23 100755 --- a/frontend/src/components/Character3D.vue +++ b/frontend/src/components/Character3D.vue @@ -332,7 +332,7 @@ export default { // Renderer erstellen this.renderer = markRaw(new runtime.WebGLRenderer({ antialias: true, alpha: true })); this.renderer.setSize(container.clientWidth, container.clientHeight); - this.renderer.setPixelRatio(window.devicePixelRatio); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); container.appendChild(this.renderer.domElement); // Verbesserte Beleuchtung für kräftigere Farben @@ -687,7 +687,12 @@ export default { if (container && this.renderer.domElement) { container.removeChild(this.renderer.domElement); } + // dispose() alone leaves the browser context alive until garbage + // collection. Route changes or long character lists can otherwise + // exceed Chromium's context limit and crash the tab. + this.renderer.forceContextLoss?.(); this.renderer.dispose(); + this.renderer = null; } if (this.scene) { diff --git a/frontend/src/components/falukant/House3D.vue b/frontend/src/components/falukant/House3D.vue index fcd8f24..fb0b5c8 100644 --- a/frontend/src/components/falukant/House3D.vue +++ b/frontend/src/components/falukant/House3D.vue @@ -124,9 +124,15 @@ export default { this.removeModel(); if (this.renderer) { this.$refs.container?.removeChild(this.renderer.domElement); + // Release immediately instead of waiting for garbage collection. + // Several house previews otherwise accumulate WebGL contexts. + this.renderer.forceContextLoss?.(); this.renderer.dispose(); + this.renderer = null; } this.scene?.clear(); + this.scene = null; + this.camera = null; }, }, }; diff --git a/update-frontend.sh b/update-frontend.sh index ae49f5a..fae7602 100755 --- a/update-frontend.sh +++ b/update-frontend.sh @@ -60,8 +60,8 @@ if [ ! -f "$MODELS_MANIFEST" ]; then exit 1 fi -# Source GLBs are versioned in their own Git-LFS repository. Keep them outside -# the application worktree, because update.sh removes untracked files there. +# Deployment-ready GLBs are versioned in their own Git-LFS repository. Source +# GLBs stay out of the deployment entirely and are optimized before publishing. # shellcheck disable=SC1090 source "$MODELS_MANIFEST" : "${FALUKANT_MODELS_REPOSITORY:?Falukant models repository is missing}" @@ -79,30 +79,18 @@ echo "Lade Falukant-3D-Modelle aus ${FALUKANT_MODELS_REPOSITORY}@${FALUKANT_MODE rm -rf "$ASSETS_DIR" GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 --branch "$FALUKANT_MODELS_REF" \ "$FALUKANT_MODELS_REPOSITORY" "$ASSETS_DIR" -git -C "$ASSETS_DIR" lfs pull +git -C "$ASSETS_DIR" lfs pull --include="public/models/3d/falukant/**" -ASSET_MODELS_DIR="$ASSETS_DIR/models-src/3d/falukant" -if [ ! -d "$ASSET_MODELS_DIR/characters" ]; then - # Keep deployments available while the dedicated LFS repository is being - # populated. The active slot has already been built with these sources, - # so it is a safe, read-only fallback for the new inactive slot. - FALLBACK_MODELS_DIR="$CURRENT_FRONTEND/models-src/3d/falukant" - if [ -d "$FALLBACK_MODELS_DIR/characters" ]; then - echo "WARN: Falukant models are missing from ${FALUKANT_MODELS_REPOSITORY}@${FALUKANT_MODELS_REF}; nutze Modelle aus dem aktiven Slot." >&2 - ASSET_MODELS_DIR="$FALLBACK_MODELS_DIR" - else - echo "ERROR: Falukant models are missing from ${FALUKANT_MODELS_REPOSITORY}@${FALUKANT_MODELS_REF} and from the active slot" >&2 - exit 1 - fi +ASSET_RUNTIME_MODELS_DIR="$ASSETS_DIR/public/models/3d/falukant" +if [ ! -f "$ASSET_RUNTIME_MODELS_DIR/characters/female_opt.glb" ] || [ ! -f "$ASSET_RUNTIME_MODELS_DIR/houses/under_bridge_opt.glb" ]; then + echo "ERROR: Optimized Falukant runtime models are missing from ${FALUKANT_MODELS_REPOSITORY}@${FALUKANT_MODELS_REF}. Run: npm --prefix frontend run build-runtime-models -- --assets-dir ." >&2 + exit 1 fi -echo "Übernehme Falukant-3D-Quellmodelle..." -rm -rf "$FRONTEND_DIR/models-src/3d/falukant" -mkdir -p "$FRONTEND_DIR/models-src/3d" -cp -a "$ASSET_MODELS_DIR" "$FRONTEND_DIR/models-src/3d/" - -echo "Optimiere Falukant-3D-Modelle..." -npm run optimize-models +echo "Übernehme voroptimierte Falukant-3D-Modelle..." +rm -rf "$FRONTEND_DIR/public/models/3d/falukant" +mkdir -p "$FRONTEND_DIR/public/models/3d" +cp -a "$ASSET_RUNTIME_MODELS_DIR" "$FRONTEND_DIR/public/models/3d/" echo "Baue Frontend..." npm run build