Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions src/ImportBootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { BVHImporter } from './lib/processes/import/BVHImporter'
import { skeletonStorage } from './lib/services/SkeletonStorage'
import { add_preview_skeleton_from_bvh, remove_preview_skeleton } from './lib/processes/load-skeleton/PreviewSkeletonManager'
import { ThemeManager } from './lib/ThemeManager'
import tippy from 'tippy.js'
import 'tippy.js/dist/tippy.css'
import './environment.js'

class ImportBootstrap {
private readonly camera = this.createCamera()
private readonly renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
private controls: OrbitControls | undefined
private readonly scene = new THREE.Scene()
private readonly bvhImporter = new BVHImporter()
private readonly themeManager = new ThemeManager()
private currentPreviewArmature: THREE.Object3D | null = null

constructor () {
this.setupEnvironment()
this.addEventListeners()
this.setupTooltips()
this.injectBuildVersion()
this.animate()
this.updateStoredSkeletonsList()
}

private createCamera (): THREE.PerspectiveCamera {
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 1.7, 5)
return camera
}

private setupEnvironment (): void {
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.shadowMap.enabled = true
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
this.renderer.toneMappingExposure = 2.0
document.body.appendChild(this.renderer.domElement)

this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.target.set(0, 0.9, 0)
this.controls.minDistance = 0.5
this.controls.maxDistance = 30
this.controls.update()

// Add lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
this.scene.add(ambientLight)

const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 10, 7)
directionalLight.castShadow = true
this.scene.add(directionalLight)

// Add grid
const gridHelper = new THREE.GridHelper(20, 20, 0x4f6f6f, 0x2d4353)
this.scene.add(gridHelper)

// Handle window resize
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
})
}

private addEventListeners (): void {
// Theme changes
this.themeManager.addEventListener('theme-changed', () => {
this.updateGridColor()
})

// BVH file upload
const fileInput = document.getElementById('import-bvh-upload') as HTMLInputElement
fileInput?.addEventListener('change', async (event) => {
const files = (event.target as HTMLInputElement).files
if (files && files.length > 0) {
await this.handleBVHImport(files[0])
}
})

// Attribution link
document.getElementById('attribution-link')?.addEventListener('click', (event) => {
event.preventDefault()
this.showContributorsDialog()
})

// Theme toggle buttons
document.querySelectorAll('#theme-toggle').forEach(button => {
button.addEventListener('click', () => {
this.themeManager.toggle_theme()
})
})
}

private async handleBVHImport (file: File): Promise<void> {
try {
console.log('Importing BVH file:', file.name)

// Read file content as text
const bvhContent = await this.readFileAsText(file)

// Parse to preview
const result = await this.bvhImporter.importFromFile(file)

if (!result) {
console.error('Failed to import BVH')
return
}

// Remove previous preview
if (this.currentPreviewArmature) {
remove_preview_skeleton(this.scene)
}

// Store the skeleton with BVH content for persistence
const skeletonName = file.name.replace(/\.bvh$/i, '')
await skeletonStorage.storeSkeletonFromBVH(skeletonName, bvhContent)

// Show preview
this.currentPreviewArmature = result.armature.clone()
await add_preview_skeleton_from_bvh(this.scene, this.currentPreviewArmature)

// Update UI
const nameElement = document.getElementById('imported-skeleton-name')
if (nameElement) {
nameElement.textContent = skeletonName
}

const infoElement = document.getElementById('imported-skeleton-info')
if (infoElement) {
infoElement.style.display = 'block'
}

// Update stored skeletons list
this.updateStoredSkeletonsList()

console.log('BVH imported successfully:', {
name: skeletonName,
boneCount: result.skeleton.bones.length,
animationCount: result.animations.length
})

} catch (error) {
console.error('Error importing BVH:', error)
alert('Failed to import BVH file. Please ensure it is a valid BVH format.')
}
}

private updateStoredSkeletonsList (): void {
const listElement = document.getElementById('stored-skeletons-list')
if (!listElement) return

const skeletons = skeletonStorage.getAllSkeletonsInfo()

if (skeletons.length === 0) {
listElement.innerHTML = '<p style="color: var(--muted-text-color); font-style: italic;">No imported skeletons yet.</p>'
return
}

listElement.innerHTML = skeletons.map(skeleton => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; background: var(--secondary-bg-color); margin-bottom: 0.5rem; border-radius: 4px;">
<span style="font-weight: 500;">${skeleton.name}</span>
<span style="font-size: 0.8rem; color: var(--muted-text-color);">${skeleton.animationCount} animation${skeleton.animationCount !== 1 ? 's' : ''}</span>
</div>
`).join('')
}

private readFileAsText (file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (event) => {
const text = event.target?.result as string
if (text) {
resolve(text)
} else {
reject(new Error('Failed to read file'))
}
}
reader.onerror = () => reject(new Error('File read error'))
reader.readAsText(file)
})
}

private updateGridColor (): void {
// Remove old grid
const oldGrid = this.scene.getObjectByName('GridHelper')
if (oldGrid) {
this.scene.remove(oldGrid)
}

// Add new grid with appropriate color
const isLight = this.themeManager.get_current_theme() === 'light'
const gridColor = isLight ? 0xcccccc : 0x4f6f6f
const gridHelper = new THREE.GridHelper(20, 20, gridColor, isLight ? 0xecf0f1 : 0x2d4353)
gridHelper.name = 'GridHelper'
this.scene.add(gridHelper)
}

private setupTooltips (): void {
tippy('[data-tippy-content]', { theme: 'mesh2motion' })
}

private injectBuildVersion (): void {
const buildVersionElement = document.getElementById('build-version')
const commitSha = (window as unknown as { CLOUDFLARE_COMMIT_SHA?: string }).CLOUDFLARE_COMMIT_SHA
const branch = (window as unknown as { CLOUDFLARE_BRANCH?: string }).CLOUDFLARE_BRANCH
if (buildVersionElement && commitSha) {
buildVersionElement.textContent = `git:${commitSha.slice(0, 9)}-${branch ?? 'unknown'}`
}
}

private showContributorsDialog (): void {
// Simple alert for now - could be a proper modal
alert('Contributors: Mesh2Motion Team')
}

private animate (): void {
requestAnimationFrame(() => this.animate())
this.controls?.update()
this.renderer.render(this.scene, this.camera)
}
}

// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new ImportBootstrap()
})
9 changes: 8 additions & 1 deletion src/Mesh2MotionEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,11 @@ export class Mesh2MotionEngine {
}
else if (this.process_step === ProcessStep.AnimationsListing) {
this.process_step = ProcessStep.AnimationsListing
this.animations_listing_step.begin(this.load_skeleton_step.skeleton_type(), this.load_skeleton_step.skeleton_scale())
this.animations_listing_step.begin(
this.load_skeleton_step.skeleton_type(),
this.load_skeleton_step.skeleton_scale(),
this.load_skeleton_step.get_selected_custom_skeleton_id()
)

// update reference of skeleton helper to use the final skinned mesh
this.regenerate_skeleton_helper(this.weight_skin_step.skeleton())
Expand All @@ -385,6 +389,9 @@ export class Mesh2MotionEngine {
this.update_a_pose_options_visibility()

this.animations_listing_step.load_and_apply_default_animation_to_skinned_mesh(this.weight_skin_step.final_skinned_meshes())
.catch((err) => {
console.error('Error loading animations for custom skeleton:', err)
})

if (this.skeleton_helper !== undefined) {
this.skeleton_helper.hide() // hide skeleton helper in animations listing step
Expand Down
6 changes: 3 additions & 3 deletions src/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<a href="/">Explore</a>
<span class="active-nav-item">Use Your Model</span>
<a href="/retarget/index.html">Use Your Rigged Model</a>
<a href="/import.html">Import</a>
</div>

<div style="display: inline-flex; align-items: center;">
Expand Down Expand Up @@ -189,7 +190,7 @@
</div>
<div style="display: flex; flex-direction: row; gap: 1rem; justify-content: flex-start; align-items: center;">
<span id="scale-skeleton-percentage-display">100%</span>
<input id="scale-skeleton-input" style="flex-grow: 1;" type="range" min=".10" max="2.00" value="1.0" step="0.01" />
<input id="scale-skeleton-input" style="flex-grow: 1;" type="range" min="0.01" max="2.00" value="1.0" step="0.01" />
</div>
</div>

Expand All @@ -202,7 +203,6 @@

</div>


<span id="skeleton-step-actions">

<!-- undo/redo system-->
Expand Down Expand Up @@ -428,4 +428,4 @@
<span id="build-version"></span>

</body>
</html>
</html>
104 changes: 104 additions & 0 deletions src/import.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title style="display: none">Mesh2Motion - Import Skeleton</title>
<link rel="icon" type="image/png" href="./images/favicon.png"/>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0">
<script type="module" src="./ImportBootstrap.ts"></script>
<link href="./styles.css" rel="stylesheet" crossorigin="anonymous" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />

<nav>
<div style="display: inline-flex; align-items: center;">
<a href="https://mesh2motion.org"><img src="./images/mesh2motion.svg" width="200" /></a>
<a href="/">Explore</a>
<a href="/create.html">Use Your Model</a>
<a href="/retarget/index.html">Use Your Rigged Model</a>
<span class="active-nav-item">Import</span>
</div>

<div style="display: inline-flex; align-items: center;">
<a href="https://support.mesh2motion.org" target="_blank"> 💗 Support</a>
<a href="https://github.com/scottpetrovic/mesh2motion-app" target="_blank">Github</a>
<a href="#" id="attribution-link">Contributors</a>

<!-- Theme Toggle -->
<button id="theme-toggle">
<span class="theme-icon"></span>
</button>
</div>
</nav>

<!-- Top toolbar with mouse controls -->
<div id="header-ui">
<div>
<img src="images/mouse-left.svg" height="30" width="30" style="vertical-align: middle;"/> Rotate
</div>
<div>
<img src="images/mouse-right.svg" height="30" width="30" style="vertical-align: middle;"/> Pan
</div>
<div>
<img src="images/mouse-middle.svg" height="30" width="30" style="vertical-align: middle;"/> Zoom
</div>
</div>

<!-- Theme Toggle -->
<div id="theme-toggle-container">
<button id="theme-toggle">
<span class="theme-icon"></span>
</button>
</div>

<div id="tool-panel">
<div id="tool-selection-group">
<div style="margin-bottom: 1rem; padding: 0;">
<span id="current-step-index">1</span>
<span id="current-step-label">Import Skeleton</span>
</div>

<div id="import-tools">
<p class="step-instructions">Import a custom skeleton with animation from a BVH file. The skeleton will be available in the Create page.</p>

<div style="display: flex; flex-direction: column; gap: 1rem;">
<label for="import-bvh-upload" class="button" data-tippy-content="Import a BVH file containing skeleton hierarchy and animation">
<span class="button-icon-group">
<span class="material-symbols-outlined">upload</span>
<span>Import BVH File</span>
<span class="material-symbols-outlined">help</span>
</span>
</label>
<input id="import-bvh-upload" type="file" accept=".bvh" />

<div id="imported-skeleton-info" style="display: none;" class="alternate-background-section">
<span>Imported: <span id="imported-skeleton-name">-</span></span>
<p style="margin: 0.5rem 0 0 0; font-size: 0.85rem; color: var(--muted-text-color);">
This skeleton is now available in the Create page's skeleton template dropdown.
</p>
</div>
</div>

<hr />

<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<span>Stored Skeletons</span>
<div id="stored-skeletons-list" style="max-height: 200px; overflow-y: auto;">
<p style="color: var(--muted-text-color); font-style: italic;">No imported skeletons yet.</p>
</div>
</div>

<hr />

<div style="display: flex; gap: 0.5rem;">
<a href="/create.html" class="button" style="text-decoration: none; text-align: center;">Go to Create &nbsp;&#x203a;</a>
</div>
</div>
</div>
</div>

<div id="build-version" style="position: fixed; bottom: 10px; right: 10px; font-size: 0.75rem; color: var(--muted-text-color);"></div>
</body>
</html>
Loading
Loading