UNPKG

myroom-react

Version:

React component wrapper for MyRoom 3D scene

1,286 lines (1,273 loc) 115 kB
// src/components/MyRoomScene.tsx import { forwardRef as forwardRef2, useImperativeHandle as useImperativeHandle2, useRef as useRef6, useState as useState3, useEffect as useEffect9 } from "react"; // src/components/IntegratedBabylonScene.tsx import { useRef as useRef5, useEffect as useEffect8, useState as useState2, useImperativeHandle, forwardRef } from "react"; import { Engine, Scene as Scene7, ArcRotateCamera as ArcRotateCamera2, Vector3 as Vector36, HemisphericLight, DirectionalLight, ShadowGenerator as ShadowGenerator3, TransformNode as TransformNode5, PointerEventTypes, Color3 as Color33, UtilityLayerRenderer as UtilityLayerRenderer2, EasingFunction, CubicEase, Color4 as Color43, Matrix as Matrix3, Animation, MeshBuilder as MeshBuilder2, StandardMaterial, AbstractMesh } from "@babylonjs/core"; import "@babylonjs/loaders"; // src/components/ItemManipulationControls.tsx import { jsx, jsxs } from "react/jsx-runtime"; var ItemManipulationControls = ({ gizmoMode, onGizmoModeChange }) => { return /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: "10px", left: "10px", display: "flex", flexDirection: "column", gap: "8px", zIndex: 100 }, children: [ /* @__PURE__ */ jsx( "button", { onClick: () => onGizmoModeChange?.("position"), style: { backgroundColor: gizmoMode === "position" ? "rgba(33, 150, 243, 0.8)" : "rgba(0, 0, 0, 0.15)", color: "white", border: gizmoMode === "position" ? "2px solid #2196F3" : "none", borderRadius: "1px", padding: "5px", cursor: "pointer", fontSize: "13px", width: "50px", height: "25px", display: "flex", alignItems: "center", justifyContent: "center", backdropFilter: "blur(10px)", transition: "all 0.2s ease", boxShadow: gizmoMode === "position" ? "0 0 10px rgba(33, 150, 243, 0.5)" : "none" }, title: "Move Items (Position)", onMouseOver: (e) => { if (gizmoMode !== "position") { e.target.style.backgroundColor = "rgba(33, 150, 243, 0.3)"; } }, onMouseOut: (e) => { if (gizmoMode !== "position") { e.target.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; } }, children: "Pos" } ), /* @__PURE__ */ jsx( "button", { onClick: () => onGizmoModeChange?.("rotation"), style: { backgroundColor: gizmoMode === "rotation" ? "rgba(255, 152, 0, 0.8)" : "rgba(0, 0, 0, 0.15)", color: "white", border: gizmoMode === "rotation" ? "2px solid #FF9800" : "none", borderRadius: "1px", padding: "5px", cursor: "pointer", fontSize: "13px", width: "50px", height: "25px", display: "flex", alignItems: "center", justifyContent: "center", backdropFilter: "blur(10px)", transition: "all 0.2s ease", boxShadow: gizmoMode === "rotation" ? "0 0 10px rgba(255, 152, 0, 0.5)" : "none" }, title: "Rotate Items", onMouseOver: (e) => { if (gizmoMode !== "rotation") { e.target.style.backgroundColor = "rgba(255, 152, 0, 0.3)"; } }, onMouseOut: (e) => { if (gizmoMode !== "rotation") { e.target.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; } }, children: "Rot" } ) ] }); }; // src/components/useAvatarMovement.ts import { useRef, useEffect, useCallback } from "react"; import { Vector3 } from "@babylonjs/core"; function useAvatarMovement({ sceneRef, cameraRef, avatarRef, touchMovement, isSceneReady, idleAnimRef, walkAnimRef, currentAnimRef, allIdleAnimationsRef, allWalkAnimationsRef, allCurrentAnimationsRef, AVATAR_BOUNDARY_LIMIT = 2.2, CAMERA_TARGET_HEAD_OFFSET = 1 }) { const avatarMovementStateRef = useRef({ isMoving: false, targetPosition: null, startPosition: null, animationProgress: 0, movementSpeed: 1.5, totalDistance: 0, targetRotation: 0, startRotation: 0, shouldRotate: false }); const cameraFollowStateRef = useRef({ currentTarget: new Vector3(0, 1, 0), dampingFactor: 0.1, shouldFollowAvatar: false }); const isRightMouseDownRef = useRef(false); const animationBlendingRef = useRef({ isBlending: false, blendDuration: 0.3, blendProgress: 0, fromAnimations: [], toAnimations: [], startTime: 0 }); const avatarMovementObserverRef = useRef(null); const moveAvatarToPosition = useCallback((targetPosition, targetDisc) => { if (!avatarRef.current || !sceneRef.current) return; console.log("Moving avatar to position:", targetPosition); const constrainedX = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, targetPosition.x)); const constrainedZ = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, targetPosition.z)); const targetPos = new Vector3(constrainedX, 0, constrainedZ); const currentPos = avatarRef.current.position; if (targetDisc) { targetDisc.position = targetPos.clone(); targetDisc.position.y += 0.02; targetDisc.isVisible = true; } const direction = targetPos.subtract(currentPos); const targetRotationY = Math.atan2(direction.x, direction.z); const distance = Vector3.Distance(currentPos, targetPos); const currentRotY = avatarRef.current.rotation.y; let rotDiff = targetRotationY - currentRotY; if (rotDiff > Math.PI) rotDiff -= 2 * Math.PI; if (rotDiff < -Math.PI) rotDiff += 2 * Math.PI; const rotationDuration = 0.1; let accumulatedRotationTime = 0; sceneRef.current.registerBeforeRender(function rotateBeforeMove() { const deltaTime = sceneRef.current.getEngine().getDeltaTime() / 1e3; accumulatedRotationTime += deltaTime; const completionRatio = Math.min(accumulatedRotationTime / rotationDuration, 1); const newRotationY = currentRotY + rotDiff * completionRatio; if (avatarRef.current) { avatarRef.current.rotation.y = newRotationY; } if (completionRatio >= 1 && avatarRef.current) { avatarRef.current.rotation.y = targetRotationY; avatarMovementStateRef.current.startPosition = avatarRef.current.position.clone(); avatarMovementStateRef.current.targetPosition = targetPos; avatarMovementStateRef.current.totalDistance = distance; avatarMovementStateRef.current.startRotation = targetRotationY; avatarMovementStateRef.current.targetRotation = targetRotationY; avatarMovementStateRef.current.isMoving = true; avatarMovementStateRef.current.shouldRotate = false; avatarMovementStateRef.current.animationProgress = 0; sceneRef.current.unregisterBeforeRender(rotateBeforeMove); } }); cameraFollowStateRef.current.shouldFollowAvatar = true; if (targetDisc) { const movementObserver = sceneRef.current.onBeforeRenderObservable.add(() => { const movementState = avatarMovementStateRef.current; if (movementState.targetPosition && Vector3.Distance(avatarRef.current.position, movementState.targetPosition) < 0.1) { targetDisc.isVisible = false; sceneRef.current.onBeforeRenderObservable.remove(movementObserver); } }); } }, [avatarRef, sceneRef, AVATAR_BOUNDARY_LIMIT]); const resetAvatarMovement = useCallback(() => { if (avatarRef.current) { avatarRef.current.position = new Vector3(0, 0, 0); avatarRef.current.rotation = new Vector3(0, 0, 0); avatarMovementStateRef.current = { isMoving: false, targetPosition: null, startPosition: null, animationProgress: 0, movementSpeed: 1.5, totalDistance: 0, targetRotation: 0, startRotation: 0, shouldRotate: false }; } }, [avatarRef]); useEffect(() => { if (!isSceneReady || !sceneRef.current || !avatarRef.current) return; if (avatarMovementObserverRef.current) { sceneRef.current.onBeforeRenderObservable.remove(avatarMovementObserverRef.current); } avatarMovementObserverRef.current = sceneRef.current.onBeforeRenderObservable.add(() => { if (!avatarRef.current) return; if (cameraFollowStateRef.current.currentTarget.equals(Vector3.Zero())) { const headPosition = avatarRef.current.position.clone(); headPosition.y += CAMERA_TARGET_HEAD_OFFSET; cameraFollowStateRef.current.currentTarget = headPosition; } let isMoving = false; if (touchMovement && (Math.abs(touchMovement.x) > 1e-3 || Math.abs(touchMovement.y) > 1e-3)) { isMoving = true; const moveSpeed = 1.25; const deltaTime = sceneRef.current.getEngine().getDeltaTime() / 1e3; const moveX = touchMovement.x * moveSpeed * deltaTime; const moveZ = -touchMovement.y * moveSpeed * deltaTime; if (Math.abs(moveX) > 1e-3 || Math.abs(moveZ) > 1e-3) { const targetRotationY = Math.atan2(moveX, moveZ); const currentRotY = avatarRef.current.rotation.y; let rotDiff = targetRotationY - currentRotY; if (rotDiff > Math.PI) rotDiff -= 2 * Math.PI; if (rotDiff < -Math.PI) rotDiff += 2 * Math.PI; const rotationDuration = 0.1; const elapsedTime = Math.min(deltaTime, rotationDuration); const completionRatio = elapsedTime / rotationDuration; if (completionRatio < 1) { avatarRef.current.rotation.y += rotDiff * completionRatio; } else { avatarRef.current.rotation.y = targetRotationY; } } const newX = avatarRef.current.position.x + moveX; const newZ = avatarRef.current.position.z + moveZ; avatarRef.current.position.x = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, newX)); avatarRef.current.position.z = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, newZ)); } const movementState = avatarMovementStateRef.current; if (movementState.isMoving && movementState.targetPosition && movementState.startPosition) { isMoving = true; const deltaTime = sceneRef.current.getEngine().getDeltaTime() / 1e3; if (movementState.totalDistance > 0) { const progressIncrement = movementState.movementSpeed * deltaTime / movementState.totalDistance; movementState.animationProgress += progressIncrement; } else { movementState.animationProgress = 1; } if (movementState.animationProgress >= 1) { avatarRef.current.position.copyFrom(movementState.targetPosition); movementState.isMoving = false; movementState.shouldRotate = false; movementState.targetPosition = null; movementState.startPosition = null; movementState.totalDistance = 0; movementState.animationProgress = 0; } else { const currentPos = Vector3.Lerp( movementState.startPosition, movementState.targetPosition, movementState.animationProgress ); avatarRef.current.position.copyFrom(currentPos); if (movementState.shouldRotate) { let startRot = movementState.startRotation; let targetRot = movementState.targetRotation; let diff = targetRot - startRot; if (diff > Math.PI) { startRot += 2 * Math.PI; } else if (diff < -Math.PI) { targetRot += 2 * Math.PI; } const currentRotY = startRot + (targetRot - startRot) * movementState.animationProgress; avatarRef.current.rotation.y = currentRotY; } if (cameraRef.current && cameraFollowStateRef.current.shouldFollowAvatar) { const headPosition = avatarRef.current.position.clone(); headPosition.y += CAMERA_TARGET_HEAD_OFFSET; cameraRef.current.setTarget(headPosition); } } } const blendState = animationBlendingRef.current; const currentTime = performance.now() / 1e3; if (blendState.isBlending) { const elapsedTime = currentTime - blendState.startTime; blendState.blendProgress = Math.min(elapsedTime / blendState.blendDuration, 1); if (blendState.fromAnimations.length > 0 && blendState.toAnimations.length > 0) { const fromWeight = 1 - blendState.blendProgress; const toWeight = blendState.blendProgress; blendState.fromAnimations.forEach((anim) => { if (anim && !anim.isDisposed) { anim.setWeightForAllAnimatables(fromWeight); } }); blendState.toAnimations.forEach((anim) => { if (anim && !anim.isDisposed) { anim.setWeightForAllAnimatables(toWeight); } }); } if (blendState.blendProgress >= 1) { blendState.fromAnimations.forEach((anim) => { if (anim && !anim.isDisposed) { anim.stop(); anim.setWeightForAllAnimatables(0); } }); blendState.toAnimations.forEach((anim) => { if (anim && !anim.isDisposed) { anim.setWeightForAllAnimatables(1); } }); allCurrentAnimationsRef.current = [...blendState.toAnimations]; currentAnimRef.current = blendState.toAnimations[0]; blendState.isBlending = false; blendState.fromAnimations = []; blendState.toAnimations = []; blendState.blendProgress = 0; console.log(`\u2705 Animation blend completed for ${allCurrentAnimationsRef.current.length} parts`); } } const targetAnimations = isMoving ? allWalkAnimationsRef.current : allIdleAnimationsRef.current; const targetMainAnimation = isMoving ? walkAnimRef.current : idleAnimRef.current; if (targetMainAnimation && targetMainAnimation !== currentAnimRef.current && !blendState.isBlending && targetAnimations.length > 0) { const animationType = isMoving ? "walk" : "idle"; console.log(`\u{1F3AD} Starting blend to ${animationType} animations: ${targetAnimations.length} parts`); blendState.isBlending = true; blendState.fromAnimations = [...allCurrentAnimationsRef.current]; blendState.toAnimations = [...targetAnimations]; blendState.blendProgress = 0; blendState.startTime = currentTime; blendState.toAnimations.forEach((anim) => { if (anim && !anim.isDisposed) { anim.setWeightForAllAnimatables(0); anim.play(true); } }); blendState.fromAnimations.forEach((anim) => { if (anim && !anim.isDisposed) { anim.setWeightForAllAnimatables(1); } }); console.log(`\u{1F3AC} Started blending ${blendState.fromAnimations.length} \u2192 ${blendState.toAnimations.length} animations`); } if (cameraRef.current && avatarRef.current && !isRightMouseDownRef.current && cameraFollowStateRef.current.shouldFollowAvatar) { const cameraFollowState = cameraFollowStateRef.current; const avatarHeadPosition = avatarRef.current.position.clone(); avatarHeadPosition.y += CAMERA_TARGET_HEAD_OFFSET; cameraFollowState.currentTarget = Vector3.Lerp( cameraFollowState.currentTarget, avatarHeadPosition, cameraFollowState.dampingFactor ); cameraRef.current.setTarget(cameraFollowState.currentTarget); } }); return () => { if (avatarMovementObserverRef.current && sceneRef.current) { sceneRef.current.onBeforeRenderObservable.remove(avatarMovementObserverRef.current); avatarMovementObserverRef.current = null; } }; }, [isSceneReady, touchMovement, sceneRef, avatarRef, cameraRef, AVATAR_BOUNDARY_LIMIT, CAMERA_TARGET_HEAD_OFFSET, idleAnimRef, walkAnimRef, currentAnimRef, allIdleAnimationsRef, allWalkAnimationsRef, allCurrentAnimationsRef]); return { // State refs avatarMovementStateRef, cameraFollowStateRef, isRightMouseDownRef, animationBlendingRef, avatarMovementObserverRef, // Functions moveAvatarToPosition, resetAvatarMovement, // Constants AVATAR_BOUNDARY_LIMIT, CAMERA_TARGET_HEAD_OFFSET }; } // src/components/SceneControlButtons.tsx import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; var SceneControlButtons = ({ onReset, onToggleFullscreen, onToggleUIOverlay, isFullscreen }) => { return /* @__PURE__ */ jsxs2( "div", { style: { position: "absolute", top: "10px", left: "0", width: "100%", display: "flex", justifyContent: "center", alignItems: "center", zIndex: 100, gap: "10px" }, children: [ /* @__PURE__ */ jsx2( "button", { onClick: onReset, style: { backgroundColor: "rgba(0, 0, 0, 0.3)", color: "white", border: "none", borderRadius: "4px", padding: "8px", cursor: "pointer", fontSize: "16px", width: "32px", height: "32px", display: "flex", alignItems: "center", justifyContent: "center", backdropFilter: "blur(2px)", transition: "background-color 0.2s ease", outline: "none" }, title: "Reset Camera and Avatar", children: "\u{1F504}" } ), /* @__PURE__ */ jsx2( "button", { onClick: onToggleFullscreen, style: { backgroundColor: "rgba(0, 0, 0, 0.3)", color: "white", border: "none", borderRadius: "4px", padding: "8px", cursor: "pointer", fontSize: "16px", width: "32px", height: "32px", display: "flex", alignItems: "center", justifyContent: "center", backdropFilter: "blur(2px)", transition: "background-color 0.2s ease", outline: "none" }, title: isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen", children: isFullscreen ? "\u2913" : "\u2922" } ), /* @__PURE__ */ jsx2( "button", { onClick: onToggleUIOverlay, style: { backgroundColor: "rgba(0, 0, 0, 0.3)", color: "white", border: "none", borderRadius: "4px", padding: "8px", cursor: "pointer", fontSize: "16px", width: "32px", height: "32px", display: "flex", alignItems: "center", justifyContent: "center", backdropFilter: "blur(2px)", transition: "background-color 0.2s ease", outline: "none" }, title: "Toggle UI Controls", children: "\u2699" } ) ] } ); }; var SceneControlButtons_default = SceneControlButtons; // src/components/useAvatarLoader.ts import { useRef as useRef2, useState, useCallback as useCallback2, useEffect as useEffect2 } from "react"; import { SceneLoader } from "@babylonjs/core"; // src/data/avatarPartsData.ts var availablePartsData = { male: { fixedParts: { body: "/models/male/male_body/male_body.glb" }, selectableParts: { hair: [ { name: "No Hair", fileName: null }, { name: "Hair Style 1", fileName: "/models/male/male_hair/male_hair_001.glb" }, { name: "Hair Style 2", fileName: "/models/male/male_hair/male_hair_002.glb" }, { name: "Hair Style 3", fileName: "/models/male/male_hair/male_hair_003.glb" } ], top: [ { name: "Male T-Shirt 1", fileName: "/models/male/male_top/male_top_001.glb" }, { name: "Male Jacket 1", fileName: "/models/male/male_top/male_top_002.glb" } ], bottom: [ { name: "Male Pant 1", fileName: "/models/male/male_bottom/male_bottom_001.glb" } ], shoes: [ { name: "No Shoes", fileName: null }, { name: "Shoes 1", fileName: "/models/male/male_shoes/male_shoes_001.glb" }, { name: "Shoes 2", fileName: "/models/male/male_shoes/male_shoes_002.glb" }, { name: "Shoes 3", fileName: "/models/male/male_shoes/male_shoes_003.glb" } ], fullset: [ { name: "No Fullset", fileName: null }, { name: "Fullset 1", fileName: "/models/male/male_fullset/male_fullset_001.glb" }, { name: "Fullset 2", fileName: "/models/male/male_fullset/male_fullset_002.glb" }, { name: "Fullset 3", fileName: "/models/male/male_fullset/male_fullset_003.glb" }, { name: "Fullset 4", fileName: "/models/male/male_fullset/male_fullset_004.glb" }, { name: "Fullset 5", fileName: "/models/male/male_fullset/male_fullset_005.glb" } ], accessory: [ { name: "No Accessory", fileName: null }, { name: "Earing 1", fileName: "/models/male/male_acc/male_acc_001.glb" }, { name: "Glasses 1", fileName: "/models/male/male_acc/male_acc_002.glb" } ] }, defaultColors: { hair: "#4A301B", top: "#1E90FF" } }, female: { fixedParts: { body: "/models/female/female_body/female_body.glb" }, selectableParts: { hair: [ { name: "No Hair", fileName: null }, { name: "Hair Style 1", fileName: "/models/female/female_hair/female_hair_001.glb" }, { name: "Hair Style 2", fileName: "/models/female/female_hair/female_hair_002.glb" } ], top: [ { name: "Blazer", fileName: "/models/female/female_top/female_top_001.glb" }, { name: "Shirt", fileName: "/models/female/female_top/female_top_002.glb" } ], bottom: [ { name: "Cute Pants", fileName: "/models/female/female_bottom/female_bottom_001.glb" }, { name: "Flower Pants", fileName: "/models/female/female_bottom/female_bottom_002.glb" } ], shoes: [ { name: "No Shoes", fileName: null }, { name: "Sport Shoes", fileName: "/models/female/female_shoes/female_shoes_001.glb" } ], fullset: [ { name: "No Fullset", fileName: null } ], accessory: [ { name: "No Accessory", fileName: null }, { name: "Sun Glasses", fileName: "/models/female/female_acc/female_acc_001.glb" }, { name: "Earing Set", fileName: "/models/female/female_acc/female_acc_002.glb" }, { name: "Camera", fileName: "/models/female/female_acc/female_acc_003.glb" } ] }, defaultColors: { hair: "#5E3D25", top: "#FF69B4" } } }; // src/data/skeletonMapping.ts var SKELETON_BONE_MAPPING = { // Standard_walk.glb bones -> Avatar bones "b_Hip_01": "mixamorigHips", "b_Spine01_02": "mixamorigSpine", "b_Spine02_03": "mixamorigSpine1", "b_Neck_04": "mixamorigNeck", "b_Head_05": "mixamorigHead", // Right arm "b_RightUpperArm_06": "mixamorigRightArm", "b_RightForeArm_07": "mixamorigRightForeArm", "b_RightHand_08": "mixamorigRightHand", // Left arm "b_LeftUpperArm_09": "mixamorigLeftArm", "b_LeftForeArm_010": "mixamorigLeftForeArm", "b_LeftHand_011": "mixamorigLeftHand", // Tail (can be skipped if avatar has no tail) "b_Tail01_012": null, "b_Tail02_013": null, "b_Tail03_014": null, // Left leg "b_LeftLeg01_015": "mixamorigLeftUpLeg", "b_LeftLeg02_016": "mixamorigLeftLeg", "b_LeftFoot01_017": "mixamorigLeftFoot", // Right leg "b_RightLeg01_019": "mixamorigRightUpLeg", "b_RightLeg02_020": "mixamorigRightLeg", "b_RightFoot01_021": "mixamorigRightFoot" }; function findMappedBone(originalBoneName, targetSkeleton) { const mappedName = SKELETON_BONE_MAPPING[originalBoneName]; if (mappedName === null) { console.log(` f994 Bone ${originalBoneName} doesn't need mapping (null)`); return null; } if (mappedName) { const bone = targetSkeleton.bones.find((b) => b.name === mappedName); if (bone) { console.log(` f994 Mapped bone ${originalBoneName} -> ${mappedName} (found)`); return bone; } console.log(` a0 Mapped bone ${originalBoneName} -> ${mappedName} (not found in target skeleton)`); } const originalBone = targetSkeleton.bones.find((b) => b.name === originalBoneName); if (originalBone) { } else { console.log(` f494 Bone ${originalBoneName} not found in target skeleton (no mapping available)`); } return originalBone; } // src/components/useAvatarLoader.ts function useAvatarLoader({ sceneRef, avatarConfig, domainConfig: domainConfig2, idleAnimRef, walkAnimRef, currentAnimRef, allIdleAnimationsRef, allWalkAnimationsRef, allCurrentAnimationsRef, avatarRef, shadowGeneratorRef }) { const loadedAvatarPartsRef = useRef2({}); const pendingPartsRef = useRef2({}); const oldPartsToDisposeRef = useRef2({}); const loadingGenderPartsRef = useRef2({ isLoading: false, gender: null, parts: {} }); const [isAnimationReady, setIsAnimationReady] = useState(false); const addAnimationTargets = useCallback2((newMeshes) => { const newSkeletons = newMeshes.filter((mesh) => mesh && mesh.skeleton).map((mesh) => mesh.skeleton); if (newSkeletons.length === 0) return; const applyToAnimGroup = (animGroup) => { if (!animGroup || animGroup.targetedAnimations.length === 0) return; animGroup.targetedAnimations.forEach((sourceTa) => { const animation = sourceTa.animation; const oldTargetName = sourceTa.target.name; newSkeletons.forEach((newSkeleton) => { const mappedBone = findMappedBone(oldTargetName, newSkeleton); if (mappedBone) { const newTargetNode = mappedBone.getTransformNode() || mappedBone; const alreadyExists = animGroup.targetedAnimations.some( (ta) => ta.animation === animation && ta.target === newTargetNode ); if (!alreadyExists) { animGroup.addTargetedAnimation(animation, newTargetNode); } } }); }); }; applyToAnimGroup(idleAnimRef.current); applyToAnimGroup(walkAnimRef.current); }, [idleAnimRef, walkAnimRef]); const loadAnimationFromGLB = useCallback2(async (animationName, options) => { if (!sceneRef.current || !avatarRef.current) return; try { const currentGender = avatarConfig.gender; const animationFileName = currentGender === "male" ? "male_anims.glb" : "female_anims.glb"; const animationUrl = `${domainConfig2.baseDomain}/animations/${animationFileName}`; const result = await SceneLoader.ImportMeshAsync("", animationUrl, "", sceneRef.current); if (result.animationGroups && result.animationGroups.length > 0) { const targetAnimGroup = result.animationGroups.find((group) => group.name.toLowerCase().includes(animationName.toLowerCase())); if (!targetAnimGroup) return; const avatarSkeletons = []; const allParts = { ...loadedAvatarPartsRef.current, ...pendingPartsRef.current }; for (const [partType, partMeshes] of Object.entries(allParts)) { if (partMeshes && partMeshes.length > 0) { for (const mesh of partMeshes) { if (mesh.skeleton) { avatarSkeletons.push({ skeleton: mesh.skeleton, partType, meshName: mesh.name, mesh }); } } } } const mainSkeleton = avatarSkeletons.find((s) => s.partType === "body")?.skeleton || avatarSkeletons[0]?.skeleton; for (const [partType, partMeshes] of Object.entries(allParts)) { if (partMeshes && partMeshes.length > 0) { for (const mesh of partMeshes) { if (!mesh.skeleton) { mesh.skeleton = mainSkeleton; } if (mesh.skeleton) { avatarSkeletons.push({ skeleton: mesh.skeleton, partType, meshName: mesh.name, mesh }); } } } } const clonedAnimations = avatarSkeletons.map((skeletonInfo, index) => { const clonedAnim = targetAnimGroup.clone(`${targetAnimGroup.name}_${skeletonInfo.partType}_${index}`, (oldTarget) => { if (oldTarget.name && skeletonInfo.skeleton.bones) { const mappedBone = findMappedBone(oldTarget.name, skeletonInfo.skeleton); if (mappedBone) { return mappedBone.getTransformNode() || mappedBone; } } return null; }); return clonedAnim ? { animation: clonedAnim, skeletonInfo } : null; }).filter(Boolean); const allClonedAnims = clonedAnimations.map((ca) => ca.animation); if (animationName.toLowerCase().includes("walk")) { allWalkAnimationsRef.current.forEach((anim) => anim._cleanup && anim._cleanup()); allWalkAnimationsRef.current = allClonedAnims; walkAnimRef.current = allClonedAnims[0]; } else if (animationName.toLowerCase().includes("idle")) { allIdleAnimationsRef.current.forEach((anim) => anim._cleanup && anim._cleanup()); allIdleAnimationsRef.current = allClonedAnims; idleAnimRef.current = allClonedAnims[0]; } const shouldSynchronize = options?.synchronizeAnimations !== false; if (shouldSynchronize && clonedAnimations.length > 1) { const mainAnim = clonedAnimations[0].animation; const allAnimations = clonedAnimations.map((ca) => ca.animation); const originalPlay = mainAnim.play.bind(mainAnim); const originalStop = mainAnim.stop.bind(mainAnim); const originalPause = mainAnim.pause.bind(mainAnim); mainAnim.play = (loop) => { const result2 = originalPlay(loop); setTimeout(() => { for (let i = 1; i < allAnimations.length; i++) { if (allAnimations[i] && mainAnim.isPlaying && !allAnimations[i].isDisposed) { allAnimations[i].play(loop); } } }, 50); return result2; }; mainAnim.stop = () => { const result2 = originalStop(); for (let i = 1; i < allAnimations.length; i++) { if (allAnimations[i] && !allAnimations[i].isDisposed) { allAnimations[i].stop(); } } return result2; }; mainAnim.pause = () => { const result2 = originalPause(); for (let i = 1; i < allAnimations.length; i++) { if (allAnimations[i] && !allAnimations[i].isDisposed) { allAnimations[i].pause(); } } return result2; }; } if (options?.playImmediately === true) { const animToPlay = clonedAnimations[0].animation; if (currentAnimRef.current) { currentAnimRef.current.stop(); } clonedAnimations.forEach((clonedAnim) => { if (clonedAnim.animation.animatables && clonedAnim.animation.animatables.length > 0) { clonedAnim.animation.animatables.forEach((animatable) => { if (animatable.getAnimations && animatable.getAnimations().length > 0) { animatable.getAnimations().forEach((animation) => { if (typeof animation.goToFrame === "function") { animation.goToFrame(0); } }); } }); } }); animToPlay.play(true); currentAnimRef.current = animToPlay; } result.meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); } } catch (error) { } }, [sceneRef, avatarConfig, domainConfig2, loadedAvatarPartsRef, pendingPartsRef, idleAnimRef, walkAnimRef, currentAnimRef, allIdleAnimationsRef, allWalkAnimationsRef]); const loadAvatar = useCallback2(async () => { if (!sceneRef.current || !avatarConfig || !avatarRef.current) return; const genderData = availablePartsData[avatarConfig.gender]; const currentBodyMeshes = loadedAvatarPartsRef.current["body"]; const isGenderChanged = currentBodyMeshes && Array.isArray(currentBodyMeshes) && currentBodyMeshes.length > 0 && currentBodyMeshes[0].metadata?.gender !== avatarConfig.gender; const needReloadBody = !currentBodyMeshes || !Array.isArray(currentBodyMeshes) || currentBodyMeshes.length === 0 || isGenderChanged; let wasPartSwapped = false; const wasWalking = walkAnimRef.current && walkAnimRef.current.isPlaying; if (isGenderChanged) { setIsAnimationReady(false); if (currentAnimRef.current) { currentAnimRef.current.stop(); if (currentAnimRef.current._cleanup) { currentAnimRef.current._cleanup(); } } idleAnimRef.current = null; walkAnimRef.current = null; currentAnimRef.current = null; allIdleAnimationsRef.current = []; allWalkAnimationsRef.current = []; allCurrentAnimationsRef.current = []; loadingGenderPartsRef.current = { isLoading: true, gender: avatarConfig.gender, parts: {} }; Object.entries(loadedAvatarPartsRef.current).forEach(([partType, meshes]) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.setEnabled(false); mesh.isVisible = false; } }); }); try { const bodyPath = genderData.fixedParts.body; const fullBodyUrl = bodyPath.startsWith("http") ? bodyPath : `${domainConfig2.baseDomain}${bodyPath}`; const bodyResult = await SceneLoader.ImportMeshAsync( "", fullBodyUrl, "", sceneRef.current ); bodyResult.meshes.forEach((mesh) => { if (mesh.parent === null && avatarRef.current) { mesh.parent = avatarRef.current; } mesh.setEnabled(false); mesh.isVisible = false; mesh.metadata = { gender: avatarConfig.gender }; }); loadingGenderPartsRef.current.parts["body"] = bodyResult.meshes; } catch (error) { } const loadPromises = []; for (const [partType, partKey] of Object.entries(avatarConfig.parts)) { if (partType === "body") continue; if (partKey && genderData.selectableParts[partType]) { const partsList = genderData.selectableParts[partType]; const partData = partsList?.find((item) => item.fileName === partKey); if (partData && partData.fileName) { const loadPartPromise = (async () => { try { const partFileName = partData.fileName; const fullPartUrl = partFileName.startsWith("http") ? partFileName : `${domainConfig2.baseDomain}${partFileName}`; const partResult = await SceneLoader.ImportMeshAsync( "", fullPartUrl, "", sceneRef.current ); partResult.meshes.forEach((mesh) => { if (mesh.parent === null && avatarRef.current) { mesh.parent = avatarRef.current; } mesh.setEnabled(false); mesh.isVisible = false; mesh.metadata = { fileName: partData.fileName }; }); loadingGenderPartsRef.current.parts[partType] = partResult.meshes; } catch (error) { } })(); loadPromises.push(loadPartPromise); } } } await Promise.all(loadPromises); Object.entries(loadedAvatarPartsRef.current).forEach(([partType, meshes]) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); }); loadedAvatarPartsRef.current = {}; Object.entries(loadingGenderPartsRef.current.parts).forEach(([partType, meshes]) => { loadedAvatarPartsRef.current[partType] = meshes; }); loadingGenderPartsRef.current = { isLoading: false, gender: null, parts: {} }; } else { if (genderData.fixedParts.body && needReloadBody) { try { if (currentBodyMeshes) { currentBodyMeshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); } const bodyPath = genderData.fixedParts.body; const fullBodyUrl = bodyPath.startsWith("http") ? bodyPath : `${domainConfig2.baseDomain}${bodyPath}`; const bodyResult = await SceneLoader.ImportMeshAsync( "", fullBodyUrl, "", sceneRef.current ); bodyResult.meshes.forEach((mesh) => { if (mesh.parent === null && avatarRef.current) { mesh.parent = avatarRef.current; } mesh.setEnabled(false); mesh.isVisible = false; mesh.metadata = { gender: avatarConfig.gender }; }); loadedAvatarPartsRef.current["body"] = bodyResult.meshes; } catch (error) { } } for (const [partType, partKey] of Object.entries(avatarConfig.parts)) { if (partType === "body") continue; if (partKey && genderData.selectableParts[partType]) { const partsList = genderData.selectableParts[partType]; const partData = partsList?.find((item) => item.fileName === partKey); if (partData && partData.fileName) { const currentPart = loadedAvatarPartsRef.current[partType]; const isCurrentPartSame = currentPart && currentPart.some( (mesh) => mesh.metadata?.fileName === partData.fileName ); if (!isCurrentPartSame) { let oldPartToDispose = null; if (currentPart) { currentPart.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.setEnabled(false); mesh.isVisible = false; } }); oldPartToDispose = currentPart; oldPartsToDisposeRef.current[partType] = currentPart; delete loadedAvatarPartsRef.current[partType]; } const partFileName = partData.fileName; const fullPartUrl = partFileName.startsWith("http") ? partFileName : `${domainConfig2.baseDomain}${partFileName}`; const partResult = await SceneLoader.ImportMeshAsync( "", fullPartUrl, "", sceneRef.current ); partResult.meshes.forEach((mesh) => { if (mesh.parent === null && avatarRef.current) { mesh.parent = avatarRef.current; } mesh.setEnabled(false); mesh.isVisible = false; mesh.metadata = { fileName: partData.fileName }; }); loadedAvatarPartsRef.current[partType] = partResult.meshes; wasPartSwapped = true; if (idleAnimRef.current && walkAnimRef.current) { addAnimationTargets(partResult.meshes); if (currentAnimRef.current) { currentAnimRef.current.stop(); currentAnimRef.current.play(true); } } } } } else { const currentPart = loadedAvatarPartsRef.current[partType]; if (currentPart) { currentPart.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); delete loadedAvatarPartsRef.current[partType]; } } } } const allPartsLoaded = () => { const partsStatus = Object.keys(avatarConfig.parts).map((partType) => { const loadedParts = loadedAvatarPartsRef.current[partType]; const isLoaded = avatarConfig.parts[partType] == null || loadedParts && loadedParts.length > 0; return isLoaded; }); return partsStatus.every((status) => status); }; while (!allPartsLoaded()) { await new Promise((resolve) => setTimeout(resolve, 100)); } if (sceneRef.current && avatarRef.current) { const isInitialLoad = !walkAnimRef.current || !idleAnimRef.current; if (isInitialLoad) { if (currentAnimRef.current) { currentAnimRef.current.stop(); } await loadAnimationFromGLB("standard_walk"); await loadAnimationFromGLB("breathing_idle", { playImmediately: true }); } setIsAnimationReady(true); Object.entries(loadedAvatarPartsRef.current).forEach(([partType, meshes]) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.setEnabled(true); mesh.isVisible = true; if (shadowGeneratorRef.current) shadowGeneratorRef.current.addShadowCaster(mesh); } }); }); if (idleAnimRef.current && !currentAnimRef.current?.isPlaying) { idleAnimRef.current.play(true); currentAnimRef.current = idleAnimRef.current; } } }, [sceneRef, avatarConfig, domainConfig2, avatarRef, loadedAvatarPartsRef, oldPartsToDisposeRef, idleAnimRef, walkAnimRef, currentAnimRef, allIdleAnimationsRef, allWalkAnimationsRef, loadAnimationFromGLB, addAnimationTargets]); useEffect2(() => { loadAvatar(); }, [sceneRef.current, avatarConfig]); useEffect2(() => { return () => { Object.values(loadedAvatarPartsRef.current).forEach((meshes) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); }); Object.values(pendingPartsRef.current).forEach((meshes) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); }); Object.values(loadingGenderPartsRef.current.parts).forEach((meshes) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); }); Object.values(oldPartsToDisposeRef.current).forEach((meshes) => { meshes.forEach((mesh) => { if (!mesh.isDisposed()) { mesh.dispose(); } }); }); [walkAnimRef.current, idleAnimRef.current, currentAnimRef.current].forEach((anim) => { if (anim && anim._cleanup) { anim._cleanup(); } }); loadedAvatarPartsRef.current = {}; pendingPartsRef.current = {}; oldPartsToDisposeRef.current = {}; loadingGenderPartsRef.current = { isLoading: false, gender: null, parts: {} }; }; }, []); return { loadedAvatarPartsRef, pendingPartsRef, oldPartsToDisposeRef, loadingGenderPartsRef, isAnimationReady, setIsAnimationReady, loadAvatar, loadAnimationFromGLB }; } // src/components/room/RoomLoader.tsx import { useEffect as useEffect3 } from "react"; import { SceneLoader as SceneLoader2 } from "@babylonjs/core"; // src/config/appConfig.ts var getConfiguredDomain = () => { if (typeof window !== "undefined" && window.MYROOM_CONFIG && window.MYROOM_CONFIG.baseDomain) { return window.MYROOM_CONFIG.baseDomain; } return "https://myroom.petarainsoft.com"; }; var domainConfig = { // The base domain URL (without trailing slash) baseDomain: getConfiguredDomain(), // Paths for different resources paths: { webComponent: "/dist/myroom-webcomponent.umd.js", embedHtml: "/embed.html", models: { rooms: "/models/rooms", items: "/models/items", avatars: "/models/avatars" } } }; // src/components/room/RoomLoader.tsx var useRoomLoader = ({ scene, roomPath, isSceneReady, roomRef }) => { useEffect3(() => { if (!isSceneReady || !scene || !roomPath || !roomRef.current) return; const loadRoom = async () => { try { roomRef.current.getChildMeshes().forEach((mesh) => mesh.dispose()); const fullRoomUrl = roomPath.startsWith("http") ? roomPath : `${domainConfig.baseDomain}${roomPath}`; const result = await SceneLoader2.ImportMeshAsync( "", fullRoomUrl, "", scene ); result.meshes.forEach((mesh) => { if (mesh.parent === null) { mesh.parent = roomRef.current; } mesh.receiveShadows = true; }); console.log("Room loaded:", roomPath); } catch (error) { console.error("Error loading room:", error); } }; loadRoom(); }, [isSceneReady, roomPath, scene, roomRef]); }; // src/components/items/ItemLoader.tsx import { useEffect as useEffect4 } from "react"; import { SceneLoader as SceneLoader3, TransformNode as TransformNode4, Vector3 as Vector33 } from "@babylonjs/core"; var useItemLoader = ({ scene, loadedItems, isSceneReady, itemsRef, loadedItemMeshesRef, shadowGeneratorRef }) => { useEffect4(() => { if (!isSceneReady || !scene || !loadedItems || !itemsRef.current) return; const loadItems = async () => { try { if (loadedItemMeshesRef.current.length > 0) { loadedItemMeshesRef.current.forEach((container) => { if (container && container.dispose) { container.dispose(); } }); loadedItemMeshesRef.current = []; } if (itemsRef.current) { itemsRef.current.getChildMeshes().forEach((mesh) => { if (mesh && mesh.dispose) { mesh.dispose(); } }); } for (const item of loadedItems) { if (!item || !item.path || typeof item.path !== "string") { console.warn("Skipping invalid item:", item); continue; } const fullItemUrl = item.path.startsWith("http") ? item.path : `${domainConfig.baseDomain}${item.path}`; const result = await SceneLoader3.ImportMeshAsync( "", fullItemUrl, "", scene ); const itemContainer = new TransformNode4(item.id, scene); itemContainer.position = new Vector33(item.position.x, item.position.y, item.position.z); if (item.rotation) { itemContainer.rotation = new Vector33(item.rotation.x, item.rotation.y, item.rotation.z); } if (item.scale) { itemContainer.scaling = new Vector33(item.scale.x, item.scale.y, item.scale.z); } itemContainer.parent = itemsRef.current; result.meshes.forEach((mesh) => { if (mesh.parent === null) { mesh.parent = itemContainer; } mesh.isPickable = true; mesh.metadata = { isFurniture: true }; if (shadowGeneratorRef.current) shadowGeneratorRef.current.addShadowCaster(mesh); }); loadedItemMeshesRef.current.push(itemContainer); } console.log(`Loaded ${loadedItems.length} items`); } catch (error) { console.error("Error loading items:", error); } }; loadItems(); }, [isSceneReady, loadedItems, scene, itemsRef, loadedItemMeshesRef]); }; // src/components/items/ItemManipulator.tsx import { useEffect as useEffect5, useRef as useRef3, useCallback as useCallback3 } from "react"; import { PositionGizmo, RotationGizmo, ScaleGizmo } from "@babylonjs/core"; var useItemManipulator = ({ gizmoMode, selectedItem, utilityLayerRef, onItemTransformChange, onSelectItem, loadedItemMeshesRef, highlightDiscRef }) => { const gizmoRef = useRef3(null); const selectedItemRef = useRef3(null); const transformTimeoutRef = useRef3(null); const isDraggingRef = useRef3(false); useEffect5(() => { selectedItemRef.current = selectedItem; }, [selectedItem]); const selectItem = (mesh) => { console.log("selectItem called with mesh:", mesh.name); const itemContainer = loadedItemMeshesRef.current.find((container) => { const isDirectChild = mesh.parent === container; let isDescendant = false; let currentParent = mesh.parent; while (currentParent && !isDescendant) { if (currentParent === container) { isDescendant = true; } currentParent = currentParent.parent; } console.log("Checking container in selectItem:", container.name, "Direct child:", isDirectChild, "Descendant:", isDescendant); return isDirectChild || isDescendant; }); console.log("Found item container:", itemContainer?.name); if (itemContainer) { selectedItemRef.current = itemContainer; onSelectItem?.(itemContainer); console.log("Item selected:", itemContainer.name); if (highlightDiscRef.current) { highlightDiscRef.current.position = itemContainer.position.clone(); highlightDiscRef.current.