myroom-react
Version:
React component wrapper for MyRoom 3D scene
1,271 lines (1,256 loc) • 121 kB
JavaScript
"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