blacktrigram 0.7.39 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/audio/AudioAssetRegistry.js.map +1 -1
- package/lib/audio/AudioCache.js.map +1 -1
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioMonitor.js.map +1 -1
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/AudioProvider.js.map +1 -1
- package/lib/audio/AudioUtils.js.map +1 -1
- package/lib/audio/BoneImpactAudioMap.js.map +1 -1
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/audio/types.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
- package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
- package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
- package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/constants/bodyDimensions.js.map +1 -1
- package/lib/constants/bodyRenderingConstants.js.map +1 -1
- package/lib/data/archetypeClothing.js.map +1 -1
- package/lib/data/archetypePhysicalAttributes.js.map +1 -1
- package/lib/data/techniqueMappings.js.map +1 -1
- package/lib/data/techniques.js.map +1 -1
- package/lib/hooks/useActionFeedback.js.map +1 -1
- package/lib/hooks/useBalanceAnimations.js.map +1 -1
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useDebounce.js.map +1 -1
- package/lib/hooks/useHUDLayout.js.map +1 -1
- package/lib/hooks/useHandPoseTransitions.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useMatchCountdown.js.map +1 -1
- package/lib/hooks/useMuscleActivation.js.map +1 -1
- package/lib/hooks/usePauseMenu.js.map +1 -1
- package/lib/hooks/usePlayerAnimation.js.map +1 -1
- package/lib/hooks/useResponsiveLayout.js.map +1 -1
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useThrottle.js.map +1 -1
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/EffectCalculator.js.map +1 -1
- package/lib/systems/LayoutSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ResponsiveScaling.js.map +1 -1
- package/lib/systems/TrigramSystem.js.map +1 -1
- package/lib/systems/VitalPointSystem.js.map +1 -1
- package/lib/systems/ai/AIPersonality.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
- package/lib/systems/ai/ComboSystem.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/ai/TrainingAI.js.map +1 -1
- package/lib/systems/ai/types.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/HandPoses.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeInterpolation.js +3 -90
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
- package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
- package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
- package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
- package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
- package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
- package/lib/systems/animation/core/types.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
- package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
- package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
- package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
- package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
- package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/bodypart/types.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/breathing/feedback.js.map +1 -1
- package/lib/systems/breathing/integration.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/FallIntegration.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
- package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
- package/lib/systems/combat/typeGuards.js.map +1 -1
- package/lib/systems/effects.js.map +1 -1
- package/lib/systems/game.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
- package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
- package/lib/systems/movement/integration.js.map +1 -1
- package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
- package/lib/systems/physics/CollisionDetection.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
- package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
- package/lib/systems/trigram/KoreanCulture.js.map +1 -1
- package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
- package/lib/systems/trigram/StanceManager.js.map +1 -1
- package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
- package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
- package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
- package/lib/systems/trigram/techniques/index.js.map +1 -1
- package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
- package/lib/systems/trigram/types.js.map +1 -1
- package/lib/systems/types.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
- package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
- package/lib/types/AccessibilityTypes.js.map +1 -1
- package/lib/types/PhysicsTypes.js.map +1 -1
- package/lib/types/common.js.map +1 -1
- package/lib/types/constants/colors.js.map +1 -1
- package/lib/types/constants/designSystem.js.map +1 -1
- package/lib/types/constants/layout.js.map +1 -1
- package/lib/types/constants/performance.js.map +1 -1
- package/lib/types/constants/typography.js.map +1 -1
- package/lib/types/facial.js.map +1 -1
- package/lib/types/hand-animation.js.map +1 -1
- package/lib/types/injury.js.map +1 -1
- package/lib/types/physics.js.map +1 -1
- package/lib/types/skeletal.js.map +1 -1
- package/lib/types/techniqueId.js.map +1 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/arenaWorldDimensions.js.map +1 -1
- package/lib/utils/assetConfig.js.map +1 -1
- package/lib/utils/characterScaling.js.map +1 -1
- package/lib/utils/colorHelpers.js.map +1 -1
- package/lib/utils/colorUtils.js.map +1 -1
- package/lib/utils/combatReadiness.js.map +1 -1
- package/lib/utils/controlMapping.js.map +1 -1
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/fabricTextures.js.map +1 -1
- package/lib/utils/hapticFeedback.js.map +1 -1
- package/lib/utils/haptics.js.map +1 -1
- package/lib/utils/htmlOverlayHelpers.js.map +1 -1
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/math.js.map +1 -1
- package/lib/utils/mobileLayoutHelpers.js.map +1 -1
- package/lib/utils/mobileUIUtils.js.map +1 -1
- package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
- package/lib/utils/performanceOptimization.js.map +1 -1
- package/lib/utils/player3DHelpers.js.map +1 -1
- package/lib/utils/playerUtils.js.map +1 -1
- package/lib/utils/responsiveLayout.js.map +1 -1
- package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
- package/lib/utils/responsiveOrientationConstants.js.map +1 -1
- package/lib/utils/safeAreaUtils.js.map +1 -1
- package/lib/utils/sharedPhysicsConfig.js.map +1 -1
- package/lib/utils/skeletonScaling.js.map +1 -1
- package/lib/utils/stanceHelpers.js.map +1 -1
- package/lib/utils/threeObjectPool.js.map +1 -1
- package/lib/utils/visualEffects.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BoneRenderer.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/BoneRenderer.tsx"],"sourcesContent":["/**\n * BoneRenderer component for visualizing skeletal hierarchy\n *\n * Renders bone hierarchy as connected capsule meshes with proper transformations.\n * Recursively renders parent-child bone relationships for complete skeleton visualization.\n *\n * @module components/three/BoneRenderer\n * @category 3D Components\n * @korean 뼈렌더러컴포넌트\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport {\n BASE_BONE_RADIUS_RATIO,\n calculateBoneThickness,\n} from \"../../../../constants/bodyRenderingConstants\";\nimport type { PlayerArchetype } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport type {\n FacialDamageState,\n FacialExpression,\n} from \"../../../../types/facial\";\nimport { DEFAULT_FACIAL_DAMAGE } from \"../../../../types/facial\";\nimport type { HandAnimationState } from \"../../../../types/hand-animation\";\nimport type { Bone, SkeletalRig } from \"../../../../types/skeletal\";\nimport { BoneMuscles } from \"./BoneAttachedMuscles\";\nimport { BoneClothing } from \"./BoneClothing\";\nimport { BodySurface } from \"./BodySurface\";\nimport Face3D from \"./Face3D\";\nimport Foot3D from \"./Foot3D\";\nimport Hand3D from \"./Hand3D\";\n\n/**\n * Props for BoneRenderer component\n *\n * @public\n * @category Component Props\n * @korean 뼈렌더러속성\n */\nexport interface BoneRendererProps {\n /**\n * Skeletal rig to render\n * @korean 골격\n */\n readonly rig: SkeletalRig;\n\n /**\n * Bone color\n * @korean 뼈색상\n */\n readonly color?: number;\n\n /**\n * Whether to show bones (for debugging)\n * @korean 뼈표시여부\n */\n readonly showBones?: boolean;\n\n /**\n * Render mode: 'solid' for solid meshes, 'debug' for wireframe\n * @korean 렌더모드\n */\n readonly renderMode?: \"solid\" | \"debug\";\n\n /**\n * Left hand animation state\n * @korean 왼손애니메이션상태\n */\n readonly leftHandState?: HandAnimationState;\n\n /**\n * Right hand animation state\n * @korean 오른손애니메이션상태\n */\n readonly rightHandState?: HandAnimationState;\n\n /**\n * Distance from camera for LOD\n * @korean 카메라거리\n */\n readonly cameraDistance?: number;\n\n /**\n * Current facial expression\n * @korean 얼굴표정\n */\n readonly facialExpression?: FacialExpression;\n\n /**\n * Facial damage state\n * @korean 얼굴손상\n */\n readonly facialDamage?: FacialDamageState;\n\n /**\n * Opponent position for eye tracking\n * @korean 상대위치\n */\n readonly opponentPosition?: THREE.Vector3;\n\n /**\n * Enable facial expressions rendering\n * @korean 얼굴표정렌더링\n */\n readonly enableFacialExpressions?: boolean;\n\n /**\n * Enable eye tracking\n * @korean 눈추적활성화\n */\n readonly enableEyeTracking?: boolean;\n\n /**\n * Physical attributes for bone thickness scaling\n * @korean 신체속성\n */\n readonly physicalAttributes?: {\n readonly muscleMass: number;\n readonly fatMass: number;\n readonly shoulderWidth?: number;\n readonly torsoLength?: number;\n readonly armLength?: number;\n readonly legLength?: number;\n };\n\n /**\n * Player archetype for clothing style\n * @korean 플레이어원형\n */\n readonly archetype?: PlayerArchetype;\n\n /**\n * Muscle tension states for bone-attached muscles\n * Map of muscle group name to tension level (0-1)\n * @korean 근육상태들\n */\n readonly muscleStates?: Map<string, number>;\n\n /**\n * Whether player is exhausted (triggers muscle shaking)\n * @korean 피로여부\n */\n readonly isExhausted?: boolean;\n}\n\n/**\n * Single bone renderer with transformation\n *\n * Renders a single bone as a capsule connecting to its parent.\n *\n * @param bone - Bone to render\n * @param color - Bone color\n * @param renderMode - Render mode\n * @param leftHandState - Left hand animation state\n * @param rightHandState - Right hand animation state\n * @param cameraDistance - Distance from camera\n * @param boneThicknessMultiplier - Thickness multiplier for bone radius\n * @korean 단일뼈렌더러\n */\nconst SingleBone: React.FC<{\n readonly bone: Bone;\n readonly color: number;\n readonly renderMode: \"solid\" | \"debug\";\n readonly leftHandState?: HandAnimationState;\n readonly rightHandState?: HandAnimationState;\n readonly cameraDistance?: number;\n readonly facialExpression?: FacialExpression;\n readonly facialDamage?: FacialDamageState;\n readonly opponentPosition?: THREE.Vector3;\n readonly enableFacialExpressions?: boolean;\n readonly enableEyeTracking?: boolean;\n readonly boneThicknessMultiplier?: number;\n readonly muscleStates?: Map<string, number>;\n readonly isExhausted?: boolean;\n readonly physicalAttributes?: {\n readonly muscleMass: number;\n readonly fatMass: number;\n readonly shoulderWidth?: number;\n readonly torsoLength?: number;\n readonly armLength?: number;\n readonly legLength?: number;\n };\n readonly archetype?: PlayerArchetype;\n}> = ({\n bone,\n color,\n renderMode,\n leftHandState,\n rightHandState,\n cameraDistance = 10,\n facialExpression,\n facialDamage,\n opponentPosition,\n enableFacialExpressions = false,\n enableEyeTracking = true,\n boneThicknessMultiplier = 1.0,\n muscleStates,\n isExhausted = false,\n physicalAttributes,\n archetype,\n}) => {\n // Ref for the bone group to update rotation imperatively\n const groupRef = useRef<THREE.Group>(null);\n\n // Sync bone rotation from animation system each frame\n // This is necessary because bone.rotation is mutated in place by batchUpdateBones\n // and React doesn't detect the mutation (no state/prop change)\n useFrame(() => {\n if (groupRef.current) {\n // Apply current bone rotation and position from animation system\n groupRef.current.rotation.set(\n bone.rotation.x,\n bone.rotation.y,\n bone.rotation.z,\n );\n groupRef.current.position.set(\n bone.position.x,\n bone.position.y,\n bone.position.z,\n );\n }\n });\n\n // Calculate bone direction and length\n const boneTransform = useMemo(() => {\n const length = bone.length;\n // CapsuleGeometry in Three.js is aligned along the Y-axis by default (0, 1, 0)\n const capsuleDefaultDirection = new THREE.Vector3(0, 1, 0);\n\n // Calculate rotation to align with bone direction if parent exists\n let rotation = new THREE.Euler(0, 0, 0);\n // Offset to position capsule between parent and this bone's position\n // Capsules extend equally in both directions, so we offset by half length toward parent\n let offset = new THREE.Vector3(0, -length / 2, 0);\n\n if (bone.parent) {\n // Use this bone's local position (parent → child vector) and normalize to get the direction\n // Extract coordinates and manually normalize to avoid issues with Vector3 method availability\n const x = bone.position.x ?? 0;\n const y = bone.position.y ?? 0;\n const z = bone.position.z ?? 0;\n\n const positionLength = Math.sqrt(x * x + y * y + z * z);\n if (positionLength > 0.001) {\n // Manually normalize to get a stable direction vector\n const targetX = x / positionLength;\n const targetY = y / positionLength;\n const targetZ = z / positionLength;\n const target = new THREE.Vector3(targetX, targetY, targetZ);\n\n // Calculate quaternion rotation from capsule's default Y-axis to target direction\n const quaternion = new THREE.Quaternion().setFromUnitVectors(\n capsuleDefaultDirection,\n target,\n );\n rotation = new THREE.Euler().setFromQuaternion(quaternion);\n\n // Calculate offset in the direction toward parent (negative of bone direction)\n // This positions the capsule to connect parent → this bone\n offset = new THREE.Vector3(\n (-targetX * length) / 2,\n (-targetY * length) / 2,\n (-targetZ * length) / 2,\n );\n }\n }\n\n return { length, rotation, offset };\n }, [bone]);\n\n // Determine if muscles are being rendered - if so, hide bone geometry\n // to prevent layer stacking that causes \"bubble man\" appearance\n const hasMuscles = muscleStates !== undefined && muscleStates.size > 0;\n\n return (\n <group\n ref={groupRef}\n scale={bone.scale.toArray()}\n name={`bone-${bone.name}`}\n >\n {/* Bone capsule connecting to parent - ONLY render if no muscles (muscles provide body shape) */}\n {renderMode === \"solid\" && !hasMuscles ? (\n <mesh\n position={boneTransform.offset.toArray()}\n rotation={[\n boneTransform.rotation.x,\n boneTransform.rotation.y,\n boneTransform.rotation.z,\n ]}\n castShadow\n receiveShadow\n >\n <capsuleGeometry\n args={[\n boneTransform.length *\n BASE_BONE_RADIUS_RATIO *\n boneThicknessMultiplier, // Radius scaled by thickness\n boneTransform.length, // Length unchanged\n 4,\n 8,\n ]}\n />\n <meshPhysicalMaterial\n color={color}\n metalness={0.5}\n roughness={0.4}\n clearcoat={1.0}\n clearcoatRoughness={0.1}\n envMapIntensity={1.0}\n />\n </mesh>\n ) : renderMode === \"debug\" ? (\n <mesh\n position={boneTransform.offset.toArray()}\n rotation={[\n boneTransform.rotation.x,\n boneTransform.rotation.y,\n boneTransform.rotation.z,\n ]}\n >\n <capsuleGeometry\n args={[\n boneTransform.length *\n BASE_BONE_RADIUS_RATIO *\n boneThicknessMultiplier, // Radius scaled by thickness\n boneTransform.length, // Length unchanged\n 4,\n 8,\n ]}\n />\n <meshBasicMaterial\n color={color}\n wireframe={true}\n transparent={true}\n opacity={0.5}\n />\n </mesh>\n ) : null}\n\n {/* Joint sphere at bone position */}\n {renderMode === \"debug\" && (\n <mesh>\n <sphereGeometry\n args={[\n boneTransform.length *\n BASE_BONE_RADIUS_RATIO *\n 1.2 *\n boneThicknessMultiplier,\n 8,\n 8,\n ]}\n />\n <meshBasicMaterial color={KOREAN_COLORS.PRIMARY_CYAN} />\n </mesh>\n )}\n\n {/* Render bone-attached muscles */}\n {renderMode === \"solid\" && muscleStates && (\n <BoneMuscles\n boneName={bone.name}\n muscleStates={muscleStates}\n isExhausted={isExhausted}\n physicalAttributes={physicalAttributes}\n />\n )}\n\n {/* Render body surface (skin/flesh layer) - provides continuous humanoid appearance */}\n {renderMode === \"solid\" && archetype && physicalAttributes && (\n <BodySurface\n boneName={bone.name}\n archetype={archetype}\n cameraDistance={cameraDistance}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth ?? 45,\n torsoLength: physicalAttributes.torsoLength ?? 59,\n armLength: physicalAttributes.armLength ?? 77,\n legLength: physicalAttributes.legLength ?? 96,\n }}\n />\n )}\n\n {/* Render bone-attached clothing */}\n {renderMode === \"solid\" && archetype && physicalAttributes && (\n <BoneClothing\n boneName={bone.name}\n archetype={archetype}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth ?? 45,\n torsoLength: physicalAttributes.torsoLength ?? 59,\n armLength: physicalAttributes.armLength ?? 77,\n legLength: physicalAttributes.legLength ?? 96,\n }}\n />\n )}\n\n {/* Render children recursively */}\n {bone.children.map((childBone) => (\n <SingleBone\n key={childBone.name}\n bone={childBone}\n color={color}\n renderMode={renderMode}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={cameraDistance}\n facialExpression={facialExpression}\n facialDamage={facialDamage}\n opponentPosition={opponentPosition}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n boneThicknessMultiplier={boneThicknessMultiplier}\n muscleStates={muscleStates}\n isExhausted={isExhausted}\n physicalAttributes={physicalAttributes}\n archetype={archetype}\n />\n ))}\n\n {/* Add hands at hand bones with animation state */}\n {bone.name === \"hand_L\" && leftHandState && (\n <Hand3D\n side=\"left\"\n pose={leftHandState.currentPose}\n fingerCurl={leftHandState.currentFingerCurl}\n distanceFromCamera={cameraDistance}\n wristRotation={leftHandState.currentWristRotation}\n isHighlighted={leftHandState.isHighlighted}\n highlightMode={leftHandState.highlightMode}\n skinColor={color}\n scale={1.0}\n />\n )}\n {bone.name === \"hand_R\" && rightHandState && (\n <Hand3D\n side=\"right\"\n pose={rightHandState.currentPose}\n fingerCurl={rightHandState.currentFingerCurl}\n distanceFromCamera={cameraDistance}\n wristRotation={rightHandState.currentWristRotation}\n isHighlighted={rightHandState.isHighlighted}\n highlightMode={rightHandState.highlightMode}\n skinColor={color}\n scale={1.0}\n />\n )}\n\n {/* Add feet at foot bones with archetype scaling */}\n {bone.name === \"foot_L\" && (\n <Foot3D\n side=\"left\"\n skinColor={color}\n scale={boneThicknessMultiplier}\n isHighlighted={false}\n />\n )}\n {bone.name === \"foot_R\" && (\n <Foot3D\n side=\"right\"\n skinColor={color}\n scale={boneThicknessMultiplier}\n isHighlighted={false}\n />\n )}\n\n {/* Add facial features at head bone */}\n {bone.name === \"head\" && enableFacialExpressions && facialExpression && (\n <Face3D\n expression={facialExpression}\n damage={facialDamage ?? DEFAULT_FACIAL_DAMAGE}\n opponentPosition={opponentPosition ?? new THREE.Vector3(5, 2, 0)}\n headRotation={bone.rotation.clone()}\n enableEyeTracking={enableEyeTracking}\n enableDamageVisuals={true}\n isMobile={cameraDistance > 15}\n skinColor={color}\n />\n )}\n </group>\n );\n};\n\n/**\n * BoneRenderer component\n *\n * Renders complete skeletal rig with recursive bone hierarchy.\n *\n * @example\n * ```tsx\n * const rig = createHumanoidRig();\n * <BoneRenderer rig={rig} color={0xFF6B6B} renderMode=\"solid\" />\n * ```\n *\n * @korean 뼈렌더러컴포넌트\n */\nexport const BoneRenderer: React.FC<BoneRendererProps> = ({\n rig,\n color = KOREAN_COLORS.ACCENT_RED,\n showBones = true,\n renderMode = \"solid\",\n leftHandState,\n rightHandState,\n cameraDistance = 10,\n facialExpression,\n facialDamage,\n opponentPosition,\n enableFacialExpressions = false, // Default false to avoid breaking existing tests\n enableEyeTracking = true,\n physicalAttributes,\n muscleStates,\n isExhausted = false,\n archetype,\n}) => {\n // Calculate bone thickness multiplier from physical attributes\n const boneThicknessMultiplier = useMemo(() => {\n if (!physicalAttributes) return 1.0;\n return calculateBoneThickness(\n physicalAttributes.muscleMass,\n physicalAttributes.fatMass,\n );\n }, [physicalAttributes]);\n\n if (!showBones) {\n return null;\n }\n\n return (\n <group name=\"bone-renderer\">\n <SingleBone\n bone={rig.root}\n color={color}\n renderMode={renderMode}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={cameraDistance}\n facialExpression={facialExpression}\n facialDamage={facialDamage}\n opponentPosition={opponentPosition}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n boneThicknessMultiplier={boneThicknessMultiplier}\n muscleStates={muscleStates}\n isExhausted={isExhausted}\n physicalAttributes={physicalAttributes}\n archetype={archetype}\n />\n </group>\n );\n};\n\nexport default BoneRenderer;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiKA,IAAM,cAwBA,EACJ,MACA,OACA,YACA,eACA,gBACA,iBAAiB,IACjB,kBACA,cACA,kBACA,0BAA0B,OAC1B,oBAAoB,MACpB,0BAA0B,GAC1B,cACA,cAAc,OACd,oBACA,gBACI;CAEJ,MAAM,WAAW,OAAoB,KAAK;AAK1C,gBAAe;AACb,MAAI,SAAS,SAAS;AAEpB,YAAS,QAAQ,SAAS,IACxB,KAAK,SAAS,GACd,KAAK,SAAS,GACd,KAAK,SAAS,EACf;AACD,YAAS,QAAQ,SAAS,IACxB,KAAK,SAAS,GACd,KAAK,SAAS,GACd,KAAK,SAAS,EACf;;GAEH;CAGF,MAAM,gBAAgB,cAAc;EAClC,MAAM,SAAS,KAAK;EAEpB,MAAM,0BAA0B,IAAI,MAAM,QAAQ,GAAG,GAAG,EAAE;EAG1D,IAAI,WAAW,IAAI,MAAM,MAAM,GAAG,GAAG,EAAE;EAGvC,IAAI,SAAS,IAAI,MAAM,QAAQ,GAAG,CAAC,SAAS,GAAG,EAAE;AAEjD,MAAI,KAAK,QAAQ;GAGf,MAAM,IAAI,KAAK,SAAS,KAAK;GAC7B,MAAM,IAAI,KAAK,SAAS,KAAK;GAC7B,MAAM,IAAI,KAAK,SAAS,KAAK;GAE7B,MAAM,iBAAiB,KAAK,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE;AACvD,OAAI,iBAAiB,MAAO;IAE1B,MAAM,UAAU,IAAI;IACpB,MAAM,UAAU,IAAI;IACpB,MAAM,UAAU,IAAI;IACpB,MAAM,SAAS,IAAI,MAAM,QAAQ,SAAS,SAAS,QAAQ;IAG3D,MAAM,aAAa,IAAI,MAAM,YAAY,CAAC,mBACxC,yBACA,OACD;AACD,eAAW,IAAI,MAAM,OAAO,CAAC,kBAAkB,WAAW;AAI1D,aAAS,IAAI,MAAM,QAChB,CAAC,UAAU,SAAU,GACrB,CAAC,UAAU,SAAU,GACrB,CAAC,UAAU,SAAU,EACvB;;;AAIL,SAAO;GAAE;GAAQ;GAAU;GAAQ;IAClC,CAAC,KAAK,CAAC;CAIV,MAAM,aAAa,iBAAiB,KAAA,KAAa,aAAa,OAAO;AAErE,QACE,qBAAC,SAAD;EACE,KAAK;EACL,OAAO,KAAK,MAAM,SAAS;EAC3B,MAAM,QAAQ,KAAK;YAHrB;GAMG,eAAe,WAAW,CAAC,aAC1B,qBAAC,QAAD;IACE,UAAU,cAAc,OAAO,SAAS;IACxC,UAAU;KACR,cAAc,SAAS;KACvB,cAAc,SAAS;KACvB,cAAc,SAAS;KACxB;IACD,YAAA;IACA,eAAA;cARF,CAUE,oBAAC,mBAAD,EACE,MAAM;KACJ,cAAc,SACZ,yBACA;KACF,cAAc;KACd;KACA;KACD,EACD,CAAA,EACF,oBAAC,wBAAD;KACS;KACP,WAAW;KACX,WAAW;KACX,WAAW;KACX,oBAAoB;KACpB,iBAAiB;KACjB,CAAA,CACG;QACL,eAAe,UACjB,qBAAC,QAAD;IACE,UAAU,cAAc,OAAO,SAAS;IACxC,UAAU;KACR,cAAc,SAAS;KACvB,cAAc,SAAS;KACvB,cAAc,SAAS;KACxB;cANH,CAQE,oBAAC,mBAAD,EACE,MAAM;KACJ,cAAc,SACZ,yBACA;KACF,cAAc;KACd;KACA;KACD,EACD,CAAA,EACF,oBAAC,qBAAD;KACS;KACP,WAAW;KACX,aAAa;KACb,SAAS;KACT,CAAA,CACG;QACL;GAGH,eAAe,WACd,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,kBAAD,EACE,MAAM;IACJ,cAAc,SAAA,KAEZ,MACA;IACF;IACA;IACD,EACD,CAAA,EACF,oBAAC,qBAAD,EAAmB,OAAO,cAAc,cAAgB,CAAA,CACnD,EAAA,CAAA;GAIR,eAAe,WAAW,gBACzB,oBAAC,aAAD;IACE,UAAU,KAAK;IACD;IACD;IACO;IACpB,CAAA;GAIH,eAAe,WAAW,aAAa,sBACtC,oBAAC,aAAD;IACE,UAAU,KAAK;IACJ;IACK;IAChB,oBAAoB;KAClB,YAAY,mBAAmB;KAC/B,SAAS,mBAAmB;KAC5B,eAAe,mBAAmB,iBAAiB;KACnD,aAAa,mBAAmB,eAAe;KAC/C,WAAW,mBAAmB,aAAa;KAC3C,WAAW,mBAAmB,aAAa;KAC5C;IACD,CAAA;GAIH,eAAe,WAAW,aAAa,sBACtC,oBAAC,cAAD;IACE,UAAU,KAAK;IACJ;IACX,oBAAoB;KAClB,YAAY,mBAAmB;KAC/B,SAAS,mBAAmB;KAC5B,eAAe,mBAAmB,iBAAiB;KACnD,aAAa,mBAAmB,eAAe;KAC/C,WAAW,mBAAmB,aAAa;KAC3C,WAAW,mBAAmB,aAAa;KAC5C;IACD,CAAA;GAIH,KAAK,SAAS,KAAK,cAClB,oBAAC,YAAD;IAEE,MAAM;IACC;IACK;IACG;IACC;IACA;IACE;IACJ;IACI;IACO;IACN;IACM;IACX;IACD;IACO;IACT;IACX,EAjBK,UAAU,KAiBf,CACF;GAGD,KAAK,SAAS,YAAY,iBACzB,oBAAC,QAAD;IACE,MAAK;IACL,MAAM,cAAc;IACpB,YAAY,cAAc;IAC1B,oBAAoB;IACpB,eAAe,cAAc;IAC7B,eAAe,cAAc;IAC7B,eAAe,cAAc;IAC7B,WAAW;IACX,OAAO;IACP,CAAA;GAEH,KAAK,SAAS,YAAY,kBACzB,oBAAC,QAAD;IACE,MAAK;IACL,MAAM,eAAe;IACrB,YAAY,eAAe;IAC3B,oBAAoB;IACpB,eAAe,eAAe;IAC9B,eAAe,eAAe;IAC9B,eAAe,eAAe;IAC9B,WAAW;IACX,OAAO;IACP,CAAA;GAIH,KAAK,SAAS,YACb,oBAAC,QAAD;IACE,MAAK;IACL,WAAW;IACX,OAAO;IACP,eAAe;IACf,CAAA;GAEH,KAAK,SAAS,YACb,oBAAC,QAAD;IACE,MAAK;IACL,WAAW;IACX,OAAO;IACP,eAAe;IACf,CAAA;GAIH,KAAK,SAAS,UAAU,2BAA2B,oBAClD,oBAAC,QAAD;IACE,YAAY;IACZ,QAAQ,gBAAgB;IACxB,kBAAkB,oBAAoB,IAAI,MAAM,QAAQ,GAAG,GAAG,EAAE;IAChE,cAAc,KAAK,SAAS,OAAO;IAChB;IACnB,qBAAqB;IACrB,UAAU,iBAAiB;IAC3B,WAAW;IACX,CAAA;GAEE;;;;;;;;;;;;;;;;AAiBZ,IAAa,gBAA6C,EACxD,KACA,QAAQ,cAAc,YACtB,YAAY,MACZ,aAAa,SACb,eACA,gBACA,iBAAiB,IACjB,kBACA,cACA,kBACA,0BAA0B,OAC1B,oBAAoB,MACpB,oBACA,cACA,cAAc,OACd,gBACI;CAEJ,MAAM,0BAA0B,cAAc;AAC5C,MAAI,CAAC,mBAAoB,QAAO;AAChC,SAAO,uBACL,mBAAmB,YACnB,mBAAmB,QACpB;IACA,CAAC,mBAAmB,CAAC;AAExB,KAAI,CAAC,UACH,QAAO;AAGT,QACE,oBAAC,SAAD;EAAO,MAAK;YACV,oBAAC,YAAD;GACE,MAAM,IAAI;GACH;GACK;GACG;GACC;GACA;GACE;GACJ;GACI;GACO;GACN;GACM;GACX;GACD;GACO;GACT;GACX,CAAA;EACI,CAAA"}
|
|
1
|
+
{"version":3,"file":"BoneRenderer.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/BoneRenderer.tsx"],"sourcesContent":["/**\n * BoneRenderer component for visualizing skeletal hierarchy\n *\n * Renders bone hierarchy as connected capsule meshes with proper transformations.\n * Recursively renders parent-child bone relationships for complete skeleton visualization.\n *\n * @module components/three/BoneRenderer\n * @category 3D Components\n * @korean 뼈렌더러컴포넌트\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport {\n BASE_BONE_RADIUS_RATIO,\n calculateBoneThickness,\n} from \"../../../../constants/bodyRenderingConstants\";\nimport type { PlayerArchetype } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport type {\n FacialDamageState,\n FacialExpression,\n} from \"../../../../types/facial\";\nimport { DEFAULT_FACIAL_DAMAGE } from \"../../../../types/facial\";\nimport type { HandAnimationState } from \"../../../../types/hand-animation\";\nimport type { Bone, SkeletalRig } from \"../../../../types/skeletal\";\nimport { BoneMuscles } from \"./BoneAttachedMuscles\";\nimport { BoneClothing } from \"./BoneClothing\";\nimport { BodySurface } from \"./BodySurface\";\nimport Face3D from \"./Face3D\";\nimport Foot3D from \"./Foot3D\";\nimport Hand3D from \"./Hand3D\";\n\n/**\n * Props for BoneRenderer component\n *\n * @public\n * @category Component Props\n * @korean 뼈렌더러속성\n */\nexport interface BoneRendererProps {\n /**\n * Skeletal rig to render\n * @korean 골격\n */\n readonly rig: SkeletalRig;\n\n /**\n * Bone color\n * @korean 뼈색상\n */\n readonly color?: number;\n\n /**\n * Whether to show bones (for debugging)\n * @korean 뼈표시여부\n */\n readonly showBones?: boolean;\n\n /**\n * Render mode: 'solid' for solid meshes, 'debug' for wireframe\n * @korean 렌더모드\n */\n readonly renderMode?: \"solid\" | \"debug\";\n\n /**\n * Left hand animation state\n * @korean 왼손애니메이션상태\n */\n readonly leftHandState?: HandAnimationState;\n\n /**\n * Right hand animation state\n * @korean 오른손애니메이션상태\n */\n readonly rightHandState?: HandAnimationState;\n\n /**\n * Distance from camera for LOD\n * @korean 카메라거리\n */\n readonly cameraDistance?: number;\n\n /**\n * Current facial expression\n * @korean 얼굴표정\n */\n readonly facialExpression?: FacialExpression;\n\n /**\n * Facial damage state\n * @korean 얼굴손상\n */\n readonly facialDamage?: FacialDamageState;\n\n /**\n * Opponent position for eye tracking\n * @korean 상대위치\n */\n readonly opponentPosition?: THREE.Vector3;\n\n /**\n * Enable facial expressions rendering\n * @korean 얼굴표정렌더링\n */\n readonly enableFacialExpressions?: boolean;\n\n /**\n * Enable eye tracking\n * @korean 눈추적활성화\n */\n readonly enableEyeTracking?: boolean;\n\n /**\n * Physical attributes for bone thickness scaling\n * @korean 신체속성\n */\n readonly physicalAttributes?: {\n readonly muscleMass: number;\n readonly fatMass: number;\n readonly shoulderWidth?: number;\n readonly torsoLength?: number;\n readonly armLength?: number;\n readonly legLength?: number;\n };\n\n /**\n * Player archetype for clothing style\n * @korean 플레이어원형\n */\n readonly archetype?: PlayerArchetype;\n\n /**\n * Muscle tension states for bone-attached muscles\n * Map of muscle group name to tension level (0-1)\n * @korean 근육상태들\n */\n readonly muscleStates?: Map<string, number>;\n\n /**\n * Whether player is exhausted (triggers muscle shaking)\n * @korean 피로여부\n */\n readonly isExhausted?: boolean;\n}\n\n/**\n * Single bone renderer with transformation\n *\n * Renders a single bone as a capsule connecting to its parent.\n *\n * @param bone - Bone to render\n * @param color - Bone color\n * @param renderMode - Render mode\n * @param leftHandState - Left hand animation state\n * @param rightHandState - Right hand animation state\n * @param cameraDistance - Distance from camera\n * @param boneThicknessMultiplier - Thickness multiplier for bone radius\n * @korean 단일뼈렌더러\n */\nconst SingleBone: React.FC<{\n readonly bone: Bone;\n readonly color: number;\n readonly renderMode: \"solid\" | \"debug\";\n readonly leftHandState?: HandAnimationState;\n readonly rightHandState?: HandAnimationState;\n readonly cameraDistance?: number;\n readonly facialExpression?: FacialExpression;\n readonly facialDamage?: FacialDamageState;\n readonly opponentPosition?: THREE.Vector3;\n readonly enableFacialExpressions?: boolean;\n readonly enableEyeTracking?: boolean;\n readonly boneThicknessMultiplier?: number;\n readonly muscleStates?: Map<string, number>;\n readonly isExhausted?: boolean;\n readonly physicalAttributes?: {\n readonly muscleMass: number;\n readonly fatMass: number;\n readonly shoulderWidth?: number;\n readonly torsoLength?: number;\n readonly armLength?: number;\n readonly legLength?: number;\n };\n readonly archetype?: PlayerArchetype;\n}> = ({\n bone,\n color,\n renderMode,\n leftHandState,\n rightHandState,\n cameraDistance = 10,\n facialExpression,\n facialDamage,\n opponentPosition,\n enableFacialExpressions = false,\n enableEyeTracking = true,\n boneThicknessMultiplier = 1.0,\n muscleStates,\n isExhausted = false,\n physicalAttributes,\n archetype,\n}) => {\n // Ref for the bone group to update rotation imperatively\n const groupRef = useRef<THREE.Group>(null);\n\n // Sync bone rotation from animation system each frame\n // This is necessary because bone.rotation is mutated in place by batchUpdateBones\n // and React doesn't detect the mutation (no state/prop change)\n useFrame(() => {\n if (groupRef.current) {\n // Apply current bone rotation and position from animation system\n groupRef.current.rotation.set(\n bone.rotation.x,\n bone.rotation.y,\n bone.rotation.z,\n );\n groupRef.current.position.set(\n bone.position.x,\n bone.position.y,\n bone.position.z,\n );\n }\n });\n\n // Calculate bone direction and length\n const boneTransform = useMemo(() => {\n const length = bone.length;\n // CapsuleGeometry in Three.js is aligned along the Y-axis by default (0, 1, 0)\n const capsuleDefaultDirection = new THREE.Vector3(0, 1, 0);\n\n // Calculate rotation to align with bone direction if parent exists\n let rotation = new THREE.Euler(0, 0, 0);\n // Offset to position capsule between parent and this bone's position\n // Capsules extend equally in both directions, so we offset by half length toward parent\n let offset = new THREE.Vector3(0, -length / 2, 0);\n\n if (bone.parent) {\n // Use this bone's local position (parent → child vector) and normalize to get the direction\n // Extract coordinates and manually normalize to avoid issues with Vector3 method availability\n const x = bone.position.x ?? 0;\n const y = bone.position.y ?? 0;\n const z = bone.position.z ?? 0;\n\n const positionLength = Math.sqrt(x * x + y * y + z * z);\n if (positionLength > 0.001) {\n // Manually normalize to get a stable direction vector\n const targetX = x / positionLength;\n const targetY = y / positionLength;\n const targetZ = z / positionLength;\n const target = new THREE.Vector3(targetX, targetY, targetZ);\n\n // Calculate quaternion rotation from capsule's default Y-axis to target direction\n const quaternion = new THREE.Quaternion().setFromUnitVectors(\n capsuleDefaultDirection,\n target,\n );\n rotation = new THREE.Euler().setFromQuaternion(quaternion);\n\n // Calculate offset in the direction toward parent (negative of bone direction)\n // This positions the capsule to connect parent → this bone\n offset = new THREE.Vector3(\n (-targetX * length) / 2,\n (-targetY * length) / 2,\n (-targetZ * length) / 2,\n );\n }\n }\n\n return { length, rotation, offset };\n }, [bone]);\n\n // Determine if muscles are being rendered - if so, hide bone geometry\n // to prevent layer stacking that causes \"bubble man\" appearance\n const hasMuscles = muscleStates !== undefined && muscleStates.size > 0;\n\n return (\n <group\n ref={groupRef}\n scale={bone.scale.toArray()}\n name={`bone-${bone.name}`}\n >\n {/* Bone capsule connecting to parent - ONLY render if no muscles (muscles provide body shape) */}\n {renderMode === \"solid\" && !hasMuscles ? (\n <mesh\n position={boneTransform.offset.toArray()}\n rotation={[\n boneTransform.rotation.x,\n boneTransform.rotation.y,\n boneTransform.rotation.z,\n ]}\n castShadow\n receiveShadow\n >\n <capsuleGeometry\n args={[\n boneTransform.length *\n BASE_BONE_RADIUS_RATIO *\n boneThicknessMultiplier, // Radius scaled by thickness\n boneTransform.length, // Length unchanged\n 4,\n 8,\n ]}\n />\n <meshPhysicalMaterial\n color={color}\n metalness={0.5}\n roughness={0.4}\n clearcoat={1.0}\n clearcoatRoughness={0.1}\n envMapIntensity={1.0}\n />\n </mesh>\n ) : renderMode === \"debug\" ? (\n <mesh\n position={boneTransform.offset.toArray()}\n rotation={[\n boneTransform.rotation.x,\n boneTransform.rotation.y,\n boneTransform.rotation.z,\n ]}\n >\n <capsuleGeometry\n args={[\n boneTransform.length *\n BASE_BONE_RADIUS_RATIO *\n boneThicknessMultiplier, // Radius scaled by thickness\n boneTransform.length, // Length unchanged\n 4,\n 8,\n ]}\n />\n <meshBasicMaterial\n color={color}\n wireframe={true}\n transparent={true}\n opacity={0.5}\n />\n </mesh>\n ) : null}\n\n {/* Joint sphere at bone position */}\n {renderMode === \"debug\" && (\n <mesh>\n <sphereGeometry\n args={[\n boneTransform.length *\n BASE_BONE_RADIUS_RATIO *\n 1.2 *\n boneThicknessMultiplier,\n 8,\n 8,\n ]}\n />\n <meshBasicMaterial color={KOREAN_COLORS.PRIMARY_CYAN} />\n </mesh>\n )}\n\n {/* Render bone-attached muscles */}\n {renderMode === \"solid\" && muscleStates && (\n <BoneMuscles\n boneName={bone.name}\n muscleStates={muscleStates}\n isExhausted={isExhausted}\n physicalAttributes={physicalAttributes}\n />\n )}\n\n {/* Render body surface (skin/flesh layer) - provides continuous humanoid appearance */}\n {renderMode === \"solid\" && archetype && physicalAttributes && (\n <BodySurface\n boneName={bone.name}\n archetype={archetype}\n cameraDistance={cameraDistance}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth ?? 45,\n torsoLength: physicalAttributes.torsoLength ?? 59,\n armLength: physicalAttributes.armLength ?? 77,\n legLength: physicalAttributes.legLength ?? 96,\n }}\n />\n )}\n\n {/* Render bone-attached clothing */}\n {renderMode === \"solid\" && archetype && physicalAttributes && (\n <BoneClothing\n boneName={bone.name}\n archetype={archetype}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth ?? 45,\n torsoLength: physicalAttributes.torsoLength ?? 59,\n armLength: physicalAttributes.armLength ?? 77,\n legLength: physicalAttributes.legLength ?? 96,\n }}\n />\n )}\n\n {/* Render children recursively */}\n {bone.children.map((childBone) => (\n <SingleBone\n key={childBone.name}\n bone={childBone}\n color={color}\n renderMode={renderMode}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={cameraDistance}\n facialExpression={facialExpression}\n facialDamage={facialDamage}\n opponentPosition={opponentPosition}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n boneThicknessMultiplier={boneThicknessMultiplier}\n muscleStates={muscleStates}\n isExhausted={isExhausted}\n physicalAttributes={physicalAttributes}\n archetype={archetype}\n />\n ))}\n\n {/* Add hands at hand bones with animation state */}\n {bone.name === \"hand_L\" && leftHandState && (\n <Hand3D\n side=\"left\"\n pose={leftHandState.currentPose}\n fingerCurl={leftHandState.currentFingerCurl}\n distanceFromCamera={cameraDistance}\n wristRotation={leftHandState.currentWristRotation}\n isHighlighted={leftHandState.isHighlighted}\n highlightMode={leftHandState.highlightMode}\n skinColor={color}\n scale={1.0}\n />\n )}\n {bone.name === \"hand_R\" && rightHandState && (\n <Hand3D\n side=\"right\"\n pose={rightHandState.currentPose}\n fingerCurl={rightHandState.currentFingerCurl}\n distanceFromCamera={cameraDistance}\n wristRotation={rightHandState.currentWristRotation}\n isHighlighted={rightHandState.isHighlighted}\n highlightMode={rightHandState.highlightMode}\n skinColor={color}\n scale={1.0}\n />\n )}\n\n {/* Add feet at foot bones with archetype scaling */}\n {bone.name === \"foot_L\" && (\n <Foot3D\n side=\"left\"\n skinColor={color}\n scale={boneThicknessMultiplier}\n isHighlighted={false}\n />\n )}\n {bone.name === \"foot_R\" && (\n <Foot3D\n side=\"right\"\n skinColor={color}\n scale={boneThicknessMultiplier}\n isHighlighted={false}\n />\n )}\n\n {/* Add facial features at head bone */}\n {bone.name === \"head\" && enableFacialExpressions && facialExpression && (\n <Face3D\n expression={facialExpression}\n damage={facialDamage ?? DEFAULT_FACIAL_DAMAGE}\n opponentPosition={opponentPosition ?? new THREE.Vector3(5, 2, 0)}\n headRotation={bone.rotation.clone()}\n enableEyeTracking={enableEyeTracking}\n enableDamageVisuals={true}\n isMobile={cameraDistance > 15}\n skinColor={color}\n />\n )}\n </group>\n );\n};\n\n/**\n * BoneRenderer component\n *\n * Renders complete skeletal rig with recursive bone hierarchy.\n *\n * @example\n * ```tsx\n * const rig = createHumanoidRig();\n * <BoneRenderer rig={rig} color={0xFF6B6B} renderMode=\"solid\" />\n * ```\n *\n * @korean 뼈렌더러컴포넌트\n */\nexport const BoneRenderer: React.FC<BoneRendererProps> = ({\n rig,\n color = KOREAN_COLORS.ACCENT_RED,\n showBones = true,\n renderMode = \"solid\",\n leftHandState,\n rightHandState,\n cameraDistance = 10,\n facialExpression,\n facialDamage,\n opponentPosition,\n enableFacialExpressions = false, // Default false to avoid breaking existing tests\n enableEyeTracking = true,\n physicalAttributes,\n muscleStates,\n isExhausted = false,\n archetype,\n}) => {\n // Calculate bone thickness multiplier from physical attributes\n const boneThicknessMultiplier = useMemo(() => {\n if (!physicalAttributes) return 1.0;\n return calculateBoneThickness(\n physicalAttributes.muscleMass,\n physicalAttributes.fatMass,\n );\n }, [physicalAttributes]);\n\n if (!showBones) {\n return null;\n }\n\n return (\n <group name=\"bone-renderer\">\n <SingleBone\n bone={rig.root}\n color={color}\n renderMode={renderMode}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={cameraDistance}\n facialExpression={facialExpression}\n facialDamage={facialDamage}\n opponentPosition={opponentPosition}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n boneThicknessMultiplier={boneThicknessMultiplier}\n muscleStates={muscleStates}\n isExhausted={isExhausted}\n physicalAttributes={physicalAttributes}\n archetype={archetype}\n />\n </group>\n );\n};\n\nexport default BoneRenderer;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Face3D.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/Face3D.tsx"],"sourcesContent":["/**\n * Face3D component with realistic facial features\n *\n * Renders facial features with expressions, eye tracking, and damage visualization:\n * - Eyes with pupils that track opponent\n * - Mouth that opens/closes based on expression\n * - Nose geometry\n * - Damage effects (bruises, swelling, bleeding)\n *\n * @module components/three/Face3D\n * @category 3D Components\n * @korean 얼굴3D컴포넌트\n */\n\nimport React, { useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport {\n EYE_OPENNESS,\n MOUTH_OPENNESS,\n type EyeProps,\n type Face3DProps,\n type MouthProps,\n} from \"../../../../types/facial\";\nimport { mixColors } from \"../../../../utils/colorHelpers\";\n\n/**\n * Head position offset from bone center\n * Since Face3D is rendered inside the head bone group,\n * this is a small offset to position the face correctly\n * @korean 머리위치오프셋\n */\nconst HEAD_POSITION_OFFSET = 0.1;\n\n/**\n * Eye component with pupil tracking and swelling\n *\n * Renders eye with adjustable openness, pupil tracking, and swelling effects.\n *\n * @param props - Eye component props\n * @returns Eye 3D mesh group\n *\n * @korean 눈컴포넌트\n */\nconst Eye: React.FC<EyeProps> = ({\n position,\n expression,\n lookDirection,\n swelling,\n side,\n}) => {\n // Calculate eye openness based on expression\n const eyeOpenness = useMemo(() => {\n return EYE_OPENNESS[expression];\n }, [expression]);\n\n // Calculate pupil offset based on look direction\n const pupilOffset = useMemo(() => {\n // Normalize look direction\n const normalized = lookDirection.clone().normalize();\n\n // Convert to 2D offset (x, y in eye space)\n // Limit pupil movement to realistic range\n const maxOffset = 0.015;\n const x = normalized.x * maxOffset;\n const y = -normalized.y * maxOffset * 0.5; // Less vertical movement\n\n return new THREE.Vector3(x, y, 0.03);\n }, [lookDirection]);\n\n // Swelling color (purple bruise)\n const swellingColor = 0x663366;\n\n return (\n <group position={position} name={`eye-${side}`}>\n {/* Eye white (sclera) */}\n <mesh scale={[1, eyeOpenness, 1]}>\n <sphereGeometry args={[0.04, 8, 8]} />\n <meshPhysicalMaterial\n color={0xffffff}\n roughness={0.1}\n clearcoat={1.0}\n clearcoatRoughness={0.1}\n />\n </mesh>\n\n {/* Pupil (tracks opponent) */}\n {eyeOpenness > 0.1 && (\n <mesh position={pupilOffset}>\n <sphereGeometry args={[0.015, 8, 8]} />\n <meshPhysicalMaterial\n color={0x000000}\n roughness={0.2}\n clearcoat={0.8}\n />\n </mesh>\n )}\n\n {/* Swelling indicator */}\n {swelling > 0 && (\n <mesh scale={[1 + swelling * 0.5, 1 + swelling * 0.5, 1]}>\n <sphereGeometry args={[0.05, 8, 8]} />\n <meshStandardMaterial\n color={swellingColor}\n transparent\n opacity={swelling * 0.6}\n />\n </mesh>\n )}\n </group>\n );\n};\n\n/**\n * Mouth component with expression-based openness and bleeding\n *\n * Renders mouth that opens/closes based on expression.\n *\n * @param props - Mouth component props\n * @returns Mouth 3D mesh group\n *\n * @korean 입컴포넌트\n */\nconst Mouth: React.FC<MouthProps> = ({ position, expression, bleeding }) => {\n // Calculate mouth openness based on expression\n const mouthOpenness = useMemo(() => {\n return MOUTH_OPENNESS[expression];\n }, [expression]);\n\n // Blood color\n const bloodColor = KOREAN_COLORS.ACCENT_RED;\n\n return (\n <group position={position} name=\"mouth\">\n {/* Mouth line */}\n <mesh scale={[0.08, mouthOpenness * 0.04 + 0.005, 0.01]}>\n <boxGeometry args={[1, 1, 1]} />\n <meshPhysicalMaterial color={0x330000} roughness={0.5} />\n </mesh>\n\n {/* Blood effect */}\n {bleeding > 0 && (\n <>\n {/* Blood on lip */}\n <mesh position={[0, -0.01, 0]}>\n <sphereGeometry args={[0.015, 8, 8]} />\n <meshPhysicalMaterial\n color={bloodColor}\n roughness={0.2}\n clearcoat={1.0}\n transparent\n opacity={bleeding * 0.9}\n />\n </mesh>\n\n {/* Blood drip */}\n {bleeding > 0.5 && (\n <mesh position={[0, -0.03, 0]} scale={[0.5, 1, 0.5]}>\n <cylinderGeometry args={[0.005, 0.008, 0.04, 8]} />\n <meshPhysicalMaterial\n color={bloodColor}\n roughness={0.2}\n clearcoat={1.0}\n transparent\n opacity={bleeding * 0.7}\n />\n </mesh>\n )}\n </>\n )}\n </group>\n );\n};\n\n/**\n * Nose component\n *\n * Simple geometric nose.\n *\n * @param position - Nose position\n * @param skinColor - Skin tone color\n * @param bleeding - Bleeding intensity (0-1)\n * @returns Nose 3D mesh\n *\n * @korean 코컴포넌트\n */\nconst Nose: React.FC<{\n position: [number, number, number];\n skinColor: number;\n bleeding: number;\n}> = ({ position, skinColor, bleeding }) => {\n const bloodColor = KOREAN_COLORS.ACCENT_RED;\n\n // Memoize skin material for nose\n const noseMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: skinColor,\n roughness: 0.6,\n metalness: 0,\n clearcoat: 0.3,\n clearcoatRoughness: 0.6,\n // PBR skin properties\n transmission: 0,\n thickness: 0.05,\n ior: 1.4,\n sheen: 0.1,\n sheenRoughness: 0.8,\n // Subtle emissive\n emissive: new THREE.Color(0xff6040),\n emissiveIntensity: 0.02,\n }),\n [skinColor]\n );\n\n // Dispose nose material on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n noseMaterial.dispose();\n };\n }, [noseMaterial]);\n\n return (\n <group position={position} name=\"nose\">\n {/* Nose */}\n <mesh rotation={[Math.PI, 0, 0]}>\n <coneGeometry args={[0.03, 0.06, 8]} />\n <primitive object={noseMaterial} attach=\"material\" />\n </mesh>\n\n {/* Blood from nose */}\n {bleeding > 0 && (\n <mesh position={[0, -0.03, 0.01]}>\n <sphereGeometry args={[0.01, 6, 6]} />\n <meshBasicMaterial\n color={bloodColor}\n transparent\n opacity={bleeding * 0.8}\n />\n </mesh>\n )}\n </group>\n );\n};\n\n/**\n * Face3D component\n *\n * Main facial features component with head sphere, eyes, mouth, nose,\n * and damage visualization.\n *\n * @param props - Face3D props\n * @returns Face 3D mesh group\n *\n * @example\n * ```tsx\n * <Face3D\n * expression={FacialExpression.PAINED}\n * damage={damageState}\n * opponentPosition={new THREE.Vector3(5, 2, 0)}\n * headRotation={new THREE.Euler(0, 0, 0)}\n * enableEyeTracking={true}\n * />\n * ```\n *\n * @korean 얼굴3D\n */\nexport const Face3D: React.FC<Face3DProps> = ({\n expression,\n damage,\n opponentPosition,\n headRotation,\n enableEyeTracking = true,\n enableDamageVisuals = true,\n isMobile = false,\n skinColor = 0xffdbac, // Default skin tone\n}) => {\n // Calculate eye direction toward opponent\n const eyeDirection = useMemo(() => {\n if (!enableEyeTracking) {\n return new THREE.Vector3(0, 0, 1); // Look forward\n }\n\n // Validate opponent position - check for required THREE.Vector3 methods\n // This is more robust than instanceof check in test environments\n if (\n !opponentPosition ||\n typeof opponentPosition.x !== \"number\" ||\n typeof opponentPosition.y !== \"number\" ||\n typeof opponentPosition.z !== \"number\"\n ) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"Face3D: opponentPosition is not a valid THREE.Vector3; defaulting eye direction forward.\"\n );\n }\n return new THREE.Vector3(0, 0, 1);\n }\n\n // Calculate direction from head to opponent using component subtraction\n // to avoid prototype chain issues in test environments\n // Use the same offset as the face position\n const headPos = new THREE.Vector3(0, HEAD_POSITION_OFFSET, 0);\n const dir = new THREE.Vector3(\n opponentPosition.x - headPos.x,\n opponentPosition.y - headPos.y,\n opponentPosition.z - headPos.z\n );\n dir.normalize();\n\n return dir;\n }, [opponentPosition, enableEyeTracking]);\n\n // Calculate overall bruise color intensity\n const bruiseIntensity = useMemo(() => {\n if (!enableDamageVisuals) return 0;\n\n const avgBruise =\n (damage.leftCheekBruise +\n damage.rightCheekBruise +\n damage.foreheadBruise +\n damage.jawBruise) /\n 4;\n return avgBruise;\n }, [damage, enableDamageVisuals]);\n\n // Adjust head color for bruising using helper function\n const headColor = useMemo(() => {\n if (bruiseIntensity === 0) return skinColor;\n\n // Mix skin color with purple bruise color\n const bruiseColor = 0x663366;\n return mixColors(skinColor, bruiseColor, bruiseIntensity * 0.3);\n }, [skinColor, bruiseIntensity]);\n\n // NOTE: Damage visuals are currently handled via headColor bruise interpolation.\n // In a future implementation, this could be replaced with a canvas-based texture.\n const damageTexture = null;\n\n // Memoize head material to avoid recreating on every render\n const headMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: headColor,\n map: damageTexture,\n roughness: 0.6,\n metalness: 0,\n clearcoat: 0.3,\n clearcoatRoughness: 0.6,\n envMapIntensity: 0.5,\n // PBR skin properties\n transmission: 0,\n thickness: 0.1,\n ior: 1.4, // Index of refraction for skin\n sheen: 0.15, // Facial skin has more sheen\n sheenRoughness: 0.7,\n // Subtle emissive for alive appearance\n emissive: new THREE.Color(0xff6040),\n emissiveIntensity: 0.02,\n }),\n [headColor, damageTexture]\n );\n\n // Memoize ear material to avoid recreating on every render\n const earMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: headColor,\n roughness: 0.6,\n metalness: 0,\n clearcoat: 0.3,\n clearcoatRoughness: 0.6,\n // PBR skin properties\n transmission: 0,\n thickness: 0.05,\n ior: 1.4,\n sheen: 0.1,\n sheenRoughness: 0.8,\n // Subtle emissive\n emissive: new THREE.Color(0xff6040),\n emissiveIntensity: 0.02,\n }),\n [headColor]\n );\n\n // Dispose materials on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n headMaterial.dispose();\n earMaterial.dispose();\n };\n }, [headMaterial, earMaterial]);\n\n return (\n <group\n position={[0, HEAD_POSITION_OFFSET, 0]}\n rotation={headRotation}\n name=\"face3d\"\n >\n {/* Head sphere */}\n <mesh>\n <sphereGeometry args={[0.2, isMobile ? 12 : 16, isMobile ? 12 : 16]} />\n <primitive object={headMaterial} attach=\"material\" />\n </mesh>\n\n {/* Left eye */}\n <Eye\n position={[-0.08, 0.05, 0.15]}\n expression={expression}\n lookDirection={eyeDirection}\n swelling={enableDamageVisuals ? damage.leftEyeSwelling : 0}\n side=\"left\"\n />\n\n {/* Right eye */}\n <Eye\n position={[0.08, 0.05, 0.15]}\n expression={expression}\n lookDirection={eyeDirection}\n swelling={enableDamageVisuals ? damage.rightEyeSwelling : 0}\n side=\"right\"\n />\n\n {/* Mouth */}\n <Mouth\n position={[0, -0.05, 0.15]}\n expression={expression}\n bleeding={enableDamageVisuals ? damage.mouthBleeding : 0}\n />\n\n {/* Nose */}\n <Nose\n position={[0, 0.02, 0.18]}\n skinColor={skinColor}\n bleeding={enableDamageVisuals ? damage.noseBleeding : 0}\n />\n\n {/* Ears (simple geometric shapes) */}\n {!isMobile && (\n <>\n {/* Left ear */}\n <mesh position={[-0.2, 0, 0]} rotation={[0, 0, Math.PI / 6]}>\n <sphereGeometry args={[0.04, 8, 8]} />\n <primitive object={earMaterial} attach=\"material\" />\n </mesh>\n\n {/* Right ear */}\n <mesh position={[0.2, 0, 0]} rotation={[0, 0, -Math.PI / 6]}>\n <sphereGeometry args={[0.04, 8, 8]} />\n <primitive object={earMaterial} attach=\"material\" />\n </mesh>\n </>\n )}\n </group>\n );\n};\n\nexport default Face3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Face3D.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/Face3D.tsx"],"sourcesContent":["/**\n * Face3D component with realistic facial features\n *\n * Renders facial features with expressions, eye tracking, and damage visualization:\n * - Eyes with pupils that track opponent\n * - Mouth that opens/closes based on expression\n * - Nose geometry\n * - Damage effects (bruises, swelling, bleeding)\n *\n * @module components/three/Face3D\n * @category 3D Components\n * @korean 얼굴3D컴포넌트\n */\n\nimport React, { useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport {\n EYE_OPENNESS,\n MOUTH_OPENNESS,\n type EyeProps,\n type Face3DProps,\n type MouthProps,\n} from \"../../../../types/facial\";\nimport { mixColors } from \"../../../../utils/colorHelpers\";\n\n/**\n * Head position offset from bone center\n * Since Face3D is rendered inside the head bone group,\n * this is a small offset to position the face correctly\n * @korean 머리위치오프셋\n */\nconst HEAD_POSITION_OFFSET = 0.1;\n\n/**\n * Eye component with pupil tracking and swelling\n *\n * Renders eye with adjustable openness, pupil tracking, and swelling effects.\n *\n * @param props - Eye component props\n * @returns Eye 3D mesh group\n *\n * @korean 눈컴포넌트\n */\nconst Eye: React.FC<EyeProps> = ({\n position,\n expression,\n lookDirection,\n swelling,\n side,\n}) => {\n // Calculate eye openness based on expression\n const eyeOpenness = useMemo(() => {\n return EYE_OPENNESS[expression];\n }, [expression]);\n\n // Calculate pupil offset based on look direction\n const pupilOffset = useMemo(() => {\n // Normalize look direction\n const normalized = lookDirection.clone().normalize();\n\n // Convert to 2D offset (x, y in eye space)\n // Limit pupil movement to realistic range\n const maxOffset = 0.015;\n const x = normalized.x * maxOffset;\n const y = -normalized.y * maxOffset * 0.5; // Less vertical movement\n\n return new THREE.Vector3(x, y, 0.03);\n }, [lookDirection]);\n\n // Swelling color (purple bruise)\n const swellingColor = 0x663366;\n\n return (\n <group position={position} name={`eye-${side}`}>\n {/* Eye white (sclera) */}\n <mesh scale={[1, eyeOpenness, 1]}>\n <sphereGeometry args={[0.04, 8, 8]} />\n <meshPhysicalMaterial\n color={0xffffff}\n roughness={0.1}\n clearcoat={1.0}\n clearcoatRoughness={0.1}\n />\n </mesh>\n\n {/* Pupil (tracks opponent) */}\n {eyeOpenness > 0.1 && (\n <mesh position={pupilOffset}>\n <sphereGeometry args={[0.015, 8, 8]} />\n <meshPhysicalMaterial\n color={0x000000}\n roughness={0.2}\n clearcoat={0.8}\n />\n </mesh>\n )}\n\n {/* Swelling indicator */}\n {swelling > 0 && (\n <mesh scale={[1 + swelling * 0.5, 1 + swelling * 0.5, 1]}>\n <sphereGeometry args={[0.05, 8, 8]} />\n <meshStandardMaterial\n color={swellingColor}\n transparent\n opacity={swelling * 0.6}\n />\n </mesh>\n )}\n </group>\n );\n};\n\n/**\n * Mouth component with expression-based openness and bleeding\n *\n * Renders mouth that opens/closes based on expression.\n *\n * @param props - Mouth component props\n * @returns Mouth 3D mesh group\n *\n * @korean 입컴포넌트\n */\nconst Mouth: React.FC<MouthProps> = ({ position, expression, bleeding }) => {\n // Calculate mouth openness based on expression\n const mouthOpenness = useMemo(() => {\n return MOUTH_OPENNESS[expression];\n }, [expression]);\n\n // Blood color\n const bloodColor = KOREAN_COLORS.ACCENT_RED;\n\n return (\n <group position={position} name=\"mouth\">\n {/* Mouth line */}\n <mesh scale={[0.08, mouthOpenness * 0.04 + 0.005, 0.01]}>\n <boxGeometry args={[1, 1, 1]} />\n <meshPhysicalMaterial color={0x330000} roughness={0.5} />\n </mesh>\n\n {/* Blood effect */}\n {bleeding > 0 && (\n <>\n {/* Blood on lip */}\n <mesh position={[0, -0.01, 0]}>\n <sphereGeometry args={[0.015, 8, 8]} />\n <meshPhysicalMaterial\n color={bloodColor}\n roughness={0.2}\n clearcoat={1.0}\n transparent\n opacity={bleeding * 0.9}\n />\n </mesh>\n\n {/* Blood drip */}\n {bleeding > 0.5 && (\n <mesh position={[0, -0.03, 0]} scale={[0.5, 1, 0.5]}>\n <cylinderGeometry args={[0.005, 0.008, 0.04, 8]} />\n <meshPhysicalMaterial\n color={bloodColor}\n roughness={0.2}\n clearcoat={1.0}\n transparent\n opacity={bleeding * 0.7}\n />\n </mesh>\n )}\n </>\n )}\n </group>\n );\n};\n\n/**\n * Nose component\n *\n * Simple geometric nose.\n *\n * @param position - Nose position\n * @param skinColor - Skin tone color\n * @param bleeding - Bleeding intensity (0-1)\n * @returns Nose 3D mesh\n *\n * @korean 코컴포넌트\n */\nconst Nose: React.FC<{\n position: [number, number, number];\n skinColor: number;\n bleeding: number;\n}> = ({ position, skinColor, bleeding }) => {\n const bloodColor = KOREAN_COLORS.ACCENT_RED;\n\n // Memoize skin material for nose\n const noseMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: skinColor,\n roughness: 0.6,\n metalness: 0,\n clearcoat: 0.3,\n clearcoatRoughness: 0.6,\n // PBR skin properties\n transmission: 0,\n thickness: 0.05,\n ior: 1.4,\n sheen: 0.1,\n sheenRoughness: 0.8,\n // Subtle emissive\n emissive: new THREE.Color(0xff6040),\n emissiveIntensity: 0.02,\n }),\n [skinColor]\n );\n\n // Dispose nose material on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n noseMaterial.dispose();\n };\n }, [noseMaterial]);\n\n return (\n <group position={position} name=\"nose\">\n {/* Nose */}\n <mesh rotation={[Math.PI, 0, 0]}>\n <coneGeometry args={[0.03, 0.06, 8]} />\n <primitive object={noseMaterial} attach=\"material\" />\n </mesh>\n\n {/* Blood from nose */}\n {bleeding > 0 && (\n <mesh position={[0, -0.03, 0.01]}>\n <sphereGeometry args={[0.01, 6, 6]} />\n <meshBasicMaterial\n color={bloodColor}\n transparent\n opacity={bleeding * 0.8}\n />\n </mesh>\n )}\n </group>\n );\n};\n\n/**\n * Face3D component\n *\n * Main facial features component with head sphere, eyes, mouth, nose,\n * and damage visualization.\n *\n * @param props - Face3D props\n * @returns Face 3D mesh group\n *\n * @example\n * ```tsx\n * <Face3D\n * expression={FacialExpression.PAINED}\n * damage={damageState}\n * opponentPosition={new THREE.Vector3(5, 2, 0)}\n * headRotation={new THREE.Euler(0, 0, 0)}\n * enableEyeTracking={true}\n * />\n * ```\n *\n * @korean 얼굴3D\n */\nexport const Face3D: React.FC<Face3DProps> = ({\n expression,\n damage,\n opponentPosition,\n headRotation,\n enableEyeTracking = true,\n enableDamageVisuals = true,\n isMobile = false,\n skinColor = 0xffdbac, // Default skin tone\n}) => {\n // Calculate eye direction toward opponent\n const eyeDirection = useMemo(() => {\n if (!enableEyeTracking) {\n return new THREE.Vector3(0, 0, 1); // Look forward\n }\n\n // Validate opponent position - check for required THREE.Vector3 methods\n // This is more robust than instanceof check in test environments\n if (\n !opponentPosition ||\n typeof opponentPosition.x !== \"number\" ||\n typeof opponentPosition.y !== \"number\" ||\n typeof opponentPosition.z !== \"number\"\n ) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"Face3D: opponentPosition is not a valid THREE.Vector3; defaulting eye direction forward.\"\n );\n }\n return new THREE.Vector3(0, 0, 1);\n }\n\n // Calculate direction from head to opponent using component subtraction\n // to avoid prototype chain issues in test environments\n // Use the same offset as the face position\n const headPos = new THREE.Vector3(0, HEAD_POSITION_OFFSET, 0);\n const dir = new THREE.Vector3(\n opponentPosition.x - headPos.x,\n opponentPosition.y - headPos.y,\n opponentPosition.z - headPos.z\n );\n dir.normalize();\n\n return dir;\n }, [opponentPosition, enableEyeTracking]);\n\n // Calculate overall bruise color intensity\n const bruiseIntensity = useMemo(() => {\n if (!enableDamageVisuals) return 0;\n\n const avgBruise =\n (damage.leftCheekBruise +\n damage.rightCheekBruise +\n damage.foreheadBruise +\n damage.jawBruise) /\n 4;\n return avgBruise;\n }, [damage, enableDamageVisuals]);\n\n // Adjust head color for bruising using helper function\n const headColor = useMemo(() => {\n if (bruiseIntensity === 0) return skinColor;\n\n // Mix skin color with purple bruise color\n const bruiseColor = 0x663366;\n return mixColors(skinColor, bruiseColor, bruiseIntensity * 0.3);\n }, [skinColor, bruiseIntensity]);\n\n // NOTE: Damage visuals are currently handled via headColor bruise interpolation.\n // In a future implementation, this could be replaced with a canvas-based texture.\n const damageTexture = null;\n\n // Memoize head material to avoid recreating on every render\n const headMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: headColor,\n map: damageTexture,\n roughness: 0.6,\n metalness: 0,\n clearcoat: 0.3,\n clearcoatRoughness: 0.6,\n envMapIntensity: 0.5,\n // PBR skin properties\n transmission: 0,\n thickness: 0.1,\n ior: 1.4, // Index of refraction for skin\n sheen: 0.15, // Facial skin has more sheen\n sheenRoughness: 0.7,\n // Subtle emissive for alive appearance\n emissive: new THREE.Color(0xff6040),\n emissiveIntensity: 0.02,\n }),\n [headColor, damageTexture]\n );\n\n // Memoize ear material to avoid recreating on every render\n const earMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: headColor,\n roughness: 0.6,\n metalness: 0,\n clearcoat: 0.3,\n clearcoatRoughness: 0.6,\n // PBR skin properties\n transmission: 0,\n thickness: 0.05,\n ior: 1.4,\n sheen: 0.1,\n sheenRoughness: 0.8,\n // Subtle emissive\n emissive: new THREE.Color(0xff6040),\n emissiveIntensity: 0.02,\n }),\n [headColor]\n );\n\n // Dispose materials on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n headMaterial.dispose();\n earMaterial.dispose();\n };\n }, [headMaterial, earMaterial]);\n\n return (\n <group\n position={[0, HEAD_POSITION_OFFSET, 0]}\n rotation={headRotation}\n name=\"face3d\"\n >\n {/* Head sphere */}\n <mesh>\n <sphereGeometry args={[0.2, isMobile ? 12 : 16, isMobile ? 12 : 16]} />\n <primitive object={headMaterial} attach=\"material\" />\n </mesh>\n\n {/* Left eye */}\n <Eye\n position={[-0.08, 0.05, 0.15]}\n expression={expression}\n lookDirection={eyeDirection}\n swelling={enableDamageVisuals ? damage.leftEyeSwelling : 0}\n side=\"left\"\n />\n\n {/* Right eye */}\n <Eye\n position={[0.08, 0.05, 0.15]}\n expression={expression}\n lookDirection={eyeDirection}\n swelling={enableDamageVisuals ? damage.rightEyeSwelling : 0}\n side=\"right\"\n />\n\n {/* Mouth */}\n <Mouth\n position={[0, -0.05, 0.15]}\n expression={expression}\n bleeding={enableDamageVisuals ? damage.mouthBleeding : 0}\n />\n\n {/* Nose */}\n <Nose\n position={[0, 0.02, 0.18]}\n skinColor={skinColor}\n bleeding={enableDamageVisuals ? damage.noseBleeding : 0}\n />\n\n {/* Ears (simple geometric shapes) */}\n {!isMobile && (\n <>\n {/* Left ear */}\n <mesh position={[-0.2, 0, 0]} rotation={[0, 0, Math.PI / 6]}>\n <sphereGeometry args={[0.04, 8, 8]} />\n <primitive object={earMaterial} attach=\"material\" />\n </mesh>\n\n {/* Right ear */}\n <mesh position={[0.2, 0, 0]} rotation={[0, 0, -Math.PI / 6]}>\n <sphereGeometry args={[0.04, 8, 8]} />\n <primitive object={earMaterial} attach=\"material\" />\n </mesh>\n </>\n )}\n </group>\n );\n};\n\nexport default Face3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Foot3D.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/Foot3D.tsx"],"sourcesContent":["/**\n * Foot3D component with anatomically accurate foot geometry\n *\n * Renders detailed 3D foot with proper dimensions for martial arts stances\n * and kicks. Supports left/right feet with Korean skin tone coloring.\n *\n * Implements anatomically correct foot proportions:\n * - Length: ~26-29cm (varies by archetype)\n * - Width: ~10cm at widest point\n * - Height: ~8cm at ankle\n *\n * @module components/three/Foot3D\n * @category 3D Components\n * @korean 발3D컴포넌트\n */\n\nimport React, { useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Props for Foot3D component\n *\n * @public\n * @korean 발3D속성\n */\nexport interface Foot3DProps {\n /**\n * Foot side (left or right)\n * @korean 발쪽\n */\n readonly side: \"left\" | \"right\";\n\n /**\n * Base skin color\n * @korean 피부색\n */\n readonly skinColor?: number;\n\n /**\n * Scale multiplier (based on archetype physical attributes)\n * @korean 크기배율\n */\n readonly scale?: number;\n\n /**\n * Whether foot is highlighted (e.g., during kicks)\n * @korean 표시여부\n */\n readonly isHighlighted?: boolean;\n}\n\n/**\n * Foot3D Component\n *\n * Complete foot geometry with anatomically correct dimensions suitable\n * for Korean martial arts stance visualization and kick animations.\n *\n * Design notes:\n * - Main foot body is box-shaped with rounded edges\n * - Toe area is slightly elevated and separated\n * - Heel is wider than toe area for stability\n * - Dimensions scale with archetype (Amsalja: smaller, Jojik: larger)\n *\n * @example\n * ```tsx\n * <Foot3D\n * side=\"right\"\n * skinColor={0xffdbac}\n * scale={1.0}\n * isHighlighted={false}\n * />\n * ```\n *\n * @korean 발3D컴포넌트\n */\nexport const Foot3D: React.FC<Foot3DProps> = ({\n side,\n skinColor = 0xffdbac,\n scale = 1.0,\n isHighlighted = false,\n}) => {\n // Anatomically correct foot dimensions for average male (180cm height)\n // These scale with archetype physical attributes\n const footDimensions = useMemo(() => {\n // Average male foot: 26-29cm length, ~10cm width, ~8cm height\n const footLength = 0.26 * scale; // 26cm base length\n const footWidth = 0.1 * scale; // 10cm width\n const footHeight = 0.08 * scale; // 8cm height at ankle\n\n // Toe area dimensions (front 30% of foot)\n const toeLength = footLength * 0.3;\n const toeWidth = footWidth * 0.9; // Slightly narrower than heel\n const toeHeight = footHeight * 0.6; // Lower profile\n\n // Heel area dimensions (back 70% of foot)\n const heelLength = footLength * 0.7;\n\n return {\n footLength,\n footWidth,\n footHeight,\n toeLength,\n toeWidth,\n toeHeight,\n heelLength,\n };\n }, [scale]);\n\n // Foot color (highlight during kicks with brighter color)\n const footColor = useMemo(() => {\n if (isHighlighted) {\n return KOREAN_COLORS.ACCENT_GOLD;\n }\n return skinColor;\n }, [isHighlighted, skinColor]);\n\n // Memoize shared skin material for all foot parts\n const skinMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: footColor,\n metalness: 0,\n roughness: 0.8,\n clearcoat: 0.3,\n clearcoatRoughness: 0.5,\n // PBR skin properties\n transmission: 0,\n thickness: 0.1,\n ior: 1.4, // Index of refraction for skin\n sheen: 0.1, // Subtle skin sheen\n sheenRoughness: 0.8,\n // Subtle emissive for alive appearance\n emissive: new THREE.Color(footColor),\n emissiveIntensity: isHighlighted ? 0.3 : 0.02,\n }),\n [footColor, isHighlighted],\n );\n\n // Dispose skin material on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n skinMaterial.dispose();\n };\n }, [skinMaterial]);\n\n return (\n <group name={`foot-3d-${side}`}>\n {/* Ankle connector - larger sphere that bridges shin body surface to foot */}\n <mesh\n position={[0, 0, 0]}\n castShadow\n receiveShadow\n name={`foot-ankle-bridge-${side}`}\n >\n <sphereGeometry args={[footDimensions.footHeight * 0.6, 10, 10]} />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n\n {/* Ankle-to-heel transition cylinder */}\n <mesh\n position={[\n 0,\n -footDimensions.footHeight * 0.2,\n footDimensions.heelLength * 0.1,\n ]}\n castShadow\n receiveShadow\n name={`foot-ankle-transition-${side}`}\n >\n <cylinderGeometry\n args={[\n footDimensions.footHeight * 0.5, // Top radius matches ankle sphere\n footDimensions.footWidth * 0.45, // Bottom matches heel width\n footDimensions.footHeight * 0.4,\n 8,\n ]}\n />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n\n {/* Main heel/midfoot body - capsule for smooth organic shape */}\n <mesh\n position={[\n 0,\n -footDimensions.footHeight * 0.4,\n footDimensions.heelLength * 0.2,\n ]}\n rotation={[Math.PI / 2, 0, 0]}\n castShadow\n receiveShadow\n name={`foot-heel-${side}`}\n >\n <capsuleGeometry\n args={[\n footDimensions.footWidth * 0.45, // Radius\n footDimensions.heelLength * 0.5, // Length (shorter than full heel - caps add length)\n 4,\n 8,\n ]}\n />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n\n {/* Toe area - capsule for rounded toe shape */}\n <mesh\n position={[\n 0,\n -footDimensions.footHeight * 0.35 + footDimensions.toeHeight * 0.2,\n footDimensions.heelLength * 0.7 + footDimensions.toeLength / 2,\n ]}\n rotation={[Math.PI / 2, 0, 0]}\n castShadow\n receiveShadow\n name={`foot-toes-${side}`}\n >\n <capsuleGeometry\n args={[\n footDimensions.toeWidth * 0.35, // Narrower radius for tapered toes\n footDimensions.toeLength * 0.4, // Shorter length\n 4,\n 8,\n ]}\n />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n </group>\n );\n};\n\nexport default Foot3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,IAAa,UAAiC,EAC5C,MACA,YAAY,UACZ,QAAQ,GACR,gBAAgB,YACZ;CAGJ,MAAM,iBAAiB,cAAc;EAEnC,MAAM,aAAa,MAAO;EAC1B,MAAM,YAAY,KAAM;EACxB,MAAM,aAAa,MAAO;
|
|
1
|
+
{"version":3,"file":"Foot3D.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/Foot3D.tsx"],"sourcesContent":["/**\n * Foot3D component with anatomically accurate foot geometry\n *\n * Renders detailed 3D foot with proper dimensions for martial arts stances\n * and kicks. Supports left/right feet with Korean skin tone coloring.\n *\n * Implements anatomically correct foot proportions:\n * - Length: ~26-29cm (varies by archetype)\n * - Width: ~10cm at widest point\n * - Height: ~8cm at ankle\n *\n * @module components/three/Foot3D\n * @category 3D Components\n * @korean 발3D컴포넌트\n */\n\nimport React, { useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Props for Foot3D component\n *\n * @public\n * @korean 발3D속성\n */\nexport interface Foot3DProps {\n /**\n * Foot side (left or right)\n * @korean 발쪽\n */\n readonly side: \"left\" | \"right\";\n\n /**\n * Base skin color\n * @korean 피부색\n */\n readonly skinColor?: number;\n\n /**\n * Scale multiplier (based on archetype physical attributes)\n * @korean 크기배율\n */\n readonly scale?: number;\n\n /**\n * Whether foot is highlighted (e.g., during kicks)\n * @korean 표시여부\n */\n readonly isHighlighted?: boolean;\n}\n\n/**\n * Foot3D Component\n *\n * Complete foot geometry with anatomically correct dimensions suitable\n * for Korean martial arts stance visualization and kick animations.\n *\n * Design notes:\n * - Main foot body is box-shaped with rounded edges\n * - Toe area is slightly elevated and separated\n * - Heel is wider than toe area for stability\n * - Dimensions scale with archetype (Amsalja: smaller, Jojik: larger)\n *\n * @example\n * ```tsx\n * <Foot3D\n * side=\"right\"\n * skinColor={0xffdbac}\n * scale={1.0}\n * isHighlighted={false}\n * />\n * ```\n *\n * @korean 발3D컴포넌트\n */\nexport const Foot3D: React.FC<Foot3DProps> = ({\n side,\n skinColor = 0xffdbac,\n scale = 1.0,\n isHighlighted = false,\n}) => {\n // Anatomically correct foot dimensions for average male (180cm height)\n // These scale with archetype physical attributes\n const footDimensions = useMemo(() => {\n // Average male foot: 26-29cm length, ~10cm width, ~8cm height\n const footLength = 0.26 * scale; // 26cm base length\n const footWidth = 0.1 * scale; // 10cm width\n const footHeight = 0.08 * scale; // 8cm height at ankle\n\n // Toe area dimensions (front 30% of foot)\n const toeLength = footLength * 0.3;\n const toeWidth = footWidth * 0.9; // Slightly narrower than heel\n const toeHeight = footHeight * 0.6; // Lower profile\n\n // Heel area dimensions (back 70% of foot)\n const heelLength = footLength * 0.7;\n\n return {\n footLength,\n footWidth,\n footHeight,\n toeLength,\n toeWidth,\n toeHeight,\n heelLength,\n };\n }, [scale]);\n\n // Foot color (highlight during kicks with brighter color)\n const footColor = useMemo(() => {\n if (isHighlighted) {\n return KOREAN_COLORS.ACCENT_GOLD;\n }\n return skinColor;\n }, [isHighlighted, skinColor]);\n\n // Memoize shared skin material for all foot parts\n const skinMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: footColor,\n metalness: 0,\n roughness: 0.8,\n clearcoat: 0.3,\n clearcoatRoughness: 0.5,\n // PBR skin properties\n transmission: 0,\n thickness: 0.1,\n ior: 1.4, // Index of refraction for skin\n sheen: 0.1, // Subtle skin sheen\n sheenRoughness: 0.8,\n // Subtle emissive for alive appearance\n emissive: new THREE.Color(footColor),\n emissiveIntensity: isHighlighted ? 0.3 : 0.02,\n }),\n [footColor, isHighlighted],\n );\n\n // Dispose skin material on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n skinMaterial.dispose();\n };\n }, [skinMaterial]);\n\n return (\n <group name={`foot-3d-${side}`}>\n {/* Ankle connector - larger sphere that bridges shin body surface to foot */}\n <mesh\n position={[0, 0, 0]}\n castShadow\n receiveShadow\n name={`foot-ankle-bridge-${side}`}\n >\n <sphereGeometry args={[footDimensions.footHeight * 0.6, 10, 10]} />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n\n {/* Ankle-to-heel transition cylinder */}\n <mesh\n position={[\n 0,\n -footDimensions.footHeight * 0.2,\n footDimensions.heelLength * 0.1,\n ]}\n castShadow\n receiveShadow\n name={`foot-ankle-transition-${side}`}\n >\n <cylinderGeometry\n args={[\n footDimensions.footHeight * 0.5, // Top radius matches ankle sphere\n footDimensions.footWidth * 0.45, // Bottom matches heel width\n footDimensions.footHeight * 0.4,\n 8,\n ]}\n />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n\n {/* Main heel/midfoot body - capsule for smooth organic shape */}\n <mesh\n position={[\n 0,\n -footDimensions.footHeight * 0.4,\n footDimensions.heelLength * 0.2,\n ]}\n rotation={[Math.PI / 2, 0, 0]}\n castShadow\n receiveShadow\n name={`foot-heel-${side}`}\n >\n <capsuleGeometry\n args={[\n footDimensions.footWidth * 0.45, // Radius\n footDimensions.heelLength * 0.5, // Length (shorter than full heel - caps add length)\n 4,\n 8,\n ]}\n />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n\n {/* Toe area - capsule for rounded toe shape */}\n <mesh\n position={[\n 0,\n -footDimensions.footHeight * 0.35 + footDimensions.toeHeight * 0.2,\n footDimensions.heelLength * 0.7 + footDimensions.toeLength / 2,\n ]}\n rotation={[Math.PI / 2, 0, 0]}\n castShadow\n receiveShadow\n name={`foot-toes-${side}`}\n >\n <capsuleGeometry\n args={[\n footDimensions.toeWidth * 0.35, // Narrower radius for tapered toes\n footDimensions.toeLength * 0.4, // Shorter length\n 4,\n 8,\n ]}\n />\n <primitive object={skinMaterial} attach=\"material\" />\n </mesh>\n </group>\n );\n};\n\nexport default Foot3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,IAAa,UAAiC,EAC5C,MACA,YAAY,UACZ,QAAQ,GACR,gBAAgB,YACZ;CAGJ,MAAM,iBAAiB,cAAc;EAEnC,MAAM,aAAa,MAAO;EAC1B,MAAM,YAAY,KAAM;EACxB,MAAM,aAAa,MAAO;EAU1B,OAAO;GACL;GACA;GACA;GACA,WAXgB,aAAa;GAY7B,UAXe,YAAY;GAY3B,WAXgB,aAAa;GAY7B,YATiB,aAAa;GAU/B;IACA,CAAC,MAAM,CAAC;CAGX,MAAM,YAAY,cAAc;EAC9B,IAAI,eACF,OAAO,cAAc;EAEvB,OAAO;IACN,CAAC,eAAe,UAAU,CAAC;CAG9B,MAAM,eAAe,cAEjB,IAAI,MAAM,qBAAqB;EAC7B,OAAO;EACP,WAAW;EACX,WAAW;EACX,WAAW;EACX,oBAAoB;EAEpB,cAAc;EACd,WAAW;EACX,KAAK;EACL,OAAO;EACP,gBAAgB;EAEhB,UAAU,IAAI,MAAM,MAAM,UAAU;EACpC,mBAAmB,gBAAgB,KAAM;EAC1C,CAAC,EACJ,CAAC,WAAW,cAAc,CAC3B;CAGD,gBAAgB;EACd,aAAa;GACX,aAAa,SAAS;;IAEvB,CAAC,aAAa,CAAC;CAElB,OACE,qBAAC,SAAD;EAAO,MAAM,WAAW;YAAxB;GAEE,qBAAC,QAAD;IACE,UAAU;KAAC;KAAG;KAAG;KAAE;IACnB,YAAA;IACA,eAAA;IACA,MAAM,qBAAqB;cAJ7B,CAME,oBAAC,kBAAD,EAAgB,MAAM;KAAC,eAAe,aAAa;KAAK;KAAI;KAAG,EAAI,CAAA,EACnE,oBAAC,aAAD;KAAW,QAAQ;KAAc,QAAO;KAAa,CAAA,CAChD;;GAGP,qBAAC,QAAD;IACE,UAAU;KACR;KACA,CAAC,eAAe,aAAa;KAC7B,eAAe,aAAa;KAC7B;IACD,YAAA;IACA,eAAA;IACA,MAAM,yBAAyB;cARjC,CAUE,oBAAC,oBAAD,EACE,MAAM;KACJ,eAAe,aAAa;KAC5B,eAAe,YAAY;KAC3B,eAAe,aAAa;KAC5B;KACD,EACD,CAAA,EACF,oBAAC,aAAD;KAAW,QAAQ;KAAc,QAAO;KAAa,CAAA,CAChD;;GAGP,qBAAC,QAAD;IACE,UAAU;KACR;KACA,CAAC,eAAe,aAAa;KAC7B,eAAe,aAAa;KAC7B;IACD,UAAU;KAAC,KAAK,KAAK;KAAG;KAAG;KAAE;IAC7B,YAAA;IACA,eAAA;IACA,MAAM,aAAa;cATrB,CAWE,oBAAC,mBAAD,EACE,MAAM;KACJ,eAAe,YAAY;KAC3B,eAAe,aAAa;KAC5B;KACA;KACD,EACD,CAAA,EACF,oBAAC,aAAD;KAAW,QAAQ;KAAc,QAAO;KAAa,CAAA,CAChD;;GAGP,qBAAC,QAAD;IACE,UAAU;KACR;KACA,CAAC,eAAe,aAAa,MAAO,eAAe,YAAY;KAC/D,eAAe,aAAa,KAAM,eAAe,YAAY;KAC9D;IACD,UAAU;KAAC,KAAK,KAAK;KAAG;KAAG;KAAE;IAC7B,YAAA;IACA,eAAA;IACA,MAAM,aAAa;cATrB,CAWE,oBAAC,mBAAD,EACE,MAAM;KACJ,eAAe,WAAW;KAC1B,eAAe,YAAY;KAC3B;KACA;KACD,EACD,CAAA,EACF,oBAAC,aAAD;KAAW,QAAQ;KAAc,QAAO;KAAa,CAAA,CAChD;;GACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Hand3D.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/Hand3D.tsx"],"sourcesContent":["/**\n * Hand3D component with finger geometry for martial arts techniques\n *\n * Renders detailed hand with palm and 5 fingers, supporting multiple\n * Korean martial arts hand poses (Fist, Knife-hand, Spear-hand, etc.).\n *\n * Implements LOD (Level of Detail) for performance optimization:\n * - High detail (<5 units): Full finger bones (4 segments per finger)\n * - Medium detail (5-15 units): Simplified fingers (3 segments)\n * - Low detail (>15 units): No finger detail (hand as single unit)\n *\n * @module components/three/Hand3D\n * @category 3D Components\n * @korean 손3D컴포넌트\n */\n\nimport React, { useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport type {\n FingerCurl,\n HandLODConfig,\n HandPoseType,\n HandSide,\n} from \"../../../../types/hand-animation\";\n\n/**\n * Props for Hand3D component\n *\n * @public\n * @korean 손3D속성\n */\nexport interface Hand3DProps {\n /**\n * Hand side (left or right)\n * @korean 손쪽\n */\n readonly side: HandSide;\n\n /**\n * Current hand pose\n * @korean 현재손자세\n */\n readonly pose: HandPoseType;\n\n /**\n * Finger curl amounts (0-1 per finger)\n * @korean 손가락구부림\n */\n readonly fingerCurl: FingerCurl;\n\n /**\n * Distance from camera for LOD\n * @korean 카메라거리\n */\n readonly distanceFromCamera: number;\n\n /**\n * Wrist rotation\n * @korean 손목회전\n */\n readonly wristRotation: THREE.Euler;\n\n /**\n * Whether hand is highlighted for vital point targeting\n * @korean 표시여부\n */\n readonly isHighlighted?: boolean;\n\n /**\n * Highlight mode for striking surfaces\n * @korean 표시모드\n */\n readonly highlightMode?:\n | \"none\"\n | \"knuckles\"\n | \"palm\"\n | \"knife_edge\"\n | \"fingertips\"\n | null;\n\n /**\n * Base skin color\n * @korean 피부색\n */\n readonly skinColor?: number;\n\n /**\n * Scale multiplier\n * @korean 크기배율\n */\n readonly scale?: number;\n}\n\n/**\n * Determine LOD config based on camera distance\n *\n * @param distance - Distance from camera\n * @returns LOD configuration\n * @korean LOD설정결정\n */\nconst getLODConfig = (distance: number): HandLODConfig => {\n if (distance < 5) {\n return {\n detailLevel: \"high\",\n distanceThresholds: { high: 5, medium: 15, low: 100 },\n renderFingers: true,\n fingerSegments: 4,\n };\n } else if (distance < 15) {\n return {\n detailLevel: \"medium\",\n distanceThresholds: { high: 5, medium: 15, low: 100 },\n renderFingers: true,\n fingerSegments: 3,\n };\n } else {\n return {\n detailLevel: \"low\",\n distanceThresholds: { high: 5, medium: 15, low: 100 },\n renderFingers: false,\n fingerSegments: 0,\n };\n }\n};\n\n/**\n * Finger segment component\n *\n * Renders a single finger segment (proximal, intermediate, or distal phalanx).\n *\n * @korean 손가락세그먼트컴포넌트\n */\ninterface FingerSegmentProps {\n readonly position: [number, number, number];\n readonly rotation: [number, number, number];\n readonly length: number;\n readonly radius: number;\n readonly color: number;\n}\n\nconst FingerSegment: React.FC<FingerSegmentProps> = ({\n position,\n rotation,\n length,\n radius,\n color,\n}) => {\n // Memoize material to avoid recreating on every render\n const material = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: color,\n metalness: 0,\n roughness: 0.6,\n clearcoat: 0.3,\n clearcoatRoughness: 0.5,\n // PBR skin properties\n transmission: 0,\n thickness: 0.1,\n ior: 1.4, // Index of refraction for skin\n sheen: 0.1, // Subtle skin sheen\n sheenRoughness: 0.8,\n // Subtle emissive for alive appearance\n emissive: new THREE.Color(color),\n emissiveIntensity: 0.02,\n }),\n [color],\n );\n\n // Dispose material on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n material.dispose();\n };\n }, [material]);\n\n return (\n <mesh position={position} rotation={rotation} castShadow receiveShadow>\n <capsuleGeometry args={[radius, length, 4, 8]} />\n <primitive object={material} attach=\"material\" />\n </mesh>\n );\n};\n\n/**\n * Single finger component with curl animation\n *\n * @korean 손가락컴포넌트\n */\ninterface FingerProps {\n readonly fingerName: string;\n readonly basePosition: [number, number, number];\n readonly curl: number;\n readonly segments: number;\n readonly isHighlighted: boolean;\n readonly skinColor: number;\n}\n\nconst Finger: React.FC<FingerProps> = ({\n fingerName,\n basePosition,\n curl,\n segments,\n isHighlighted,\n skinColor,\n}) => {\n // Finger dimensions based on anatomical proportions\n // For ~180cm person: index ~7cm, middle ~8cm, ring ~7.5cm, pinky ~6cm, thumb ~5cm\n // These dimensions are in METERS for Three.js\n const dimensions = useMemo(() => {\n // Finger segment lengths based on anatomical proportions (in meters)\n // Total finger length: proximal + intermediate + distal\n // Index: ~7cm total = 0.03 + 0.022 + 0.018\n const baseLength =\n fingerName === \"thumb\"\n ? 0.025 // Thumb proximal is shorter\n : fingerName === \"pinky\"\n ? 0.022 // Pinky is shorter\n : 0.03; // Other fingers\n\n // Finger thickness: ~1.5-1.8cm diameter (0.75-0.9cm radius)\n const baseRadius =\n fingerName === \"pinky\"\n ? 0.006 // Pinky is thinner\n : fingerName === \"thumb\"\n ? 0.009 // Thumb is thicker\n : 0.007;\n\n return {\n proximalLength: baseLength,\n intermediateLength: baseLength * 0.75,\n distalLength: baseLength * 0.55,\n radius: baseRadius,\n };\n }, [fingerName]);\n\n // Calculate curl rotations for each segment\n // Curl value 0-1 maps to rotation angles\n // These factors represent biomechanical constraints of human finger joints\n const PROXIMAL_CURL_FACTOR = 0.5; // Proximal joint bends less (0-90 degrees)\n const INTERMEDIATE_CURL_FACTOR = 0.7; // Middle joint bends moderately (0-126 degrees)\n const DISTAL_CURL_FACTOR = 1.0; // Distal joint bends most (0-180 degrees)\n\n const proximalCurl = curl * PROXIMAL_CURL_FACTOR * Math.PI;\n const intermediateCurl = curl * INTERMEDIATE_CURL_FACTOR * Math.PI;\n const distalCurl = curl * DISTAL_CURL_FACTOR * Math.PI;\n\n // Highlight with color change only (NO glow/emissive)\n const highlightColor = isHighlighted ? KOREAN_COLORS.ACCENT_RED : skinColor;\n\n return (\n <group position={basePosition}>\n {/* Proximal phalanx (knuckle joint) */}\n <FingerSegment\n position={[0, dimensions.proximalLength / 2, 0]}\n rotation={[0, 0, proximalCurl]}\n length={dimensions.proximalLength}\n radius={dimensions.radius}\n color={highlightColor}\n />\n\n {/* Intermediate phalanx (middle joint) - skip for thumb or low detail */}\n {segments >= 4 && fingerName !== \"thumb\" && (\n <FingerSegment\n position={[\n 0,\n dimensions.proximalLength + dimensions.intermediateLength / 2,\n 0,\n ]}\n rotation={[0, 0, intermediateCurl]}\n length={dimensions.intermediateLength}\n radius={dimensions.radius * 0.9}\n color={highlightColor}\n />\n )}\n\n {/* Distal phalanx (fingertip) */}\n <FingerSegment\n position={[\n 0,\n dimensions.proximalLength +\n (fingerName !== \"thumb\" && segments >= 4\n ? dimensions.intermediateLength\n : 0) +\n dimensions.distalLength / 2,\n 0,\n ]}\n rotation={[0, 0, distalCurl]}\n length={dimensions.distalLength}\n radius={dimensions.radius * 0.7}\n color={highlightColor}\n />\n </group>\n );\n};\n\n/**\n * Hand3D Component\n *\n * Complete hand with palm and 5 fingers, supporting Korean martial arts\n * hand poses with LOD optimization for performance.\n *\n * @example\n * ```tsx\n * <Hand3D\n * side=\"right\"\n * pose={HandPoseType.FIST}\n * fingerCurl={{\n * thumb: 0.8,\n * index: 1.0,\n * middle: 1.0,\n * ring: 1.0,\n * pinky: 1.0,\n * }}\n * distanceFromCamera={3}\n * wristRotation={new THREE.Euler(0, 0, 0)}\n * isHighlighted={false}\n * highlightMode=\"knuckles\"\n * />\n * ```\n *\n * @korean 손3D컴포넌트\n */\nexport const Hand3D: React.FC<Hand3DProps> = ({\n side,\n // pose, // TODO: Reserved for future pose-specific hand adjustments (e.g., different palm sizes per pose)\n fingerCurl,\n distanceFromCamera,\n wristRotation,\n isHighlighted = false,\n highlightMode = null,\n skinColor = 0xffdbac,\n scale = 1.0,\n}) => {\n // Determine LOD based on distance\n const lodConfig = useMemo(\n () => getLODConfig(distanceFromCamera),\n [distanceFromCamera],\n );\n\n // Hand orientation\n const sideMultiplier = side === \"left\" ? -1 : 1;\n\n // Palm dimensions (realistic for ~180cm person)\n // Palm width: ~8-9cm, Palm length (metacarpal area): ~9-10cm\n // Total hand length including palm and fingers: ~18-19cm\n // These dimensions are in METERS for Three.js\n const palmWidth = 0.085 * scale; // 8.5cm palm width\n const palmLength = 0.095 * scale; // 9.5cm palm/metacarpal length\n const palmThickness = 0.025 * scale; // 2.5cm palm thickness\n\n // Determine which parts to highlight\n const highlightKnuckles = highlightMode === \"knuckles\";\n const highlightPalm = highlightMode === \"palm\";\n const highlightKnifeEdge = highlightMode === \"knife_edge\";\n const highlightFingertips = highlightMode === \"fingertips\";\n\n // Palm color (highlight with brighter color, NO emissive/glow)\n const palmColor =\n isHighlighted && highlightPalm ? KOREAN_COLORS.ACCENT_GOLD : skinColor;\n\n return (\n <group\n rotation={[wristRotation.x, wristRotation.y, wristRotation.z]}\n name={`hand-3d-${side}`}\n >\n {/* Wrist connector - smooth sphere bridging forearm to palm */}\n <mesh\n position={[0, -palmLength * 0.45, 0]}\n castShadow\n receiveShadow\n name={`hand-wrist-bridge-${side}`}\n >\n <sphereGeometry args={[palmWidth * 0.35, 8, 8]} />\n <meshPhysicalMaterial\n color={skinColor}\n metalness={0}\n roughness={0.6}\n clearcoat={0.3}\n clearcoatRoughness={0.5}\n transmission={0}\n thickness={0.1}\n ior={1.4}\n sheen={0.1}\n sheenRoughness={0.8}\n emissive={new THREE.Color(0xff6040)}\n emissiveIntensity={0.02}\n />\n </mesh>\n\n {/* Palm */}\n <mesh castShadow receiveShadow name={`hand-palm-${side}`}>\n <boxGeometry args={[palmWidth, palmLength, palmThickness]} />\n <meshPhysicalMaterial\n color={palmColor}\n metalness={0}\n roughness={0.6}\n clearcoat={0.3}\n clearcoatRoughness={0.5}\n // PBR skin properties\n transmission={0}\n thickness={0.1}\n ior={1.4} // Index of refraction for skin\n sheen={0.1} // Subtle skin sheen\n sheenRoughness={0.8}\n // Subtle emissive for alive appearance\n emissive={new THREE.Color(0xff6040)}\n emissiveIntensity={0.02}\n />\n </mesh>\n\n {/* Knife edge highlight (pinky side of hand) - emissive highlight for visibility */}\n {isHighlighted && highlightKnifeEdge && (\n <mesh\n position={[(-palmWidth / 2) * sideMultiplier, 0, 0]}\n castShadow\n receiveShadow\n name={`hand-knife-edge-${side}`}\n >\n <boxGeometry args={[0.005, palmLength, palmThickness]} />\n <meshPhysicalMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n metalness={0.8}\n roughness={0.2}\n clearcoat={0.8}\n clearcoatRoughness={0.1}\n // High emissive for striking surface visibility\n emissive={KOREAN_COLORS.ACCENT_GOLD}\n emissiveIntensity={2.0}\n transmission={0}\n thickness={0.05}\n />\n </mesh>\n )}\n\n {/* Render fingers based on LOD */}\n {lodConfig.renderFingers && (\n <>\n {/* Thumb - offset toward palm center and forward */}\n <Finger\n fingerName=\"thumb\"\n basePosition={[\n 0.015 * sideMultiplier * scale,\n palmLength / 2,\n palmThickness / 2,\n ]}\n curl={fingerCurl.thumb}\n segments={3} // Thumb has no intermediate phalanx\n isHighlighted={isHighlighted && highlightFingertips}\n skinColor={skinColor}\n />\n\n {/* Index finger */}\n <Finger\n fingerName=\"index\"\n basePosition={[0.015 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.index}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n\n {/* Middle finger */}\n <Finger\n fingerName=\"middle\"\n basePosition={[0.005 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.middle}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n\n {/* Ring finger */}\n <Finger\n fingerName=\"ring\"\n basePosition={[-0.005 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.ring}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n\n {/* Pinky finger */}\n <Finger\n fingerName=\"pinky\"\n basePosition={[-0.015 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.pinky}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n </>\n )}\n\n {/* Low detail: just palm, no fingers */}\n {!lodConfig.renderFingers && (\n <mesh\n position={[0, palmLength / 2, 0]}\n castShadow\n receiveShadow\n name={`hand-simple-${side}`}\n >\n <capsuleGeometry args={[palmWidth / 2, palmLength * 0.3, 4, 8]} />\n <meshStandardMaterial\n color={skinColor}\n metalness={0.1}\n roughness={0.8}\n />\n </mesh>\n )}\n </group>\n );\n};\n\nexport default Hand3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Hand3D.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/Hand3D.tsx"],"sourcesContent":["/**\n * Hand3D component with finger geometry for martial arts techniques\n *\n * Renders detailed hand with palm and 5 fingers, supporting multiple\n * Korean martial arts hand poses (Fist, Knife-hand, Spear-hand, etc.).\n *\n * Implements LOD (Level of Detail) for performance optimization:\n * - High detail (<5 units): Full finger bones (4 segments per finger)\n * - Medium detail (5-15 units): Simplified fingers (3 segments)\n * - Low detail (>15 units): No finger detail (hand as single unit)\n *\n * @module components/three/Hand3D\n * @category 3D Components\n * @korean 손3D컴포넌트\n */\n\nimport React, { useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport type {\n FingerCurl,\n HandLODConfig,\n HandPoseType,\n HandSide,\n} from \"../../../../types/hand-animation\";\n\n/**\n * Props for Hand3D component\n *\n * @public\n * @korean 손3D속성\n */\nexport interface Hand3DProps {\n /**\n * Hand side (left or right)\n * @korean 손쪽\n */\n readonly side: HandSide;\n\n /**\n * Current hand pose\n * @korean 현재손자세\n */\n readonly pose: HandPoseType;\n\n /**\n * Finger curl amounts (0-1 per finger)\n * @korean 손가락구부림\n */\n readonly fingerCurl: FingerCurl;\n\n /**\n * Distance from camera for LOD\n * @korean 카메라거리\n */\n readonly distanceFromCamera: number;\n\n /**\n * Wrist rotation\n * @korean 손목회전\n */\n readonly wristRotation: THREE.Euler;\n\n /**\n * Whether hand is highlighted for vital point targeting\n * @korean 표시여부\n */\n readonly isHighlighted?: boolean;\n\n /**\n * Highlight mode for striking surfaces\n * @korean 표시모드\n */\n readonly highlightMode?:\n | \"none\"\n | \"knuckles\"\n | \"palm\"\n | \"knife_edge\"\n | \"fingertips\"\n | null;\n\n /**\n * Base skin color\n * @korean 피부색\n */\n readonly skinColor?: number;\n\n /**\n * Scale multiplier\n * @korean 크기배율\n */\n readonly scale?: number;\n}\n\n/**\n * Determine LOD config based on camera distance\n *\n * @param distance - Distance from camera\n * @returns LOD configuration\n * @korean LOD설정결정\n */\nconst getLODConfig = (distance: number): HandLODConfig => {\n if (distance < 5) {\n return {\n detailLevel: \"high\",\n distanceThresholds: { high: 5, medium: 15, low: 100 },\n renderFingers: true,\n fingerSegments: 4,\n };\n } else if (distance < 15) {\n return {\n detailLevel: \"medium\",\n distanceThresholds: { high: 5, medium: 15, low: 100 },\n renderFingers: true,\n fingerSegments: 3,\n };\n } else {\n return {\n detailLevel: \"low\",\n distanceThresholds: { high: 5, medium: 15, low: 100 },\n renderFingers: false,\n fingerSegments: 0,\n };\n }\n};\n\n/**\n * Finger segment component\n *\n * Renders a single finger segment (proximal, intermediate, or distal phalanx).\n *\n * @korean 손가락세그먼트컴포넌트\n */\ninterface FingerSegmentProps {\n readonly position: [number, number, number];\n readonly rotation: [number, number, number];\n readonly length: number;\n readonly radius: number;\n readonly color: number;\n}\n\nconst FingerSegment: React.FC<FingerSegmentProps> = ({\n position,\n rotation,\n length,\n radius,\n color,\n}) => {\n // Memoize material to avoid recreating on every render\n const material = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: color,\n metalness: 0,\n roughness: 0.6,\n clearcoat: 0.3,\n clearcoatRoughness: 0.5,\n // PBR skin properties\n transmission: 0,\n thickness: 0.1,\n ior: 1.4, // Index of refraction for skin\n sheen: 0.1, // Subtle skin sheen\n sheenRoughness: 0.8,\n // Subtle emissive for alive appearance\n emissive: new THREE.Color(color),\n emissiveIntensity: 0.02,\n }),\n [color],\n );\n\n // Dispose material on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n material.dispose();\n };\n }, [material]);\n\n return (\n <mesh position={position} rotation={rotation} castShadow receiveShadow>\n <capsuleGeometry args={[radius, length, 4, 8]} />\n <primitive object={material} attach=\"material\" />\n </mesh>\n );\n};\n\n/**\n * Single finger component with curl animation\n *\n * @korean 손가락컴포넌트\n */\ninterface FingerProps {\n readonly fingerName: string;\n readonly basePosition: [number, number, number];\n readonly curl: number;\n readonly segments: number;\n readonly isHighlighted: boolean;\n readonly skinColor: number;\n}\n\nconst Finger: React.FC<FingerProps> = ({\n fingerName,\n basePosition,\n curl,\n segments,\n isHighlighted,\n skinColor,\n}) => {\n // Finger dimensions based on anatomical proportions\n // For ~180cm person: index ~7cm, middle ~8cm, ring ~7.5cm, pinky ~6cm, thumb ~5cm\n // These dimensions are in METERS for Three.js\n const dimensions = useMemo(() => {\n // Finger segment lengths based on anatomical proportions (in meters)\n // Total finger length: proximal + intermediate + distal\n // Index: ~7cm total = 0.03 + 0.022 + 0.018\n const baseLength =\n fingerName === \"thumb\"\n ? 0.025 // Thumb proximal is shorter\n : fingerName === \"pinky\"\n ? 0.022 // Pinky is shorter\n : 0.03; // Other fingers\n\n // Finger thickness: ~1.5-1.8cm diameter (0.75-0.9cm radius)\n const baseRadius =\n fingerName === \"pinky\"\n ? 0.006 // Pinky is thinner\n : fingerName === \"thumb\"\n ? 0.009 // Thumb is thicker\n : 0.007;\n\n return {\n proximalLength: baseLength,\n intermediateLength: baseLength * 0.75,\n distalLength: baseLength * 0.55,\n radius: baseRadius,\n };\n }, [fingerName]);\n\n // Calculate curl rotations for each segment\n // Curl value 0-1 maps to rotation angles\n // These factors represent biomechanical constraints of human finger joints\n const PROXIMAL_CURL_FACTOR = 0.5; // Proximal joint bends less (0-90 degrees)\n const INTERMEDIATE_CURL_FACTOR = 0.7; // Middle joint bends moderately (0-126 degrees)\n const DISTAL_CURL_FACTOR = 1.0; // Distal joint bends most (0-180 degrees)\n\n const proximalCurl = curl * PROXIMAL_CURL_FACTOR * Math.PI;\n const intermediateCurl = curl * INTERMEDIATE_CURL_FACTOR * Math.PI;\n const distalCurl = curl * DISTAL_CURL_FACTOR * Math.PI;\n\n // Highlight with color change only (NO glow/emissive)\n const highlightColor = isHighlighted ? KOREAN_COLORS.ACCENT_RED : skinColor;\n\n return (\n <group position={basePosition}>\n {/* Proximal phalanx (knuckle joint) */}\n <FingerSegment\n position={[0, dimensions.proximalLength / 2, 0]}\n rotation={[0, 0, proximalCurl]}\n length={dimensions.proximalLength}\n radius={dimensions.radius}\n color={highlightColor}\n />\n\n {/* Intermediate phalanx (middle joint) - skip for thumb or low detail */}\n {segments >= 4 && fingerName !== \"thumb\" && (\n <FingerSegment\n position={[\n 0,\n dimensions.proximalLength + dimensions.intermediateLength / 2,\n 0,\n ]}\n rotation={[0, 0, intermediateCurl]}\n length={dimensions.intermediateLength}\n radius={dimensions.radius * 0.9}\n color={highlightColor}\n />\n )}\n\n {/* Distal phalanx (fingertip) */}\n <FingerSegment\n position={[\n 0,\n dimensions.proximalLength +\n (fingerName !== \"thumb\" && segments >= 4\n ? dimensions.intermediateLength\n : 0) +\n dimensions.distalLength / 2,\n 0,\n ]}\n rotation={[0, 0, distalCurl]}\n length={dimensions.distalLength}\n radius={dimensions.radius * 0.7}\n color={highlightColor}\n />\n </group>\n );\n};\n\n/**\n * Hand3D Component\n *\n * Complete hand with palm and 5 fingers, supporting Korean martial arts\n * hand poses with LOD optimization for performance.\n *\n * @example\n * ```tsx\n * <Hand3D\n * side=\"right\"\n * pose={HandPoseType.FIST}\n * fingerCurl={{\n * thumb: 0.8,\n * index: 1.0,\n * middle: 1.0,\n * ring: 1.0,\n * pinky: 1.0,\n * }}\n * distanceFromCamera={3}\n * wristRotation={new THREE.Euler(0, 0, 0)}\n * isHighlighted={false}\n * highlightMode=\"knuckles\"\n * />\n * ```\n *\n * @korean 손3D컴포넌트\n */\nexport const Hand3D: React.FC<Hand3DProps> = ({\n side,\n // pose, // TODO: Reserved for future pose-specific hand adjustments (e.g., different palm sizes per pose)\n fingerCurl,\n distanceFromCamera,\n wristRotation,\n isHighlighted = false,\n highlightMode = null,\n skinColor = 0xffdbac,\n scale = 1.0,\n}) => {\n // Determine LOD based on distance\n const lodConfig = useMemo(\n () => getLODConfig(distanceFromCamera),\n [distanceFromCamera],\n );\n\n // Hand orientation\n const sideMultiplier = side === \"left\" ? -1 : 1;\n\n // Palm dimensions (realistic for ~180cm person)\n // Palm width: ~8-9cm, Palm length (metacarpal area): ~9-10cm\n // Total hand length including palm and fingers: ~18-19cm\n // These dimensions are in METERS for Three.js\n const palmWidth = 0.085 * scale; // 8.5cm palm width\n const palmLength = 0.095 * scale; // 9.5cm palm/metacarpal length\n const palmThickness = 0.025 * scale; // 2.5cm palm thickness\n\n // Determine which parts to highlight\n const highlightKnuckles = highlightMode === \"knuckles\";\n const highlightPalm = highlightMode === \"palm\";\n const highlightKnifeEdge = highlightMode === \"knife_edge\";\n const highlightFingertips = highlightMode === \"fingertips\";\n\n // Palm color (highlight with brighter color, NO emissive/glow)\n const palmColor =\n isHighlighted && highlightPalm ? KOREAN_COLORS.ACCENT_GOLD : skinColor;\n\n return (\n <group\n rotation={[wristRotation.x, wristRotation.y, wristRotation.z]}\n name={`hand-3d-${side}`}\n >\n {/* Wrist connector - smooth sphere bridging forearm to palm */}\n <mesh\n position={[0, -palmLength * 0.45, 0]}\n castShadow\n receiveShadow\n name={`hand-wrist-bridge-${side}`}\n >\n <sphereGeometry args={[palmWidth * 0.35, 8, 8]} />\n <meshPhysicalMaterial\n color={skinColor}\n metalness={0}\n roughness={0.6}\n clearcoat={0.3}\n clearcoatRoughness={0.5}\n transmission={0}\n thickness={0.1}\n ior={1.4}\n sheen={0.1}\n sheenRoughness={0.8}\n emissive={new THREE.Color(0xff6040)}\n emissiveIntensity={0.02}\n />\n </mesh>\n\n {/* Palm */}\n <mesh castShadow receiveShadow name={`hand-palm-${side}`}>\n <boxGeometry args={[palmWidth, palmLength, palmThickness]} />\n <meshPhysicalMaterial\n color={palmColor}\n metalness={0}\n roughness={0.6}\n clearcoat={0.3}\n clearcoatRoughness={0.5}\n // PBR skin properties\n transmission={0}\n thickness={0.1}\n ior={1.4} // Index of refraction for skin\n sheen={0.1} // Subtle skin sheen\n sheenRoughness={0.8}\n // Subtle emissive for alive appearance\n emissive={new THREE.Color(0xff6040)}\n emissiveIntensity={0.02}\n />\n </mesh>\n\n {/* Knife edge highlight (pinky side of hand) - emissive highlight for visibility */}\n {isHighlighted && highlightKnifeEdge && (\n <mesh\n position={[(-palmWidth / 2) * sideMultiplier, 0, 0]}\n castShadow\n receiveShadow\n name={`hand-knife-edge-${side}`}\n >\n <boxGeometry args={[0.005, palmLength, palmThickness]} />\n <meshPhysicalMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n metalness={0.8}\n roughness={0.2}\n clearcoat={0.8}\n clearcoatRoughness={0.1}\n // High emissive for striking surface visibility\n emissive={KOREAN_COLORS.ACCENT_GOLD}\n emissiveIntensity={2.0}\n transmission={0}\n thickness={0.05}\n />\n </mesh>\n )}\n\n {/* Render fingers based on LOD */}\n {lodConfig.renderFingers && (\n <>\n {/* Thumb - offset toward palm center and forward */}\n <Finger\n fingerName=\"thumb\"\n basePosition={[\n 0.015 * sideMultiplier * scale,\n palmLength / 2,\n palmThickness / 2,\n ]}\n curl={fingerCurl.thumb}\n segments={3} // Thumb has no intermediate phalanx\n isHighlighted={isHighlighted && highlightFingertips}\n skinColor={skinColor}\n />\n\n {/* Index finger */}\n <Finger\n fingerName=\"index\"\n basePosition={[0.015 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.index}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n\n {/* Middle finger */}\n <Finger\n fingerName=\"middle\"\n basePosition={[0.005 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.middle}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n\n {/* Ring finger */}\n <Finger\n fingerName=\"ring\"\n basePosition={[-0.005 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.ring}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n\n {/* Pinky finger */}\n <Finger\n fingerName=\"pinky\"\n basePosition={[-0.015 * sideMultiplier * scale, palmLength / 2, 0]}\n curl={fingerCurl.pinky}\n segments={lodConfig.fingerSegments}\n isHighlighted={\n isHighlighted && (highlightFingertips || highlightKnuckles)\n }\n skinColor={skinColor}\n />\n </>\n )}\n\n {/* Low detail: just palm, no fingers */}\n {!lodConfig.renderFingers && (\n <mesh\n position={[0, palmLength / 2, 0]}\n castShadow\n receiveShadow\n name={`hand-simple-${side}`}\n >\n <capsuleGeometry args={[palmWidth / 2, palmLength * 0.3, 4, 8]} />\n <meshStandardMaterial\n color={skinColor}\n metalness={0.1}\n roughness={0.8}\n />\n </mesh>\n )}\n </group>\n );\n};\n\nexport default Hand3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n// Animation phase thresholds (as percentage of total duration)\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n // Derive visible feedbacks from props - no need for state sync\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n // Use useState lazy initializer for Date.now() to avoid impure function during render\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n // Animation phases: fade in (0-FADE_IN_THRESHOLD), hold (FADE_IN_THRESHOLD-FADE_OUT_THRESHOLD), fade out (FADE_OUT_THRESHOLD-1)\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n // Fade in phase\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n // Hold phase\n setOpacity(1);\n setScale(1);\n } else {\n // Fade out phase\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n // Position at center of scene, slightly below top\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n// Animation phase thresholds (as percentage of total duration)\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n // Derive visible feedbacks from props - no need for state sync\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n // Use useState lazy initializer for Date.now() to avoid impure function during render\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n // Animation phases: fade in (0-FADE_IN_THRESHOLD), hold (FADE_IN_THRESHOLD-FADE_OUT_THRESHOLD), fade out (FADE_OUT_THRESHOLD-1)\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n // Fade in phase\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n // Hold phase\n setOpacity(1);\n setScale(1);\n } else {\n // Fade out phase\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n // Position at center of scene, slightly below top\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n // Calculate critical bonus based on damage type\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n // Custom comparison: re-render only when props that affect rendering change\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n // Derive visible damages from props - no need for state sync\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n // Compare damages array length and contents\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n // Check if array contents changed (compare IDs)\n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n // Check other props\n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n // Calculate critical bonus based on damage type\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n // Custom comparison: re-render only when props that affect rendering change\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n // Derive visible damages from props - no need for state sync\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n // Compare damages array length and contents\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n // Check if array contents changed (compare IDs)\n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n // Check other props\n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|