Skip to content

Conversation

@zhangligei
Copy link
Collaborator

  • Create interactive 3D lab scene using React Three Fiber
  • Implement 12+ device components (liquid handler, microscope, centrifuge, AGV robot, etc.)
  • Add device detail modal with 3D preview and specifications
  • Support device animations and interactive highlighting
  • Include comprehensive device information database
  • Update navbar with 3D Lab navigation links
  • Add 3D Lab route with lazy loading and Suspense
  • Enhance scrollbar styling for better UX

变更内容

  • 新功能
  • 修复 Bug
  • 增强重构
  • 其他(请描述)

简要描述本次 PR 的主要变更内容。

相关 Issue

请关联相关 Issue(如有):#编号

检查清单

默认已勾选,如不满足,请检查。

  • 已在本地测试通过
  • 已补充/更新相关文档
  • 代码风格已经过 pre-commit 钩子检查
  • 非企业版分支
  • 遵循 贡献者文档 中的最佳实践

其他说明

如有特殊说明或注意事项,请补充。

- Create interactive 3D lab scene using React Three Fiber
- Implement 12+ device components (liquid handler, microscope, centrifuge, AGV robot, etc.)
- Add device detail modal with 3D preview and specifications
- Support device animations and interactive highlighting
- Include comprehensive device information database
- Update navbar with 3D Lab navigation links
- Add 3D Lab route with lazy loading and Suspense
- Enhance scrollbar styling for better UX
Copilot AI review requested due to automatic review settings December 16, 2025 05:35
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @zhangligei, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an interactive 3D laboratory visualization feature using React Three Fiber, adding 12+ device components with animations, device details modal, and comprehensive navigation integration. The implementation provides an immersive view of laboratory equipment with clickable interactions and detailed specifications.

Key Changes

  • Added interactive 3D lab visualization with animated device components (liquid handler, microscope, centrifuge, AGV robots, etc.)
  • Implemented lazy loading for all routes with Suspense fallback to improve initial page load performance
  • Enhanced navbar with 3D Lab navigation links and proper routing differentiation (internal vs external links)

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
web/src/router.tsx Converts all route imports to lazy loading with Suspense wrapper and adds /3D_lab route
web/src/app/navbar/Projects.tsx Adds 3D Lab and updates Anti navigation links with new icons
web/src/app/navbar/NavbarFullWidth.tsx Adds Link/a tag routing logic to differentiate internal and external links
web/src/app/navbar/Navbar.tsx Replaces Tutorial component with direct documentation link
web/src/app/3D_lab/page.tsx Main 3D lab page with device selection, list view, and modal integration
web/src/app/3D_lab/deviceInfo.ts Device information database with specs and descriptions
web/src/app/3D_lab/deviceComponents.tsx 12+ device component implementations with animation support
web/src/app/3D_lab/InteractiveLabScene.tsx Complete 3D scene layout with lighting, cameras, and device positioning
web/src/app/3D_lab/DeviceDetailModal.tsx Modal component for viewing device details with 3D preview
web/src/app/3D_lab/styles.css Custom animations and scrollbar styling for 3D lab page
web/index.css Global scrollbar webkit styling additions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

key={deviceId}
onClick={() => setSelectedDevice(deviceId)}
className="px-3 py-2 text-xs font-medium bg-indigo-100 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:hover:bg-indigo-800/50 text-indigo-700 dark:text-indigo-300 rounded-lg transition-colors"
title={info.name}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clickable device buttons in the quick access section lack accessible labels. Each button should have an aria-label attribute that includes the full device name (e.g., aria-label={info.name}) so screen reader users can understand what each button does. The title attribute alone is insufficient for accessibility as it only shows on hover.

Suggested change
title={info.name}
title={info.name}
aria-label={info.name}

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +482
// 移液站模型组件 - 支持动画
function LiquidHandlerModel({
position = [0, 0.1, 0],
isAnimating = false,
}: PositionProps & { isAnimating?: boolean }) {
const OSS_BASE_URL =
'https://storage.sciol.ac.cn/library/liquid_transform_xyz/meshes';

const baseLink = useGLTF(`${OSS_BASE_URL}/base_link.glb`);
const xLink = useGLTF(`${OSS_BASE_URL}/x_link.glb`);
const yLink = useGLTF(`${OSS_BASE_URL}/y_link.glb`);
const zLink = useGLTF(`${OSS_BASE_URL}/z_link.glb`);

const xLinkRef = useRef<Group>(null);
const yLinkRef = useRef<Group>(null);
const zLinkRef = useRef<Group>(null);

// 移液站工作动画:X、Y、Z 轴移动
useFrame((state) => {
if (isAnimating) {
const time = state.clock.elapsedTime;
if (xLinkRef.current) {
xLinkRef.current.position.x = Math.sin(time * 0.5) * 0.3;
}
if (yLinkRef.current) {
yLinkRef.current.position.y = Math.sin(time * 0.7 + 1) * 0.2;
}
if (zLinkRef.current) {
zLinkRef.current.position.z = Math.sin(time * 0.6 + 2) * 0.25;
}
}
});

return (
<group position={position} scale={1.5}>
<primitive object={baseLink.scene.clone()} />
<group ref={xLinkRef}>
<primitive object={xLink.scene.clone()} />
</group>
<group ref={yLinkRef}>
<primitive object={yLink.scene.clone()} />
</group>
<group ref={zLinkRef}>
<primitive object={zLink.scene.clone()} />
</group>
</group>
);
}

// 试剂瓶组
function ReagentBottles({ position = [0, 0, 0] }: PositionProps) {
const bottles = [
{ pos: [0, 0, 0], color: '#ef4444' },
{ pos: [0.2, 0, 0], color: '#3b82f6' },
{ pos: [0.4, 0, 0], color: '#10b981' },
{ pos: [0, 0, 0.2], color: '#f59e0b' },
{ pos: [0.2, 0, 0.2], color: '#8b5cf6' },
{ pos: [0.4, 0, 0.2], color: '#06b6d4' },
];

return (
<group position={position}>
{bottles.map((bottle, i) => (
<group key={i} position={bottle.pos as [number, number, number]}>
<mesh castShadow>
<cylinderGeometry args={[0.08, 0.08, 0.25, 16]} />
<meshStandardMaterial
color={bottle.color}
metalness={0.1}
roughness={0.2}
transparent
opacity={0.7}
/>
</mesh>
<mesh position={[0, 0.14, 0]} castShadow>
<cylinderGeometry args={[0.06, 0.06, 0.03, 16]} />
<meshStandardMaterial
color="#1f2937"
metalness={0.8}
roughness={0.3}
/>
</mesh>
<mesh position={[0, -0.03, 0]}>
<cylinderGeometry args={[0.075, 0.075, 0.18, 16]} />
<meshStandardMaterial
color={bottle.color}
metalness={0.3}
roughness={0.1}
emissive={bottle.color}
emissiveIntensity={0.3}
/>
</mesh>
</group>
))}
</group>
);
}

// 移液枪架
function PipetteStand({ position = [0, 0, 0] }: PositionProps) {
return (
<group position={position}>
<mesh castShadow>
<cylinderGeometry args={[0.15, 0.15, 0.03, 32]} />
<meshStandardMaterial color="#374151" metalness={0.6} roughness={0.4} />
</mesh>
<mesh position={[0, 0.15, 0]} castShadow>
<cylinderGeometry args={[0.02, 0.02, 0.3, 16]} />
<meshStandardMaterial color="#6b7280" metalness={0.8} roughness={0.2} />
</mesh>
{[0, 0.1, 0.2].map((y, i) => (
<group key={i} position={[0.08, y, 0]}>
<mesh rotation={[0, 0, Math.PI / 6]} castShadow>
<cylinderGeometry args={[0.015, 0.01, 0.2, 16]} />
<meshStandardMaterial
color={['#ef4444', '#3b82f6', '#10b981'][i]}
metalness={0.7}
roughness={0.3}
/>
</mesh>
</group>
))}
</group>
);
}

// 实验室货架
function LabShelf({
position = [0, 0, 0],
rotation = [0, 0, 0],
}: PositionRotationProps) {
return (
<group position={position} rotation={rotation}>
{[
[-0.4, 0.75, -0.2],
[0.4, 0.75, -0.2],
[-0.4, 0.75, 0.2],
[0.4, 0.75, 0.2],
].map((pos, i) => (
<mesh key={i} position={pos as [number, number, number]} castShadow>
<boxGeometry args={[0.04, 1.5, 0.04]} />
<meshStandardMaterial
color="#6b7280"
metalness={0.7}
roughness={0.3}
/>
</mesh>
))}
{[0.3, 0.7, 1.1, 1.5].map((y, i) => (
<mesh key={i} position={[0, y, 0]} castShadow receiveShadow>
<boxGeometry args={[0.9, 0.02, 0.45]} />
<meshStandardMaterial
color="#d1d5db"
metalness={0.3}
roughness={0.6}
/>
</mesh>
))}
</group>
);
}

// 实验台组件
function LabBench({
position = [0, 0, 0],
width = 5,
depth = 2.5,
}: LabBenchProps) {
return (
<group position={position}>
<mesh position={[0, 0.9, 0]} receiveShadow castShadow>
<boxGeometry args={[width, 0.05, depth]} />
<meshStandardMaterial color="#d1d5db" metalness={0.4} roughness={0.3} />
</mesh>
{[
[-width / 2 + 0.15, 0.45, -depth / 2 + 0.15],
[width / 2 - 0.15, 0.45, -depth / 2 + 0.15],
[-width / 2 + 0.15, 0.45, depth / 2 - 0.15],
[width / 2 - 0.15, 0.45, depth / 2 - 0.15],
].map((pos, i) => (
<mesh key={i} position={pos as [number, number, number]} castShadow>
<boxGeometry args={[0.05, 0.9, 0.05]} />
<meshStandardMaterial
color="#6b7280"
metalness={0.8}
roughness={0.2}
/>
</mesh>
))}
<mesh position={[0, 0.25, 0]} receiveShadow>
<boxGeometry args={[width - 0.2, 0.03, depth - 0.2]} />
<meshStandardMaterial color="#9ca3af" metalness={0.3} roughness={0.5} />
</mesh>
</group>
);
}

// 显微镜 - 支持动画
function Microscope({
position = [0, 0, 0],
isAnimating = false,
}: PositionProps & { isAnimating?: boolean }) {
const lensRef = useRef<Mesh>(null);

// 显微镜观察动画:镜头上下移动
useFrame((state) => {
if (lensRef.current && isAnimating) {
const time = state.clock.elapsedTime;
lensRef.current.position.y = 0.5 + Math.sin(time * 1.5) * 0.05;
}
});

return (
<group position={position} scale={0.8}>
<mesh castShadow>
<cylinderGeometry args={[0.2, 0.25, 0.05, 32]} />
<meshStandardMaterial color="#1f2937" metalness={0.8} roughness={0.2} />
</mesh>
<mesh position={[0, 0.3, 0]} castShadow>
<cylinderGeometry args={[0.03, 0.03, 0.5, 16]} />
<meshStandardMaterial color="#374151" metalness={0.9} roughness={0.1} />
</mesh>
<mesh
ref={lensRef}
position={[0, 0.5, 0.1]}
rotation={[Math.PI / 6, 0, 0]}
castShadow
>
<cylinderGeometry args={[0.08, 0.06, 0.15, 16]} />
<meshStandardMaterial color="#6b7280" metalness={0.8} roughness={0.2} />
</mesh>
<mesh position={[0, 0.15, 0]} castShadow>
<cylinderGeometry args={[0.05, 0.04, 0.08, 16]} />
<meshStandardMaterial color="#4b5563" metalness={0.9} roughness={0.1} />
</mesh>
</group>
);
}

// AGV机器人 - 支持动画
function AGVRobot({
position = [0, 0, 0],
rotation = [0, 0, 0],
isAnimating = false,
}: PositionRotationProps & { isAnimating?: boolean }) {
const robotRef = useRef<Group>(null);
const armRef = useRef<Group>(null);

// AGV移动和机械臂动画
useFrame((state) => {
if (isAnimating) {
const time = state.clock.elapsedTime;
if (robotRef.current) {
// 前后移动
robotRef.current.position.z = Math.sin(time * 0.3) * 0.5;
}
if (armRef.current) {
// 机械臂摆动
armRef.current.rotation.y = Math.sin(time * 0.5) * 0.3;
armRef.current.rotation.z = Math.sin(time * 0.7) * 0.2;
}
}
});

return (
<group ref={robotRef} position={position} rotation={rotation}>
<mesh position={[0, 0.2, 0]} castShadow>
<boxGeometry args={[1.2, 0.3, 0.9]} />
<meshStandardMaterial color="#fbbf24" metalness={0.6} roughness={0.3} />
</mesh>
{[
[-0.52, 0.1, 0.38],
[0.52, 0.1, 0.38],
[-0.52, 0.1, -0.38],
[0.52, 0.1, -0.38],
].map((pos, i) => (
<mesh
key={i}
position={pos as [number, number, number]}
rotation={[0, 0, Math.PI / 2]}
castShadow
>
<cylinderGeometry args={[0.12, 0.12, 0.15, 16]} />
<meshStandardMaterial
color="#1f2937"
metalness={0.7}
roughness={0.4}
/>
</mesh>
))}
<mesh position={[0, 0.38, 0]} castShadow>
<boxGeometry args={[1.05, 0.04, 0.75]} />
<meshStandardMaterial color="#d1d5db" metalness={0.5} roughness={0.3} />
</mesh>
<mesh position={[0, 0.55, 0]} castShadow>
<cylinderGeometry args={[0.15, 0.18, 0.25, 16]} />
<meshStandardMaterial color="#374151" metalness={0.8} roughness={0.2} />
</mesh>
<group ref={armRef} position={[0, 1.15, 0]}>
<mesh castShadow>
<cylinderGeometry args={[0.07, 0.07, 0.9, 16]} />
<meshStandardMaterial
color="#4b5563"
metalness={0.8}
roughness={0.2}
/>
</mesh>
</group>
</group>
);
}

// 离心机 - 支持动画
function Centrifuge({
position = [0, 0, 0],
isAnimating = false,
}: PositionProps & { isAnimating?: boolean }) {
const rotorRef = useRef<Mesh>(null);

// 离心机旋转动画

return (
<group position={position}>
<mesh castShadow>
<cylinderGeometry args={[0.15, 0.18, 0.25, 32]} />
<meshStandardMaterial color="#e5e7eb" metalness={0.6} roughness={0.3} />
</mesh>
<mesh ref={rotorRef} position={[0, 0.14, 0]} castShadow>
<cylinderGeometry args={[0.13, 0.13, 0.03, 32]} />
<meshStandardMaterial color="#6b7280" metalness={0.8} roughness={0.2} />
</mesh>
<mesh position={[0.16, 0.05, 0]} castShadow>
<boxGeometry args={[0.05, 0.08, 0.06]} />
<meshStandardMaterial color="#1f2937" metalness={0.5} roughness={0.5} />
</mesh>
<mesh position={[0.19, 0.08, 0]}>
<sphereGeometry args={[0.01, 16, 16]} />
<meshStandardMaterial
color={isAnimating ? '#ef4444' : '#10b981'}
emissive={isAnimating ? '#ef4444' : '#10b981'}
emissiveIntensity={isAnimating ? 2 : 1}
/>
</mesh>
</group>
);
}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate device component definitions exist in both InteractiveLabScene.tsx (lines 137-482) and deviceComponents.tsx (lines 21-505). This code duplication violates the DRY principle and will be difficult to maintain. The InteractiveLabScene.tsx should import and use the components from deviceComponents.tsx instead of redefining them.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +131
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="relative h-[90vh] w-[90vw] rounded-2xl bg-white dark:bg-neutral-900 shadow-2xl overflow-hidden">
{/* 关闭按钮 */}
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 rounded-full bg-white/90 dark:bg-neutral-800/90 p-2 shadow-lg hover:bg-white dark:hover:bg-neutral-700 transition-colors"
>
<XMarkIcon className="h-6 w-6 text-neutral-900 dark:text-neutral-100" />
</button>
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal overlay and close button lack proper accessibility attributes. The modal should include role="dialog", aria-modal="true", and aria-labelledby pointing to the device name. The close button should have aria-label="Close device details" for screen reader users. Additionally, focus should be trapped within the modal when open, and Escape key should close it.

Copilot uses AI. Check for mistakes.
<Route path="/login" element={<LoginPage />} />
<Route path="/login/callback" element={<CallbackPage />} />
<Route path="/ui-test" element={<UiTestPage />} />
<Route path="/chat" element={<ChatPage />} />
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route path '/3D_lab' uses uppercase and underscore, which violates the repository's routing convention. According to existing routes like '/chat', '/login', '/ui-test', route paths should use lowercase with hyphens. Consider changing this to '/3d-lab' to maintain consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +670 to +687
deviceId="centrifuge"
position={[-1.2, 0.95, -4.2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'centrifuge'}
isAnimating={animatingDevice === 'centrifuge'}
disabled={disabled}
>
<Centrifuge isAnimating={animatingDevice === 'centrifuge'} />
</ClickableDevice>
<ClickableDevice
deviceId="centrifuge"
position={[1.2, 0.95, -4.2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'centrifuge'}
isAnimating={animatingDevice === 'centrifuge'}
disabled={disabled}
>
<Centrifuge isAnimating={animatingDevice === 'centrifuge'} />
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple ClickableDevice instances are using the same deviceId 'centrifuge' at different positions. While this may be intentional to show multiple instances of the same device, it will cause all centrifuge instances to highlight and animate simultaneously when any one is clicked, which may not be the desired behavior. Consider using unique device IDs like 'centrifuge-1', 'centrifuge-2', etc., or modify the highlighting logic to support device instance positioning.

Suggested change
deviceId="centrifuge"
position={[-1.2, 0.95, -4.2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'centrifuge'}
isAnimating={animatingDevice === 'centrifuge'}
disabled={disabled}
>
<Centrifuge isAnimating={animatingDevice === 'centrifuge'} />
</ClickableDevice>
<ClickableDevice
deviceId="centrifuge"
position={[1.2, 0.95, -4.2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'centrifuge'}
isAnimating={animatingDevice === 'centrifuge'}
disabled={disabled}
>
<Centrifuge isAnimating={animatingDevice === 'centrifuge'} />
deviceId="centrifuge-1"
position={[-1.2, 0.95, -4.2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'centrifuge-1'}
isAnimating={animatingDevice === 'centrifuge-1'}
disabled={disabled}
>
<Centrifuge isAnimating={animatingDevice === 'centrifuge-1'} />
</ClickableDevice>
<ClickableDevice
deviceId="centrifuge-2"
position={[1.2, 0.95, -4.2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'centrifuge-2'}
isAnimating={animatingDevice === 'centrifuge-2'}
disabled={disabled}
>
<Centrifuge isAnimating={animatingDevice === 'centrifuge-2'} />

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +104
/* 自定义滚动条样式 - 覆盖 Tailwind utility */
.custom-scrollbar {
/* Firefox */
scrollbar-width: thin !important;
scrollbar-color: rgba(156, 163, 175, 0.5) rgba(0, 0, 0, 0.05) !important;
/* 强制显示滚动条 */
overflow-y: auto !important;
overflow-x: hidden !important;
}

/* Webkit 浏览器(Chrome, Safari, Edge)的滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
-webkit-appearance: none;
display: block !important;
}

.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05) !important;
border-radius: 5px;
margin: 4px 0;
}

.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5) !important;
border-radius: 5px !important;
border: 2px solid transparent;
background-clip: padding-box;
-webkit-appearance: none;
min-height: 20px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.6) !important;
}

.custom-scrollbar::-webkit-scrollbar-thumb:active {
background-color: rgba(156, 163, 175, 0.8) !important;
}

/* 深色模式下的滚动条 */
.dark .custom-scrollbar {
scrollbar-color: rgba(156, 163, 175, 0.5) transparent !important;
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5) !important;
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7) !important;
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb:active {
background-color: rgba(156, 163, 175, 0.9) !important;
}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scrollbar styling is duplicated between web/src/app/3D_lab/styles.css (lines 44-104) and web/index.css (lines 113-141). The duplicate definitions will make maintenance difficult and could lead to inconsistent styling. Consider consolidating all scrollbar styling in the global index.css file and removing the duplicate from the 3D_lab styles.css.

Suggested change
/* 自定义滚动条样式 - 覆盖 Tailwind utility */
.custom-scrollbar {
/* Firefox */
scrollbar-width: thin !important;
scrollbar-color: rgba(156, 163, 175, 0.5) rgba(0, 0, 0, 0.05) !important;
/* 强制显示滚动条 */
overflow-y: auto !important;
overflow-x: hidden !important;
}
/* Webkit 浏览器(Chrome, Safari, Edge)的滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
-webkit-appearance: none;
display: block !important;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05) !important;
border-radius: 5px;
margin: 4px 0;
}
.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5) !important;
border-radius: 5px !important;
border: 2px solid transparent;
background-clip: padding-box;
-webkit-appearance: none;
min-height: 20px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.6) !important;
}
.custom-scrollbar::-webkit-scrollbar-thumb:active {
background-color: rgba(156, 163, 175, 0.8) !important;
}
/* 深色模式下的滚动条 */
.dark .custom-scrollbar {
scrollbar-color: rgba(156, 163, 175, 0.5) transparent !important;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5) !important;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7) !important;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb:active {
background-color: rgba(156, 163, 175, 0.9) !important;
}

Copilot uses AI. Check for mistakes.
name: 'Anti',
description: '用于实验室模拟的3D数字孪生平台',
href: `/deepmd-kit`,
href: `/3D_lab`,
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route path '/3D_lab' uses uppercase and underscore format. For consistency with other routes in the navbar (like existing lowercase paths), this should be changed to '/3d-lab'.

Copilot uses AI. Check for mistakes.
Comment on lines +703 to +725
deviceId="agv-robot"
position={[-2.5, 0, 1.5]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'agv-robot'}
isAnimating={animatingDevice === 'agv-robot'}
disabled={disabled}
>
<AGVRobot
rotation={[0, Math.PI / 6, 0]}
isAnimating={animatingDevice === 'agv-robot'}
/>
</ClickableDevice>
<ClickableDevice
deviceId="agv-robot"
position={[2.8, 0, -2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'agv-robot'}
isAnimating={animatingDevice === 'agv-robot'}
disabled={disabled}
>
<AGVRobot
rotation={[0, -Math.PI / 4, 0]}
isAnimating={animatingDevice === 'agv-robot'}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two ClickableDevice instances are using the same deviceId 'agv-robot' at different positions. This will cause both AGV robots to highlight and animate simultaneously when either is clicked, which may confuse users. Consider using unique device IDs like 'agv-robot-1' and 'agv-robot-2' to enable independent interaction with each robot.

Suggested change
deviceId="agv-robot"
position={[-2.5, 0, 1.5]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'agv-robot'}
isAnimating={animatingDevice === 'agv-robot'}
disabled={disabled}
>
<AGVRobot
rotation={[0, Math.PI / 6, 0]}
isAnimating={animatingDevice === 'agv-robot'}
/>
</ClickableDevice>
<ClickableDevice
deviceId="agv-robot"
position={[2.8, 0, -2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'agv-robot'}
isAnimating={animatingDevice === 'agv-robot'}
disabled={disabled}
>
<AGVRobot
rotation={[0, -Math.PI / 4, 0]}
isAnimating={animatingDevice === 'agv-robot'}
deviceId="agv-robot-1"
position={[-2.5, 0, 1.5]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'agv-robot-1'}
isAnimating={animatingDevice === 'agv-robot-1'}
disabled={disabled}
>
<AGVRobot
rotation={[0, Math.PI / 6, 0]}
isAnimating={animatingDevice === 'agv-robot-1'}
/>
</ClickableDevice>
<ClickableDevice
deviceId="agv-robot-2"
position={[2.8, 0, -2]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'agv-robot-2'}
isAnimating={animatingDevice === 'agv-robot-2'}
disabled={disabled}
>
<AGVRobot
rotation={[0, -Math.PI / 4, 0]}
isAnimating={animatingDevice === 'agv-robot-2'}

Copilot uses AI. Check for mistakes.

{/* 前方工作台 */}
<LabBench position={[-1.5, 0, 3.5]} width={2.5} depth={1.5} />
<Monitor position={[-1.5, 0.95, 3.5]} rotation={[0, Math.PI, 0]} />
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple Monitor instances are rendered in the scene (lines 592, 631, 657, 658, 695), but only two are wrapped in ClickableDevice components (lines 586-602, 630-647). The other monitors at positions [0, 0.95, -4.2], [-1.8, 0.95, -4.2], [1.8, 0.95, -4.2], and [-1.5, 0.95, 3.5] cannot be clicked. This creates an inconsistent user experience where some monitors are interactive and others are not.

Suggested change
<Monitor position={[-1.5, 0.95, 3.5]} rotation={[0, Math.PI, 0]} />
<ClickableDevice
deviceId="monitor-front"
position={[-1.5, 0.95, 3.5]}
rotation={[0, Math.PI, 0]}
onDeviceClick={onDeviceClick}
isHighlighted={highlightedDevice === 'monitor-front'}
isAnimating={animatingDevice === 'monitor-front'}
disabled={disabled}
>
<Monitor
isAnimating={animatingDevice === 'monitor-front'}
/>
</ClickableDevice>

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +87
useFrame(() => {
if (groupRef.current) {
currentScale.current += (targetScale - currentScale.current) * 0.1;
groupRef.current.scale.setScalar(currentScale.current);
}
});

// 点击动画
useFrame((state) => {
if (groupRef.current && isAnimating) {
// 简单的脉冲动画
const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.05;
groupRef.current.scale.setScalar(1 + pulse);
}
});

Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ClickableDevice component has conflicting animation logic. Lines 72-77 handle highlight scaling animation, while lines 80-86 handle clicking animation that also modifies scale. When both isHighlighted and isAnimating are true, these two useFrame hooks will fight over the scale value, potentially causing jittery behavior. The animation logic should be consolidated into a single useFrame hook that considers both states.

Suggested change
useFrame(() => {
if (groupRef.current) {
currentScale.current += (targetScale - currentScale.current) * 0.1;
groupRef.current.scale.setScalar(currentScale.current);
}
});
// 点击动画
useFrame((state) => {
if (groupRef.current && isAnimating) {
// 简单的脉冲动画
const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.05;
groupRef.current.scale.setScalar(1 + pulse);
}
});
useFrame((state) => {
if (groupRef.current) {
// Smoothly animate towards the target scale (highlight/hover)
currentScale.current += (targetScale - currentScale.current) * 0.1;
// If animating, apply pulse on top of base scale
let finalScale = currentScale.current;
if (isAnimating) {
const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.05;
finalScale *= (1 + pulse);
}
groupRef.current.scale.setScalar(finalScale);
}
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants