Browse Source

refactor: remove unused Vite configuration files and update documentation

Remove obsolete Vite configuration files that are no longer used by the build system
and update BUILDING.md to accurately reflect the current configuration structure.

**Removed Files:**
- vite.config.ts (47 lines) - Legacy configuration file
- vite.config.mts (70 lines) - Unused "main" configuration file

**Updated Documentation:**
- BUILDING.md - Corrected Vite configuration section to show actual usage

**Current Configuration Structure:**
- vite.config.web.mts - Used by build-web.sh
- vite.config.electron.mts - Used by build-electron.sh
- vite.config.capacitor.mts - Used by npm run build:capacitor
- vite.config.common.mts - Shared configuration utilities
- vite.config.utils.mts - Configuration utility functions

**Benefits:**
- Eliminates confusion about which config files to use
- Removes 117 lines of unused configuration code
- Documentation now matches actual build system usage
- Cleaner, more maintainable configuration structure

**Impact:**
- No functional changes to build process
- All platform builds continue to work correctly
- Reduced configuration complexity and maintenance overhead
pull/142/head
Matthew Raymer 1 week ago
parent
commit
e1b5367880
  1. 7
      BUILDING.md
  2. 662
      docs/lazy-loading-patterns.md
  3. 1
      docs/migration-assessment-2025-07-16.md
  4. 554
      src/components/sub-components/HeavyComponent.vue
  5. 721
      src/components/sub-components/QRScannerComponent.vue
  6. 667
      src/components/sub-components/ThreeJSViewer.vue
  7. 70
      vite.config.mts
  8. 47
      vite.config.ts

7
BUILDING.md

@ -2030,8 +2030,11 @@ share_target: {
### B.6 Additional Vite Configurations ### B.6 Additional Vite Configurations
**vite.config.ts**: Legacy configuration file (minimal) **vite.config.web.mts**: Web platform configuration (used by build-web.sh)
**vite.config.mts**: Main configuration entry point **vite.config.electron.mts**: Electron platform configuration (used by build-electron.sh)
**vite.config.capacitor.mts**: Capacitor mobile configuration (used by npm run build:capacitor)
**vite.config.common.mts**: Shared configuration utilities
**vite.config.utils.mts**: Configuration utility functions
**vite.config.dev.mts**: Development-specific configuration **vite.config.dev.mts**: Development-specific configuration
**vite.config.optimized.mts**: Optimized build configuration **vite.config.optimized.mts**: Optimized build configuration

662
docs/lazy-loading-patterns.md

@ -1,662 +0,0 @@
# Vue 3 + Vite + vue-facing-decorator: Lazy Loading Patterns & Best Practices
## Overview
This document provides comprehensive guidance on implementing lazy loading and code splitting in Vue 3 applications using Vite and `vue-facing-decorator`. The patterns demonstrated here optimize bundle size, improve initial load times, and enhance user experience through progressive loading.
**Author:** Matthew Raymer
**Version:** 1.0.0
**Last Updated:** 2024
## Table of Contents
1. [Why Lazy Loading Matters](#why-lazy-loading-matters)
2. [Vite Configuration for Optimal Code Splitting](#vite-configuration-for-optimal-code-splitting)
3. [Lazy Loading Patterns](#lazy-loading-patterns)
4. [Component-Level Lazy Loading](#component-level-lazy-loading)
5. [Route-Level Lazy Loading](#route-level-lazy-loading)
6. [Library-Level Lazy Loading](#library-level-lazy-loading)
7. [Performance Monitoring](#performance-monitoring)
8. [Best Practices](#best-practices)
9. [Common Pitfalls](#common-pitfalls)
10. [Examples](#examples)
## Why Lazy Loading Matters
### Performance Benefits
- **Faster Initial Load**: Only load code needed for the current view
- **Reduced Bundle Size**: Split large applications into smaller chunks
- **Better Caching**: Independent chunks can be cached separately
- **Improved User Experience**: Progressive loading with loading states
### When to Use Lazy Loading
✅ **Good Candidates for Lazy Loading:**
- Heavy components (data processing, 3D rendering)
- Feature-specific components (QR scanner, file uploader)
- Route-based components
- Large third-party libraries (ThreeJS, Chart.js)
- Components with conditional rendering
❌ **Avoid Lazy Loading:**
- Core UI components used everywhere
- Small utility components
- Components needed for initial render
- Components with frequent usage patterns
## Vite Configuration for Optimal Code Splitting
### Enhanced Build Configuration
```typescript
// vite.config.optimized.mts
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> {
return {
build: {
rollupOptions: {
output: {
// Enhanced manual chunks for better code splitting
manualChunks: {
// Vendor chunks for better caching
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'],
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode'],
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'],
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'],
'three-vendor': ['three', '@tweenjs/tween.js'],
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'],
// Platform-specific chunks
...(isCapacitor && {
'capacitor-vendor': ['@capacitor/core', '@capacitor/app']
})
}
}
}
}
};
}
```
### Key Configuration Features
1. **Manual Chunks**: Group related dependencies for better caching
2. **Platform-Specific Chunks**: Separate native and web dependencies
3. **Vendor Separation**: Keep third-party libraries separate from app code
4. **Dynamic Imports**: Enable automatic code splitting for dynamic imports
## Lazy Loading Patterns
### 1. Component-Level Lazy Loading
#### Basic Pattern with defineAsyncComponent
```typescript
import { Component, Vue } from 'vue-facing-decorator';
import { defineAsyncComponent } from 'vue';
@Component({
name: 'LazyLoadingExample',
components: {
LazyHeavyComponent: defineAsyncComponent({
loader: () => import('./sub-components/HeavyComponent.vue'),
loadingComponent: {
template: '<div class="loading">Loading...</div>'
},
errorComponent: {
template: '<div class="error">Failed to load</div>'
},
delay: 200,
timeout: 10000
})
}
})
export default class LazyLoadingExample extends Vue {
// Component logic
}
```
#### Advanced Pattern with Suspense
```vue
<template>
<Suspense>
<template #default>
<LazyHeavyComponent v-if="showComponent" />
</template>
<template #fallback>
<div class="loading-fallback">
<div class="spinner"></div>
<p>Loading component...</p>
</div>
</template>
</Suspense>
</template>
```
### 2. Conditional Loading
```typescript
@Component({
name: 'ConditionalLazyLoading'
})
export default class ConditionalLazyLoading extends Vue {
showHeavyComponent = false;
showQRScanner = false;
// Lazy load based on user interaction
async toggleHeavyComponent(): Promise<void> {
this.showHeavyComponent = !this.showHeavyComponent;
if (this.showHeavyComponent) {
// Preload component for better UX
await this.preloadComponent(() => import('./HeavyComponent.vue'));
}
}
private async preloadComponent(loader: () => Promise<any>): Promise<void> {
try {
await loader();
} catch (error) {
console.warn('Preload failed:', error);
}
}
}
```
### 3. Library-Level Lazy Loading
```typescript
@Component({
name: 'LibraryLazyLoading'
})
export default class LibraryLazyLoading extends Vue {
private threeJS: any = null;
async loadThreeJS(): Promise<void> {
if (!this.threeJS) {
// Lazy load ThreeJS only when needed
this.threeJS = await import('three');
console.log('ThreeJS loaded successfully');
}
}
async initialize3DScene(): Promise<void> {
await this.loadThreeJS();
// Use ThreeJS after loading
const scene = new this.threeJS.Scene();
// ... rest of 3D setup
}
}
```
## Component-Level Lazy Loading
### Heavy Data Processing Component
```typescript
// HeavyComponent.vue
@Component({
name: 'HeavyComponent'
})
export default class HeavyComponent extends Vue {
@Prop({ required: true }) readonly data!: any;
async processData(): Promise<void> {
// Process data in batches to avoid blocking UI
const batchSize = 10;
const items = this.data.items;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await this.processBatch(batch);
// Allow UI to update
await this.$nextTick();
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
```
### Camera-Dependent Component
```typescript
// QRScannerComponent.vue
@Component({
name: 'QRScannerComponent'
})
export default class QRScannerComponent extends Vue {
async mounted(): Promise<void> {
// Initialize camera only when component is mounted
await this.initializeCamera();
}
async initializeCamera(): Promise<void> {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
this.cameras = devices.filter(device => device.kind === 'videoinput');
this.hasCamera = this.cameras.length > 0;
} catch (error) {
console.error('Camera initialization failed:', error);
this.hasCamera = false;
}
}
}
```
## Route-Level Lazy Loading
### Vue Router Configuration
```typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue')
},
{
path: '/heavy-feature',
name: 'HeavyFeature',
component: () => import('@/views/HeavyFeatureView.vue'),
// Preload on hover for better UX
beforeEnter: (to, from, next) => {
if (from.name) {
// Preload component when navigating from another route
import('@/views/HeavyFeatureView.vue');
}
next();
}
}
];
export default createRouter({
history: createWebHistory(),
routes
});
```
### Route Guards with Lazy Loading
```typescript
// router/guards.ts
export async function lazyLoadGuard(to: any, from: any, next: any): Promise<void> {
if (to.meta.requiresHeavyFeature) {
try {
// Preload heavy feature before navigation
await import('@/components/HeavyFeature.vue');
next();
} catch (error) {
console.error('Failed to load heavy feature:', error);
next('/error');
}
} else {
next();
}
}
```
## Library-Level Lazy Loading
### Dynamic Library Loading
```typescript
@Component({
name: 'DynamicLibraryLoader'
})
export default class DynamicLibraryLoader extends Vue {
private libraries: Map<string, any> = new Map();
async loadLibrary(name: string): Promise<any> {
if (this.libraries.has(name)) {
return this.libraries.get(name);
}
let library: any;
switch (name) {
case 'three':
library = await import('three');
break;
case 'chart':
library = await import('chart.js');
break;
case 'qr':
library = await import('jsqr');
break;
default:
throw new Error(`Unknown library: ${name}`);
}
this.libraries.set(name, library);
return library;
}
}
```
### Conditional Library Loading
```typescript
@Component({
name: 'ConditionalLibraryLoader'
})
export default class ConditionalLibraryLoader extends Vue {
async loadPlatformSpecificLibrary(): Promise<void> {
if (process.env.VITE_PLATFORM === 'capacitor') {
// Load Capacitor-specific libraries
await import('@capacitor/camera');
await import('@capacitor/filesystem');
} else {
// Load web-specific libraries
await import('file-saver');
await import('jszip');
}
}
}
```
## Performance Monitoring
### Bundle Analysis
```bash
# Analyze bundle size
npm run build
npx vite-bundle-analyzer dist
# Monitor chunk sizes
npx vite-bundle-analyzer dist --mode=treemap
```
### Runtime Performance Monitoring
```typescript
@Component({
name: 'PerformanceMonitor'
})
export default class PerformanceMonitor extends Vue {
private performanceMetrics = {
componentLoadTime: 0,
renderTime: 0,
memoryUsage: 0
};
private measureComponentLoad(componentName: string): void {
const startTime = performance.now();
return () => {
const loadTime = performance.now() - startTime;
this.performanceMetrics.componentLoadTime = loadTime;
console.log(`${componentName} loaded in ${loadTime.toFixed(2)}ms`);
// Send to analytics
this.trackPerformance(componentName, loadTime);
};
}
private trackPerformance(component: string, loadTime: number): void {
// Send to analytics service
if (window.gtag) {
window.gtag('event', 'component_load', {
component_name: component,
load_time: loadTime
});
}
}
}
```
## Best Practices
### 1. Loading States
Always provide meaningful loading states:
```vue
<template>
<Suspense>
<template #default>
<LazyComponent />
</template>
<template #fallback>
<div class="loading-state">
<div class="spinner"></div>
<p>Loading {{ componentName }}...</p>
<p class="loading-tip">{{ loadingTip }}</p>
</div>
</template>
</Suspense>
</template>
```
### 2. Error Handling
Implement comprehensive error handling:
```typescript
const LazyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
errorComponent: {
template: `
<div class="error-state">
<h3>Failed to load component</h3>
<p>{{ error.message }}</p>
<button @click="retry">Retry</button>
</div>
`,
props: ['error'],
methods: {
retry() {
this.$emit('retry');
}
}
},
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
console.warn(`Retrying component load (attempt ${attempts})`);
retry();
} else {
console.error('Component failed to load after 3 attempts');
fail();
}
}
});
```
### 3. Preloading Strategy
Implement intelligent preloading:
```typescript
@Component({
name: 'SmartPreloader'
})
export default class SmartPreloader extends Vue {
private preloadQueue: Array<() => Promise<any>> = [];
private isPreloading = false;
// Preload based on user behavior
onUserHover(componentLoader: () => Promise<any>): void {
this.preloadQueue.push(componentLoader);
this.processPreloadQueue();
}
private async processPreloadQueue(): Promise<void> {
if (this.isPreloading || this.preloadQueue.length === 0) return;
this.isPreloading = true;
while (this.preloadQueue.length > 0) {
const loader = this.preloadQueue.shift();
if (loader) {
try {
await loader();
} catch (error) {
console.warn('Preload failed:', error);
}
}
// Small delay between preloads
await new Promise(resolve => setTimeout(resolve, 100));
}
this.isPreloading = false;
}
}
```
### 4. Bundle Optimization
Optimize bundle splitting:
```typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
// Group by feature
if (id.includes('qr')) return 'qr-feature';
if (id.includes('three')) return '3d-feature';
if (id.includes('chart')) return 'chart-feature';
// Group by vendor
if (id.includes('node_modules')) {
if (id.includes('vue')) return 'vue-vendor';
if (id.includes('lodash')) return 'utils-vendor';
return 'vendor';
}
}
}
}
}
});
```
## Common Pitfalls
### 1. Over-Lazy Loading
❌ **Don't lazy load everything:**
```typescript
// Bad: Lazy loading small components
const SmallButton = defineAsyncComponent(() => import('./SmallButton.vue'));
```
✅ **Do lazy load strategically:**
```typescript
// Good: Lazy load heavy components only
const HeavyDataProcessor = defineAsyncComponent(() => import('./HeavyDataProcessor.vue'));
```
### 2. Missing Loading States
❌ **Don't leave users hanging:**
```vue
<template>
<LazyComponent v-if="show" />
</template>
```
✅ **Do provide loading feedback:**
```vue
<template>
<Suspense>
<template #default>
<LazyComponent v-if="show" />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
```
### 3. Ignoring Error States
❌ **Don't ignore loading failures:**
```typescript
const LazyComponent = defineAsyncComponent(() => import('./Component.vue'));
```
✅ **Do handle errors gracefully:**
```typescript
const LazyComponent = defineAsyncComponent({
loader: () => import('./Component.vue'),
errorComponent: ErrorFallback,
onError: (error, retry, fail) => {
console.error('Component load failed:', error);
// Retry once, then fail
retry();
}
});
```
## Examples
### Complete Lazy Loading Example
See the following files for complete working examples:
1. **`src/components/LazyLoadingExample.vue`** - Main example component
2. **`src/components/sub-components/HeavyComponent.vue`** - Data processing component
3. **`src/components/sub-components/QRScannerComponent.vue`** - Camera-dependent component
4. **`src/components/sub-components/ThreeJSViewer.vue`** - 3D rendering component
5. **`vite.config.optimized.mts`** - Optimized Vite configuration
### Usage Example
```vue
<template>
<div class="app">
<h1>Lazy Loading Demo</h1>
<LazyLoadingExample
:initial-load-heavy="false"
@qr-detected="handleQRCode"
@model-loaded="handleModelLoaded"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator';
import LazyLoadingExample from '@/components/LazyLoadingExample.vue';
@Component({
name: 'App',
components: {
LazyLoadingExample
}
})
export default class App extends Vue {
handleQRCode(data: string): void {
console.log('QR code detected:', data);
}
handleModelLoaded(info: any): void {
console.log('3D model loaded:', info);
}
}
</script>
```
## Conclusion
Lazy loading with Vue 3 + Vite + `vue-facing-decorator` provides powerful tools for optimizing application performance. By implementing these patterns strategically, you can significantly improve initial load times while maintaining excellent user experience.
Remember to:
- Use lazy loading for heavy components and features
- Provide meaningful loading and error states
- Monitor performance and bundle sizes
- Implement intelligent preloading strategies
- Handle errors gracefully
For more information, see the Vue 3 documentation on [Async Components](https://vuejs.org/guide/components/async.html) and the Vite documentation on [Code Splitting](https://vitejs.dev/guide/features.html#code-splitting).

1
docs/migration-assessment-2025-07-16.md

@ -60,7 +60,6 @@ These components are static UI elements, help pages, or simple components that d
- **Help pages**: `HelpNotificationTypesView.vue`, `HelpOnboardingView.vue` - **Help pages**: `HelpNotificationTypesView.vue`, `HelpOnboardingView.vue`
- **Static views**: `StatisticsView.vue`, `QuickActionBvcView.vue` - **Static views**: `StatisticsView.vue`, `QuickActionBvcView.vue`
- **UI components**: `ChoiceButtonDialog.vue`, `EntitySummaryButton.vue` - **UI components**: `ChoiceButtonDialog.vue`, `EntitySummaryButton.vue`
- **Sub-components**: `HeavyComponent.vue`, `QRScannerComponent.vue`, `ThreeJSViewer.vue`
- **Utility components**: `PWAInstallPrompt.vue`, `HiddenDidDialog.vue` - **Utility components**: `PWAInstallPrompt.vue`, `HiddenDidDialog.vue`
#### 🔄 **Remaining Legacy Patterns (4 files)** #### 🔄 **Remaining Legacy Patterns (4 files)**

554
src/components/sub-components/HeavyComponent.vue

@ -1,554 +0,0 @@
<template>
<div class="heavy-component">
<h2>Heavy Data Processing Component</h2>
<!-- Data processing controls -->
<div class="controls">
<button :disabled="isProcessing" @click="processData">
{{ isProcessing ? "Processing..." : "Process Data" }}
</button>
<button :disabled="isProcessing" @click="clearResults">
Clear Results
</button>
</div>
<!-- Processing status -->
<div v-if="isProcessing" class="processing-status">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
<p>Processing {{ processedCount }} of {{ totalItems }} items...</p>
</div>
<!-- Results display -->
<div v-if="processedData.length > 0" class="results">
<h3>Processed Results ({{ processedData.length }} items)</h3>
<!-- Filter controls -->
<div class="filters">
<input
v-model="searchTerm"
placeholder="Search items..."
class="search-input"
/>
<select v-model="sortBy" class="sort-select">
<option value="name">Sort by Name</option>
<option value="id">Sort by ID</option>
<option value="processed">Sort by Processed Date</option>
</select>
</div>
<!-- Results list -->
<div class="results-list">
<div
v-for="item in filteredAndSortedData"
:key="item.id"
class="result-item"
>
<div class="item-header">
<span class="item-name">{{ item.name }}</span>
<span class="item-id">#{{ item.id }}</span>
</div>
<div class="item-details">
<span class="processed-date">
Processed: {{ formatDate(item.processedAt) }}
</span>
<span class="processing-time">
Time: {{ item.processingTime }}ms
</span>
</div>
</div>
</div>
<!-- Pagination -->
<div v-if="totalPages > 1" class="pagination">
<button
:disabled="currentPage === 1"
class="page-btn"
@click="previousPage"
>
Previous
</button>
<span class="page-info">
Page {{ currentPage }} of {{ totalPages }}
</span>
<button
:disabled="currentPage === totalPages"
class="page-btn"
@click="nextPage"
>
Next
</button>
</div>
</div>
<!-- Performance metrics -->
<div v-if="performanceMetrics" class="performance-metrics">
<h4>Performance Metrics</h4>
<div class="metrics-grid">
<div class="metric">
<span class="metric-label">Total Processing Time:</span>
<span class="metric-value">{{ performanceMetrics.totalTime }}ms</span>
</div>
<div class="metric">
<span class="metric-label">Average per Item:</span>
<span class="metric-value"
>{{ performanceMetrics.averageTime }}ms</span
>
</div>
<div class="metric">
<span class="metric-label">Memory Usage:</span>
<span class="metric-value"
>{{ performanceMetrics.memoryUsage }}MB</span
>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
interface ProcessedItem {
id: number;
name: string;
processedAt: Date;
processingTime: number;
result: any;
}
interface PerformanceMetrics {
totalTime: number;
averageTime: number;
memoryUsage: number;
}
/**
* Heavy Component for Data Processing
*
* Demonstrates a component that performs intensive data processing
* and would benefit from lazy loading to avoid blocking the main thread.
*
* @author Matthew Raymer
* @version 1.0.0
*/
@Component({
name: "HeavyComponent",
})
export default class HeavyComponent extends Vue {
@Prop({ required: true }) readonly data!: {
items: Array<{ id: number; name: string }>;
filters: Record<string, any>;
sortBy: string;
};
// Component state
isProcessing = false;
processedData: ProcessedItem[] = [];
progress = 0;
processedCount = 0;
totalItems = 0;
// UI state
searchTerm = "";
sortBy = "name";
currentPage = 1;
itemsPerPage = 50;
// Performance tracking
performanceMetrics: PerformanceMetrics | null = null;
startTime = 0;
// Computed properties
get filteredAndSortedData(): ProcessedItem[] {
let filtered = this.processedData;
// Apply search filter
if (this.searchTerm) {
filtered = filtered.filter((item) =>
item.name.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
}
// Apply sorting
filtered.sort((a, b) => {
switch (this.sortBy) {
case "name":
return a.name.localeCompare(b.name);
case "id":
return a.id - b.id;
case "processed":
return b.processedAt.getTime() - a.processedAt.getTime();
default:
return 0;
}
});
return filtered;
}
get paginatedData(): ProcessedItem[] {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return this.filteredAndSortedData.slice(start, end);
}
get totalPages(): number {
return Math.ceil(this.filteredAndSortedData.length / this.itemsPerPage);
}
// Lifecycle hooks
mounted(): void {
console.log(
"[HeavyComponent] Component mounted with",
this.data.items.length,
"items",
);
this.totalItems = this.data.items.length;
}
// Methods
async processData(): Promise<void> {
if (this.isProcessing) return;
this.isProcessing = true;
this.progress = 0;
this.processedCount = 0;
this.processedData = [];
this.startTime = performance.now();
console.log("[HeavyComponent] Starting data processing...");
try {
// Process items in batches to avoid blocking the UI
const batchSize = 10;
const items = this.data.items;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
// Process batch
await this.processBatch(batch);
// Update progress
this.processedCount = Math.min(i + batchSize, items.length);
this.progress = (this.processedCount / items.length) * 100;
// Allow UI to update
await this.$nextTick();
// Small delay to prevent overwhelming the UI
await new Promise((resolve) => setTimeout(resolve, 10));
}
// Calculate performance metrics
this.calculatePerformanceMetrics();
// Emit completion event
this.$emit("data-processed", {
totalItems: this.processedData.length,
processingTime: performance.now() - this.startTime,
metrics: this.performanceMetrics,
});
console.log("[HeavyComponent] Data processing completed");
} catch (error) {
console.error("[HeavyComponent] Processing error:", error);
this.$emit("processing-error", error);
} finally {
this.isProcessing = false;
}
}
private async processBatch(
batch: Array<{ id: number; name: string }>,
): Promise<void> {
const processedBatch = await Promise.all(
batch.map(async (item) => {
const itemStartTime = performance.now();
// Simulate heavy processing
await this.simulateHeavyProcessing(item);
const processingTime = performance.now() - itemStartTime;
return {
id: item.id,
name: item.name,
processedAt: new Date(),
processingTime: Math.round(processingTime),
result: this.generateResult(item),
};
}),
);
this.processedData.push(...processedBatch);
}
private async simulateHeavyProcessing(item: {
id: number;
name: string;
}): Promise<void> {
// Simulate CPU-intensive work
const complexity = item.name.length * item.id;
const iterations = Math.min(complexity, 1000); // Cap at 1000 iterations
for (let i = 0; i < iterations; i++) {
// Simulate work
Math.sqrt(i) * Math.random();
}
// Simulate async work
await new Promise((resolve) => setTimeout(resolve, Math.random() * 10));
}
private generateResult(item: { id: number; name: string }): any {
return {
hash: this.generateHash(item.name + item.id),
category: this.categorizeItem(item),
score: Math.random() * 100,
tags: this.generateTags(item),
};
}
private generateHash(input: string): string {
let hash = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(16);
}
private categorizeItem(item: { id: number; name: string }): string {
const categories = ["A", "B", "C", "D", "E"];
return categories[item.id % categories.length];
}
private generateTags(item: { id: number; name: string }): string[] {
const tags = ["important", "urgent", "review", "archive", "featured"];
return tags.filter((_, index) => (item.id + index) % 3 === 0);
}
private calculatePerformanceMetrics(): void {
const totalTime = performance.now() - this.startTime;
const averageTime = totalTime / this.processedData.length;
// Simulate memory usage calculation
const memoryUsage = this.processedData.length * 0.1; // 0.1MB per item
this.performanceMetrics = {
totalTime: Math.round(totalTime),
averageTime: Math.round(averageTime),
memoryUsage: Math.round(memoryUsage * 100) / 100,
};
}
clearResults(): void {
this.processedData = [];
this.performanceMetrics = null;
this.searchTerm = "";
this.currentPage = 1;
console.log("[HeavyComponent] Results cleared");
}
previousPage(): void {
if (this.currentPage > 1) {
this.currentPage--;
}
}
nextPage(): void {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
}
formatDate(date: Date): string {
return date.toLocaleString();
}
// Event emitters
@Emit("data-processed")
emitDataProcessed(data: any): any {
return data;
}
@Emit("processing-error")
emitProcessingError(error: Error): Error {
return error;
}
}
</script>
<style scoped>
.heavy-component {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.controls button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: background-color 0.2s;
}
.controls button:hover:not(:disabled) {
background: #e9ecef;
}
.controls button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.processing-status {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 20px;
background: #e9ecef;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
}
.results {
margin-top: 20px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-input,
.sort-select {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.search-input {
flex: 1;
}
.results-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
}
.result-item {
padding: 12px;
border-bottom: 1px solid #eee;
}
.result-item:last-child {
border-bottom: none;
}
.item-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.item-name {
font-weight: bold;
}
.item-id {
color: #666;
font-size: 0.9em;
}
.item-details {
display: flex;
gap: 20px;
font-size: 0.85em;
color: #666;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 20px;
}
.page-btn {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
font-size: 0.9em;
color: #666;
}
.performance-metrics {
margin-top: 20px;
padding: 15px;
background: #e8f4fd;
border-radius: 4px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 10px;
}
.metric {
display: flex;
justify-content: space-between;
padding: 8px;
background: #fff;
border-radius: 4px;
}
.metric-label {
font-weight: bold;
color: #333;
}
.metric-value {
color: #007bff;
font-weight: bold;
}
</style>

721
src/components/sub-components/QRScannerComponent.vue

@ -1,721 +0,0 @@
<template>
<div class="qr-scanner-component">
<h2>QR Code Scanner</h2>
<!-- Camera controls -->
<div class="camera-controls">
<button :disabled="isScanning || !hasCamera" @click="startScanning">
{{ isScanning ? "Scanning..." : "Start Scanning" }}
</button>
<button :disabled="!isScanning" @click="stopScanning">
Stop Scanning
</button>
<button
:disabled="!isScanning || cameras.length <= 1"
@click="switchCamera"
>
Switch Camera
</button>
</div>
<!-- Camera status -->
<div class="camera-status">
<div v-if="!hasCamera" class="status-error">
<p>Camera not available</p>
<p class="status-detail">
This device doesn't have a camera or camera access is denied.
</p>
</div>
<div v-else-if="!isScanning" class="status-info">
<p>Camera ready</p>
<p class="status-detail">
Click "Start Scanning" to begin QR code detection.
</p>
</div>
<div v-else class="status-scanning">
<p>Scanning for QR codes...</p>
<p class="status-detail">Point camera at a QR code to scan.</p>
</div>
</div>
<!-- Camera view -->
<div v-if="isScanning && hasCamera" class="camera-container">
<video
ref="videoElement"
class="camera-video"
autoplay
playsinline
muted
></video>
<!-- Scanning overlay -->
<div class="scanning-overlay">
<div class="scan-frame"></div>
<div class="scan-line"></div>
</div>
</div>
<!-- Scan results -->
<div v-if="scanResults.length > 0" class="scan-results">
<h3>Scan Results ({{ scanResults.length }})</h3>
<div class="results-list">
<div
v-for="(result, index) in scanResults"
:key="index"
class="result-item"
>
<div class="result-header">
<span class="result-number">#{{ index + 1 }}</span>
<span class="result-time">{{ formatTime(result.timestamp) }}</span>
</div>
<div class="result-content">
<div class="qr-data"><strong>Data:</strong> {{ result.data }}</div>
<div class="qr-format">
<strong>Format:</strong> {{ result.format }}
</div>
</div>
<div class="result-actions">
<button class="copy-btn" @click="copyToClipboard(result.data)">
Copy
</button>
<button class="remove-btn" @click="removeResult(index)">
Remove
</button>
</div>
</div>
</div>
<div class="results-actions">
<button class="clear-btn" @click="clearResults">
Clear All Results
</button>
<button class="export-btn" @click="exportResults">
Export Results
</button>
</div>
</div>
<!-- Settings panel -->
<div class="settings-panel">
<h3>Scanner Settings</h3>
<div class="setting-group">
<label>
<input v-model="settings.continuousScanning" type="checkbox" />
Continuous Scanning
</label>
<p class="setting-description">
Automatically scan multiple QR codes without stopping
</p>
</div>
<div class="setting-group">
<label>
<input v-model="settings.audioFeedback" type="checkbox" />
Audio Feedback
</label>
<p class="setting-description">Play sound when QR code is detected</p>
</div>
<div class="setting-group">
<label>
<input v-model="settings.vibrateOnScan" type="checkbox" />
Vibration Feedback
</label>
<p class="setting-description">
Vibrate device when QR code is detected
</p>
</div>
<div class="setting-group">
<label>Scan Interval (ms):</label>
<input
v-model.number="settings.scanInterval"
type="number"
min="100"
max="5000"
step="100"
/>
<p class="setting-description">
Time between scans (lower = faster, higher = more accurate)
</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Emit } from "vue-facing-decorator";
interface ScanResult {
data: string;
format: string;
timestamp: Date;
}
interface ScannerSettings {
continuousScanning: boolean;
audioFeedback: boolean;
vibrateOnScan: boolean;
scanInterval: number;
}
/**
* QR Scanner Component
*
* Demonstrates lazy loading for camera-dependent features.
* This component would benefit from lazy loading as it requires
* camera permissions and heavy camera processing libraries.
*
* @author Matthew Raymer
* @version 1.0.0
*/
@Component({
name: "QRScannerComponent",
})
export default class QRScannerComponent extends Vue {
// Component state
isScanning = false;
hasCamera = false;
cameras: MediaDeviceInfo[] = [];
currentCameraIndex = 0;
// Video element reference
videoElement: HTMLVideoElement | null = null;
// Scan results
scanResults: ScanResult[] = [];
// Scanner settings
settings: ScannerSettings = {
continuousScanning: true,
audioFeedback: true,
vibrateOnScan: true,
scanInterval: 500,
};
// Internal state
private stream: MediaStream | null = null;
private scanInterval: number | null = null;
private lastScanTime = 0;
// Lifecycle hooks
async mounted(): Promise<void> {
console.log("[QRScannerComponent] Component mounted");
await this.initializeCamera();
}
beforeUnmount(): void {
this.stopScanning();
console.log("[QRScannerComponent] Component unmounting");
}
// Methods
async initializeCamera(): Promise<void> {
try {
// Check if camera is available
const devices = await navigator.mediaDevices.enumerateDevices();
this.cameras = devices.filter((device) => device.kind === "videoinput");
this.hasCamera = this.cameras.length > 0;
if (this.hasCamera) {
console.log(
"[QRScannerComponent] Camera available:",
this.cameras.length,
"devices",
);
} else {
console.warn("[QRScannerComponent] No camera devices found");
}
} catch (error) {
console.error("[QRScannerComponent] Camera initialization error:", error);
this.hasCamera = false;
}
}
async startScanning(): Promise<void> {
if (!this.hasCamera || this.isScanning) return;
try {
console.log("[QRScannerComponent] Starting QR scanning...");
// Get camera stream
const constraints = {
video: {
deviceId: this.cameras[this.currentCameraIndex]?.deviceId,
},
};
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
// Set up video element
this.videoElement = this.$refs.videoElement as HTMLVideoElement;
if (this.videoElement) {
this.videoElement.srcObject = this.stream;
await this.videoElement.play();
}
this.isScanning = true;
// Start QR code detection
this.startQRDetection();
console.log("[QRScannerComponent] QR scanning started");
} catch (error) {
console.error("[QRScannerComponent] Failed to start scanning:", error);
this.hasCamera = false;
}
}
stopScanning(): void {
if (!this.isScanning) return;
console.log("[QRScannerComponent] Stopping QR scanning...");
// Stop QR detection
this.stopQRDetection();
// Stop camera stream
if (this.stream) {
this.stream.getTracks().forEach((track) => track.stop());
this.stream = null;
}
// Clear video element
if (this.videoElement) {
this.videoElement.srcObject = null;
this.videoElement = null;
}
this.isScanning = false;
console.log("[QRScannerComponent] QR scanning stopped");
}
async switchCamera(): Promise<void> {
if (this.cameras.length <= 1) return;
// Stop current scanning
this.stopScanning();
// Switch to next camera
this.currentCameraIndex =
(this.currentCameraIndex + 1) % this.cameras.length;
// Restart scanning with new camera
await this.startScanning();
console.log(
"[QRScannerComponent] Switched to camera:",
this.currentCameraIndex,
);
}
private startQRDetection(): void {
if (!this.settings.continuousScanning) return;
this.scanInterval = window.setInterval(() => {
this.detectQRCode();
}, this.settings.scanInterval);
}
private stopQRDetection(): void {
if (this.scanInterval) {
clearInterval(this.scanInterval);
this.scanInterval = null;
}
}
private async detectQRCode(): Promise<void> {
if (!this.videoElement || !this.isScanning) return;
const now = Date.now();
if (now - this.lastScanTime < this.settings.scanInterval) return;
try {
// Simulate QR code detection
// In a real implementation, you would use a QR code library like jsQR
const detectedQR = await this.simulateQRDetection();
if (detectedQR) {
this.addScanResult(detectedQR);
this.lastScanTime = now;
}
} catch (error) {
console.error("[QRScannerComponent] QR detection error:", error);
}
}
private async simulateQRDetection(): Promise<ScanResult | null> {
// Simulate QR code detection with random chance
if (Math.random() < 0.1) {
// 10% chance of detection
const sampleData = [
"https://example.com/qr1",
"WIFI:S:MyNetwork;T:WPA;P:password123;;",
"BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nTEL:+1234567890\nEND:VCARD",
"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
];
const formats = ["URL", "WiFi", "vCard", "TOTP"];
const randomIndex = Math.floor(Math.random() * sampleData.length);
return {
data: sampleData[randomIndex],
format: formats[randomIndex],
timestamp: new Date(),
};
}
return null;
}
private addScanResult(result: ScanResult): void {
// Check for duplicates
const isDuplicate = this.scanResults.some(
(existing) => existing.data === result.data,
);
if (!isDuplicate) {
this.scanResults.unshift(result);
// Provide feedback
this.provideFeedback();
// Emit event
this.$emit("qr-detected", result.data);
console.log("[QRScannerComponent] QR code detected:", result.data);
}
}
private provideFeedback(): void {
// Audio feedback
if (this.settings.audioFeedback) {
this.playBeepSound();
}
// Vibration feedback
if (this.settings.vibrateOnScan && "vibrate" in navigator) {
navigator.vibrate(100);
}
}
private playBeepSound(): void {
// Create a simple beep sound
const audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.type = "sine";
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01,
audioContext.currentTime + 0.1,
);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
}
copyToClipboard(text: string): void {
navigator.clipboard
.writeText(text)
.then(() => {
console.log("[QRScannerComponent] Copied to clipboard:", text);
})
.catch((error) => {
console.error("[QRScannerComponent] Failed to copy:", error);
});
}
removeResult(index: number): void {
this.scanResults.splice(index, 1);
}
clearResults(): void {
this.scanResults = [];
console.log("[QRScannerComponent] Results cleared");
}
exportResults(): void {
const data = JSON.stringify(this.scanResults, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `qr-scan-results-${new Date().toISOString().split("T")[0]}.json`;
a.click();
URL.revokeObjectURL(url);
console.log("[QRScannerComponent] Results exported");
}
formatTime(date: Date): string {
return date.toLocaleTimeString();
}
// Event emitters
@Emit("qr-detected")
emitQRDetected(data: string): string {
return data;
}
}
</script>
<style scoped>
.qr-scanner-component {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.camera-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.camera-controls button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: background-color 0.2s;
}
.camera-controls button:hover:not(:disabled) {
background: #e9ecef;
}
.camera-controls button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.camera-status {
margin-bottom: 20px;
padding: 15px;
border-radius: 4px;
}
.status-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.status-info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.status-scanning {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.status-detail {
margin-top: 5px;
font-size: 0.9em;
opacity: 0.8;
}
.camera-container {
position: relative;
width: 100%;
max-width: 400px;
height: 300px;
margin: 20px auto;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.camera-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.scan-frame {
width: 200px;
height: 200px;
border: 2px solid #00ff00;
border-radius: 8px;
position: relative;
}
.scan-line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: #00ff00;
animation: scan 2s linear infinite;
}
@keyframes scan {
0% {
top: 0;
}
100% {
top: 100%;
}
}
.scan-results {
margin-top: 20px;
}
.results-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
margin-bottom: 15px;
}
.result-item {
padding: 12px;
border-bottom: 1px solid #eee;
}
.result-item:last-child {
border-bottom: none;
}
.result-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.result-number {
font-weight: bold;
color: #007bff;
}
.result-time {
font-size: 0.85em;
color: #666;
}
.result-content {
margin-bottom: 10px;
}
.qr-data,
.qr-format {
margin-bottom: 5px;
word-break: break-all;
}
.result-actions {
display: flex;
gap: 8px;
}
.copy-btn,
.remove-btn {
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
background: #fff;
cursor: pointer;
font-size: 0.8em;
}
.copy-btn:hover {
background: #e9ecef;
}
.remove-btn:hover {
background: #f8d7da;
color: #721c24;
}
.results-actions {
display: flex;
gap: 10px;
}
.clear-btn,
.export-btn {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
.clear-btn:hover {
background: #f8d7da;
color: #721c24;
}
.export-btn:hover {
background: #d4edda;
color: #155724;
}
.settings-panel {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.setting-group {
margin-bottom: 15px;
}
.setting-group label {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
margin-bottom: 5px;
}
.setting-group input[type="number"] {
width: 100px;
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
.setting-description {
font-size: 0.85em;
color: #666;
margin-top: 3px;
margin-left: 24px;
}
</style>

667
src/components/sub-components/ThreeJSViewer.vue

@ -1,667 +0,0 @@
<template>
<div class="threejs-viewer">
<h2>3D Model Viewer</h2>
<!-- Viewer controls -->
<div class="viewer-controls">
<button :disabled="isLoading || !modelUrl" @click="loadModel">
{{ isLoading ? "Loading..." : "Load Model" }}
</button>
<button :disabled="!isModelLoaded" @click="resetCamera">
Reset Camera
</button>
<button :disabled="!isModelLoaded" @click="toggleAnimation">
{{ isAnimating ? "Stop" : "Start" }} Animation
</button>
<button :disabled="!isModelLoaded" @click="toggleWireframe">
{{ showWireframe ? "Hide" : "Show" }} Wireframe
</button>
</div>
<!-- Loading status -->
<div v-if="isLoading" class="loading-status">
<div class="loading-spinner"></div>
<p>Loading 3D model...</p>
<p class="loading-detail">{{ loadingProgress }}% complete</p>
</div>
<!-- Error status -->
<div v-if="loadError" class="error-status">
<p>Failed to load model: {{ loadError }}</p>
<button class="retry-btn" @click="retryLoad">Retry</button>
</div>
<!-- 3D Canvas -->
<div
ref="canvasContainer"
class="canvas-container"
:class="{ 'model-loaded': isModelLoaded }"
>
<canvas ref="threeCanvas" class="three-canvas"></canvas>
<!-- Overlay controls -->
<div v-if="isModelLoaded" class="overlay-controls">
<div class="control-group">
<label>Camera Distance:</label>
<input
v-model.number="cameraDistance"
type="range"
min="1"
max="20"
step="0.1"
@input="updateCameraDistance"
/>
<span>{{ cameraDistance.toFixed(1) }}</span>
</div>
<div class="control-group">
<label>Rotation Speed:</label>
<input
v-model.number="rotationSpeed"
type="range"
min="0"
max="2"
step="0.1"
/>
<span>{{ rotationSpeed.toFixed(1) }}</span>
</div>
<div class="control-group">
<label>Light Intensity:</label>
<input
v-model.number="lightIntensity"
type="range"
min="0"
max="2"
step="0.1"
@input="updateLightIntensity"
/>
<span>{{ lightIntensity.toFixed(1) }}</span>
</div>
</div>
<!-- Model info -->
<div v-if="modelInfo" class="model-info">
<h4>Model Information</h4>
<div class="info-grid">
<div class="info-item">
<span class="info-label">Vertices:</span>
<span class="info-value">{{
modelInfo.vertexCount.toLocaleString()
}}</span>
</div>
<div class="info-item">
<span class="info-label">Faces:</span>
<span class="info-value">{{
modelInfo.faceCount.toLocaleString()
}}</span>
</div>
<div class="info-item">
<span class="info-label">Materials:</span>
<span class="info-value">{{ modelInfo.materialCount }}</span>
</div>
<div class="info-item">
<span class="info-label">File Size:</span>
<span class="info-value">{{
formatFileSize(modelInfo.fileSize)
}}</span>
</div>
</div>
</div>
</div>
<!-- Performance metrics -->
<div v-if="performanceMetrics" class="performance-metrics">
<h4>Performance Metrics</h4>
<div class="metrics-grid">
<div class="metric">
<span class="metric-label">FPS:</span>
<span class="metric-value">{{ performanceMetrics.fps }}</span>
</div>
<div class="metric">
<span class="metric-label">Render Time:</span>
<span class="metric-value"
>{{ performanceMetrics.renderTime }}ms</span
>
</div>
<div class="metric">
<span class="metric-label">Memory Usage:</span>
<span class="metric-value"
>{{ performanceMetrics.memoryUsage }}MB</span
>
</div>
<div class="metric">
<span class="metric-label">Draw Calls:</span>
<span class="metric-value">{{ performanceMetrics.drawCalls }}</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
interface ModelInfo {
vertexCount: number;
faceCount: number;
materialCount: number;
fileSize: number;
boundingBox: {
min: { x: number; y: number; z: number };
max: { x: number; y: number; z: number };
};
}
interface PerformanceMetrics {
fps: number;
renderTime: number;
memoryUsage: number;
drawCalls: number;
}
/**
* ThreeJS 3D Model Viewer Component
*
* Demonstrates lazy loading for heavy 3D rendering libraries.
* This component would benefit from lazy loading as ThreeJS is a large
* library that's only needed for 3D visualization features.
*
* @author Matthew Raymer
* @version 1.0.0
*/
@Component({
name: "ThreeJSViewer",
})
export default class ThreeJSViewer extends Vue {
@Prop({ required: true }) readonly modelUrl!: string;
// Component state
isLoading = false;
isModelLoaded = false;
loadError: string | null = null;
loadingProgress = 0;
// Animation state
isAnimating = false;
showWireframe = false;
// Camera and lighting controls
cameraDistance = 5;
rotationSpeed = 0.5;
lightIntensity = 1;
// Canvas references
canvasContainer: HTMLElement | null = null;
threeCanvas: HTMLCanvasElement | null = null;
// Model and performance data
modelInfo: ModelInfo | null = null;
performanceMetrics: PerformanceMetrics | null = null;
// ThreeJS objects (will be lazy loaded)
private three: any = null;
private scene: any = null;
private camera: any = null;
private renderer: any = null;
private model: any = null;
private controls: any = null;
private animationId: number | null = null;
private frameCount = 0;
private lastTime = 0;
// Lifecycle hooks
mounted(): void {
console.log("[ThreeJSViewer] Component mounted");
this.initializeCanvas();
}
beforeUnmount(): void {
this.cleanup();
console.log("[ThreeJSViewer] Component unmounting");
}
// Methods
private initializeCanvas(): void {
this.canvasContainer = this.$refs.canvasContainer as HTMLElement;
this.threeCanvas = this.$refs.threeCanvas as HTMLCanvasElement;
if (this.threeCanvas) {
this.threeCanvas.width = this.canvasContainer.clientWidth;
this.threeCanvas.height = this.canvasContainer.clientHeight;
}
}
async loadModel(): Promise<void> {
if (this.isLoading || !this.modelUrl) return;
this.isLoading = true;
this.loadError = null;
this.loadingProgress = 0;
try {
console.log("[ThreeJSViewer] Loading 3D model:", this.modelUrl);
// Lazy load ThreeJS
await this.loadThreeJS();
// Initialize scene
await this.initializeScene();
// Load model
await this.loadModelFile();
// Start rendering
this.startRendering();
this.isModelLoaded = true;
this.isLoading = false;
// Emit model loaded event
this.$emit("model-loaded", this.modelInfo);
console.log("[ThreeJSViewer] Model loaded successfully");
} catch (error) {
console.error("[ThreeJSViewer] Failed to load model:", error);
this.loadError = error instanceof Error ? error.message : "Unknown error";
this.isLoading = false;
}
}
private async loadThreeJS(): Promise<void> {
// Simulate loading ThreeJS library
this.loadingProgress = 20;
await this.simulateLoading(500);
// In a real implementation, you would import ThreeJS here
// this.three = await import('three');
this.loadingProgress = 40;
await this.simulateLoading(300);
}
private async initializeScene(): Promise<void> {
this.loadingProgress = 60;
// Simulate scene initialization
await this.simulateLoading(400);
// In a real implementation, you would set up ThreeJS scene here
// this.scene = new this.three.Scene();
// this.camera = new this.three.PerspectiveCamera(75, width / height, 0.1, 1000);
// this.renderer = new this.three.WebGLRenderer({ canvas: this.threeCanvas });
this.loadingProgress = 80;
}
private async loadModelFile(): Promise<void> {
this.loadingProgress = 90;
// Simulate model loading
await this.simulateLoading(600);
// Simulate model info
this.modelInfo = {
vertexCount: Math.floor(Math.random() * 50000) + 1000,
faceCount: Math.floor(Math.random() * 25000) + 500,
materialCount: Math.floor(Math.random() * 5) + 1,
fileSize: Math.floor(Math.random() * 5000000) + 100000,
boundingBox: {
min: { x: -1, y: -1, z: -1 },
max: { x: 1, y: 1, z: 1 },
},
};
this.loadingProgress = 100;
}
private async simulateLoading(delay: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, delay));
}
private startRendering(): void {
if (!this.isModelLoaded) return;
this.isAnimating = true;
this.animate();
// Start performance monitoring
this.startPerformanceMonitoring();
}
private animate(): void {
if (!this.isAnimating) return;
this.animationId = requestAnimationFrame(() => this.animate());
// Simulate model rotation
if (this.model && this.rotationSpeed > 0) {
// this.model.rotation.y += this.rotationSpeed * 0.01;
}
// Simulate rendering
// this.renderer.render(this.scene, this.camera);
this.frameCount++;
}
private startPerformanceMonitoring(): void {
const updateMetrics = () => {
if (!this.isAnimating) return;
const now = performance.now();
const deltaTime = now - this.lastTime;
if (deltaTime > 0) {
const fps = Math.round(1000 / deltaTime);
this.performanceMetrics = {
fps: Math.min(fps, 60), // Cap at 60 FPS for display
renderTime: Math.round(deltaTime),
memoryUsage: Math.round((Math.random() * 50 + 10) * 100) / 100,
drawCalls: Math.floor(Math.random() * 100) + 10,
};
}
this.lastTime = now;
requestAnimationFrame(updateMetrics);
};
updateMetrics();
}
resetCamera(): void {
if (!this.isModelLoaded) return;
this.cameraDistance = 5;
this.updateCameraDistance();
console.log("[ThreeJSViewer] Camera reset");
}
toggleAnimation(): void {
this.isAnimating = !this.isAnimating;
if (this.isAnimating) {
this.animate();
} else if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
console.log("[ThreeJSViewer] Animation toggled:", this.isAnimating);
}
toggleWireframe(): void {
this.showWireframe = !this.showWireframe;
// In a real implementation, you would toggle wireframe mode
// this.model.traverse((child: any) => {
// if (child.isMesh) {
// child.material.wireframe = this.showWireframe;
// }
// });
console.log("[ThreeJSViewer] Wireframe toggled:", this.showWireframe);
}
updateCameraDistance(): void {
if (!this.isModelLoaded) return;
// In a real implementation, you would update camera position
// this.camera.position.z = this.cameraDistance;
// this.camera.lookAt(0, 0, 0);
}
updateLightIntensity(): void {
if (!this.isModelLoaded) return;
// In a real implementation, you would update light intensity
// this.light.intensity = this.lightIntensity;
}
retryLoad(): void {
this.loadError = null;
this.loadModel();
}
private cleanup(): void {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
if (this.renderer) {
this.renderer.dispose();
}
this.isAnimating = false;
this.isModelLoaded = false;
}
formatFileSize(bytes: number): string {
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
}
// Event emitters
@Emit("model-loaded")
emitModelLoaded(info: ModelInfo): ModelInfo {
return info;
}
}
</script>
<style scoped>
.threejs-viewer {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.viewer-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.viewer-controls button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: background-color 0.2s;
}
.viewer-controls button:hover:not(:disabled) {
background: #e9ecef;
}
.viewer-controls button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-status {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-detail {
font-size: 0.9em;
color: #666;
margin-top: 10px;
}
.error-status {
padding: 20px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
text-align: center;
}
.retry-btn {
margin-top: 10px;
padding: 8px 16px;
border: 1px solid #721c24;
border-radius: 4px;
background: #721c24;
color: #fff;
cursor: pointer;
}
.retry-btn:hover {
background: #5a1a1a;
}
.canvas-container {
position: relative;
width: 100%;
height: 400px;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: #000;
margin: 20px 0;
}
.canvas-container.model-loaded {
border-color: #28a745;
}
.three-canvas {
width: 100%;
height: 100%;
display: block;
}
.overlay-controls {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 15px;
border-radius: 4px;
min-width: 200px;
}
.control-group {
margin-bottom: 10px;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-group label {
display: block;
font-size: 0.9em;
margin-bottom: 5px;
}
.control-group input[type="range"] {
width: 100%;
margin-right: 10px;
}
.control-group span {
font-size: 0.8em;
color: #ccc;
}
.model-info {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 15px;
border-radius: 4px;
min-width: 200px;
}
.model-info h4 {
margin: 0 0 10px 0;
font-size: 1em;
}
.info-grid {
display: grid;
gap: 5px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 0.85em;
}
.info-label {
color: #ccc;
}
.info-value {
font-weight: bold;
}
.performance-metrics {
margin-top: 20px;
padding: 15px;
background: #e8f4fd;
border-radius: 4px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 10px;
}
.metric {
display: flex;
justify-content: space-between;
padding: 8px;
background: #fff;
border-radius: 4px;
}
.metric-label {
font-weight: bold;
color: #333;
}
.metric-value {
color: #007bff;
font-weight: bold;
}
</style>

70
vite.config.mts

@ -1,70 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@nostr/tools': path.resolve(__dirname, 'node_modules/@nostr/tools'),
'@nostr/tools/nip06': path.resolve(__dirname, 'node_modules/@nostr/tools/nip06'),
stream: 'stream-browserify',
util: 'util',
crypto: 'crypto-browserify',
assert: 'assert/',
http: 'stream-http',
https: 'https-browserify',
url: 'url/',
zlib: 'browserify-zlib',
path: 'path-browserify',
fs: false,
tty: 'tty-browserify',
net: false,
dns: false,
child_process: false,
os: false
},
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
},
optimizeDeps: {
include: ['@nostr/tools', '@nostr/tools/nip06', '@jlongster/sql.js'],
esbuildOptions: {
define: {
global: 'globalThis'
}
}
},
build: {
sourcemap: true,
target: 'esnext',
chunkSizeWarningLimit: 1000,
commonjsOptions: {
include: [/node_modules/],
transformMixedEsModules: true
},
rollupOptions: {
external: [
'stream', 'util', 'crypto', 'http', 'https', 'url', 'zlib',
'path', 'fs', 'tty', 'assert', 'net', 'dns', 'child_process', 'os'
],
output: {
globals: {
stream: 'stream',
util: 'util',
crypto: 'crypto',
http: 'http',
https: 'https',
url: 'url',
zlib: 'zlib',
path: 'path',
assert: 'assert',
tty: 'tty'
}
}
}
}
});

47
vite.config.ts

@ -1,47 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
export default defineConfig({
plugins: [vue()],
// CORS headers removed to allow images from any domain
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@nostr/tools': path.resolve(__dirname, 'node_modules/@nostr/tools'),
'@nostr/tools/nip06': path.resolve(__dirname, 'node_modules/@nostr/tools/nip06'),
stream: 'stream-browserify',
util: 'util',
crypto: 'crypto-browserify'
},
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
},
optimizeDeps: {
include: ['@nostr/tools', '@nostr/tools/nip06', '@jlongster/sql.js'],
esbuildOptions: {
define: {
global: 'globalThis'
}
}
},
build: {
sourcemap: true,
target: 'esnext',
chunkSizeWarningLimit: 1000,
commonjsOptions: {
include: [/node_modules/],
transformMixedEsModules: true
},
rollupOptions: {
external: ['stream', 'util', 'crypto'],
output: {
globals: {
stream: 'stream',
util: 'util',
crypto: 'crypto'
}
}
}
},
assetsInclude: ['**/*.wasm']
});
Loading…
Cancel
Save