UNPKG

myroom-react

Version:

React component wrapper for MyRoom 3D scene

1,271 lines (1,256 loc) 121 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AdvancedExample: () => AdvancedExample_default, MyRoomDemo: () => MyRoomDemo_default, MyRoomScene: () => MyRoomScene_default, SimpleExample: () => SimpleExample_default, availablePartsData: () => availablePartsData2, getAvailableParts: () => getAvailableParts, getDefaultColorsForGender: () => getDefaultColorsForGender, getDefaultConfigForGender: () => getDefaultConfigForGender }); module.exports = __toCommonJS(index_exports); // src/components/MyRoomScene.tsx var import_react9 = require("react"); // src/components/IntegratedBabylonScene.tsx var import_react8 = require("react"); var import_core9 = require("@babylonjs/core"); var import_loaders = require("@babylonjs/loaders"); // src/components/ItemManipulationControls.tsx var import_jsx_runtime = require("react/jsx-runtime"); var ItemManipulationControls = ({ gizmoMode, onGizmoModeChange }) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "absolute", top: "10px", left: "10px", display: "flex", flexDirection: "column", gap: "8px", zIndex: 100 }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.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 var import_react = require("react"); var import_core = require("@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 = (0, import_react.useRef)({ isMoving: false, targetPosition: null, startPosition: null, animationProgress: 0, movementSpeed: 1.5, totalDistance: 0, targetRotation: 0, startRotation: 0, shouldRotate: false }); const cameraFollowStateRef = (0, import_react.useRef)({ currentTarget: new import_core.Vector3(0, 1, 0), dampingFactor: 0.1, shouldFollowAvatar: false }); const isRightMouseDownRef = (0, import_react.useRef)(false); const animationBlendingRef = (0, import_react.useRef)({ isBlending: false, blendDuration: 0.3, blendProgress: 0, fromAnimations: [], toAnimations: [], startTime: 0 }); const avatarMovementObserverRef = (0, import_react.useRef)(null); const moveAvatarToPosition = (0, import_react.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 import_core.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 = import_core.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 && import_core.Vector3.Distance(avatarRef.current.position, movementState.targetPosition) < 0.1) { targetDisc.isVisible = false; sceneRef.current.onBeforeRenderObservable.remove(movementObserver); } }); } }, [avatarRef, sceneRef, AVATAR_BOUNDARY_LIMIT]); const resetAvatarMovement = (0, import_react.useCallback)(() => { if (avatarRef.current) { avatarRef.current.position = new import_core.Vector3(0, 0, 0); avatarRef.current.rotation = new import_core.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]); (0, import_react.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(import_core.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 = import_core.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 = import_core.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 var import_jsx_runtime2 = require("react/jsx-runtime"); var SceneControlButtons = ({ onReset, onToggleFullscreen, onToggleUIOverlay, isFullscreen }) => { return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "div", { style: { position: "absolute", top: "10px", left: "0", width: "100%", display: "flex", justifyContent: "center", alignItems: "center", zIndex: 100, gap: "10px" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "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__ */ (0, import_jsx_runtime2.jsx)( "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__ */ (0, import_jsx_runtime2.jsx)( "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 var import_react2 = require("react"); var import_core2 = require("@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 = (0, import_react2.useRef)({}); const pendingPartsRef = (0, import_react2.useRef)({}); const oldPartsToDisposeRef = (0, import_react2.useRef)({}); const loadingGenderPartsRef = (0, import_react2.useRef)({ isLoading: false, gender: null, parts: {} }); const [isAnimationReady, setIsAnimationReady] = (0, import_react2.useState)(false); const addAnimationTargets = (0, import_react2.useCallback)((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 = (0, import_react2.useCallback)(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 import_core2.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 = (0, import_react2.useCallback)(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 import_core2.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 import_core2.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 import_core2.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 import_core2.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]); (0, import_react2.useEffect)(() => { loadAvatar(); }, [sceneRef.current, avatarConfig]); (0, import_react2.useEffect)(() => { 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 var import_react3 = require("react"); var import_core3 = require("@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 }) => { (0, import_react3.useEffect)(() => { 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 import_core3.SceneLoader.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 var import_react4 = require("react"); var import_core4 = require("@babylonjs/core"); var useItemLoader = ({ scene, loadedItems, isSceneReady, itemsRef, loadedItemMeshesRef, shadowGeneratorRef }) => { (0, import_react4.useEffect)(() => { 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 import_core4.SceneLoader.ImportMeshAsync( "", fullItemUrl, "", scene ); const itemContainer = new import_core4.TransformNode(item.id, scene); itemContainer.position = new import_core4.Vector3(item.position.x, item.position.y, item.position.z); if (item.rotation) { itemContainer.rotation = new import_core4.Vector3(item.rotation.x, item.rotation.y, item.rotation.z); } if (item.scale) { itemContainer.scaling = new import_core4.Vector3(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 var import_react5 = require("react"); var import_core5 = require("@babylonjs/core"); var useItemManipulator = ({ gizmoMode, selectedItem, utilityLayerRef, onItemTransformChange, onSelectItem, loadedItemMeshesRef, highlightDiscRef }) => { const gizmoRef = (0, import_react5.useRef)(null); const selectedItemRef = (0, import_react5.useRef)(nu