feat: 加载STL模型

This commit is contained in:
zhanghao 2025-12-09 10:33:05 +08:00
parent 8ca84bbbad
commit 11f369452c
56 changed files with 2082 additions and 942 deletions

View File

@ -39,6 +39,8 @@
"socket.io-client": "^4.8.1",
"sockjs-client": "^1.6.1",
"splitpanes": "3.1.5",
"three": "^0.181.2",
"urdf-loader": "^0.12.6",
"uuid": "^11.1.0",
"vue": "3.4.31",
"vue-cropper": "1.1.1",

660
public/models/dual_arm.urdf Normal file
View File

@ -0,0 +1,660 @@
<?xml version="1.0" encoding="utf-8"?>
<robot name="dual_arm">
<mujoco>
<compiler
meshdir="meshes"
balanceinertia="true"
discardvisual="false" />
</mujoco>
<link name="base_link">
<visual>
<origin xyz="0 0 0.6" rpy="0 0 0"/>
<geometry>
<cylinder radius="0.05" length="1.2"/>
</geometry>
<material name="gray">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0.6" rpy="0 0 0"/>
<geometry>
<cylinder radius="0.05" length="1.2"/>
</geometry>
</collision>
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="25.4469"/>
<inertia
ixx="3.06953"
ixy="0.0"
ixz="0.0"
iyy="3.06953"
iyz="0.0"
izz="0.03181"/>
</inertial>
</link>
<joint name="base_fixed" type="fixed">
<origin rpy="0 0 0" xyz="0 0 1.2"/>
<parent link="base_link"/>
<child link="PELVIS_S"/>
</joint>
<link name="PELVIS_S">
<inertial>
<origin xyz="3.78529087037144E-05 3.81781425684836E-07 0.0386396273530852" rpy="0 0 0" />
<mass value="2.10624590271277" />
<inertia
ixx="0.00165706865324979"
ixy="3.13663044821662E-09"
ixz="6.84209533216046E-07"
iyy="0.00136736758630241"
iyz="-1.01014607878816E-10"
izz="0.00168295663089359" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/PELVIS_S.STL" />
</geometry>
<material name="">
<color rgba="0.698039215686274 0.698039215686274 0.698039215686274 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/PELVIS_S.STL" />
</geometry>
</collision>
</link>
<link name="L_SHOULDER_P_S">
<inertial>
<origin xyz="-0.00982258725282134 0.0704593083751867 1.15261874861217E-06" rpy="0 0 0" />
<mass value="0.880737519698403" />
<inertia
ixx="0.000583190308378148"
ixy="-1.53153074560473E-05"
ixz="6.58466471754072E-09"
iyy="0.000445532440067177"
iyz="6.37001404038516E-09"
izz="0.000465648071069952" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.898039215686275 0.917647058823529 0.929411764705882 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_SHOULDER_P" type="revolute">
<origin xyz="0 0.0945 0.042" rpy="0 0 0" />
<parent link="PELVIS_S" />
<child link="L_SHOULDER_P_S" />
<axis xyz="0 1 0" />
<limit lower="-1.57" upper="1.57" effort="120" velocity="3.351" />
</joint>
<link name="L_SHOULDER_R_S">
<inertial>
<origin xyz="-0.0346025282975857 0.091739327988169 -1.67085072999562E-08" rpy="0 0 0" />
<mass value="0.594788424442483" />
<inertia
ixx="0.000380970396712656"
ixy="4.80751844573946E-05"
ixz="-1.34913746586865E-11"
iyy="0.000320962178597474"
iyz="-1.00039763442409E-09"
izz="0.000414771047901155" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_SHOULDER_R" type="revolute">
<origin xyz="0.035 0.0765 0" rpy="0 0 0" />
<parent link="L_SHOULDER_P_S" />
<child link="L_SHOULDER_R_S" />
<axis xyz="1 0 0" />
<limit lower="-2" upper="2" effort="120" velocity="3.351" />
</joint>
<link name="L_SHOULDER_Y_S">
<inertial>
<origin xyz="-0.00440976862014801 0.0863620459174068 9.50748668682166E-09"
rpy="0 0 0" />
<mass value="0.563406026626801" />
<inertia
ixx="0.000327003834287552"
ixy="-1.8057438966771E-05"
ixz="7.28778136267901E-10"
iyy="0.000213830709361531"
iyz="1.49273668517817E-10"
izz="0.000297341029639189" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_SHOULDER_Y" type="revolute">
<origin xyz="-0.035 0.1475 0" rpy="0 0 0" />
<parent link="L_SHOULDER_R_S" />
<child link="L_SHOULDER_Y_S" />
<axis xyz="0 1 0" />
<limit lower="-2.18" upper="0" effort="80" velocity="3.8758" />
</joint>
<link name="L_ELBOW_R_S">
<inertial>
<origin xyz="-0.0335624237303443 0.0603199964106564 2.99655911639718E-07" rpy="0 0 0" />
<mass value="0.393571904406493" />
<inertia
ixx="0.00017277119386253"
ixy="2.34549801867355E-05"
ixz="-1.90560556659271E-09"
iyy="0.000155340245267897"
iyz="8.82493073600007E-09"
izz="0.00018104232734159" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_ELBOW_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_ELBOW_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_ELBOW_R" type="revolute">
<origin xyz="0.034 0.1025 0" rpy="0 0 0" />
<parent link="L_SHOULDER_Y_S" />
<child link="L_ELBOW_R_S" />
<axis xyz="1 0 0" />
<limit lower="-2.05" upper="0" effort="50" velocity="4.71" />
</joint>
<link name="L_WRIST_P_S">
<inertial>
<origin xyz="-1.39659173115092E-10 0.0675972604744393 0.019200551565574" rpy="0 0 0" />
<mass value="0.442332465815496" />
<inertia
ixx="0.000476754055455895"
ixy="-4.61505824713347E-15"
ixz="6.03808112272229E-18"
iyy="0.000103466585850499"
iyz="-4.88426705501198E-05"
izz="0.000482956345754213" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.698039215686274 0.698039215686274 0.698039215686274 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_WRIST_P" type="revolute">
<origin xyz="-0.034 0.0965 0" rpy="0 0 0" />
<parent link="L_ELBOW_R_S" />
<child link="L_WRIST_P_S" />
<axis xyz="0 1 0" />
<limit lower="0" upper="3.14" effort="50" velocity="4.71" />
</joint>
<link name="L_WRIST_Y_S">
<inertial>
<origin xyz="-0.00464135887331864 -5.06426789392833E-10 -0.0341253783666609" rpy="0 0 0" />
<mass value="0.23573847772002" />
<inertia
ixx="5.10686940455785E-05"
ixy="7.45706654358429E-16"
ixz="6.2619568923368E-06"
iyy="6.00636019624519E-05"
iyz="1.27989654155383E-16"
izz="5.32182903609189E-05" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.647058823529412 0.619607843137255 0.588235294117647 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_WRIST_Y" type="revolute">
<origin xyz="0 0.1525 0.039" rpy="0 0 0" />
<parent link="L_WRIST_P_S" />
<child link="L_WRIST_Y_S" />
<axis xyz="0 0 1" />
<limit lower="-0.78" upper="0.78" effort="50" velocity="0.79" />
</joint>
<link name="L_WRIST_R_S">
<inertial>
<origin xyz="-0.0161477357620243 0.0955046558857351 -0.00499392489444117" rpy="0 0 0" />
<mass value="0.504894112043562" />
<inertia
ixx="0.000186833711781125"
ixy="-3.99488705622731E-06"
ixz="-1.51920720398336E-06"
iyy="0.000179960980467837"
iyz="3.6992669535716E-06"
izz="0.000280756101568308" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_WRIST_R" type="revolute">
<origin xyz="0.0258 0 -0.039" rpy="0 0 0" />
<parent link="L_WRIST_Y_S" />
<child link="L_WRIST_R_S" />
<axis xyz="1 0 0" />
<limit lower="-1.57" upper="0.26" effort="50" velocity="4.71" />
</joint>
<link name="R_SHOULDER_P_S">
<inertial>
<origin xyz="-0.00982258725282141 -0.0704593093873431 -1.15069920925137E-06" rpy="0 0 0" />
<mass value="0.880737519698404" />
<inertia
ixx="0.000583190308378149"
ixy="1.53153074560471E-05"
ixz="-6.58466471736542E-09"
iyy="0.000445532440067178"
iyz="6.37001403998779E-09"
izz="0.000465648071069953" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_SHOULDER_P" type="revolute">
<origin xyz="0 -0.0945 0.042" rpy="0 0 0" />
<parent link="PELVIS_S" />
<child link="R_SHOULDER_P_S" />
<axis xyz="0 -1 0" />
<limit lower="-3.14" upper="3.14" effort="120" velocity="3.351" />
</joint>
<link name="R_SHOULDER_R_S">
<inertial>
<origin xyz="-0.0346025282975784 -0.09173932900033 1.86280643132974E-08" rpy="0 0 0" />
<mass value="0.59478842444248" />
<inertia
ixx="0.000380970396712653"
ixy="-4.80751844573946E-05"
ixz="1.34913746041655E-11"
iyy="0.000320962178597472"
iyz="-1.00039763417375E-09"
izz="0.000414771047901152" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_SHOULDER_R" type="revolute">
<origin xyz="0.035 -0.0765 0" rpy="0 0 0" />
<parent link="R_SHOULDER_P_S" />
<child link="R_SHOULDER_R_S" />
<axis xyz="1 0 0" />
<limit lower="-0.78" upper="1.57" effort="120" velocity="3.351" />
</joint>
<link name="R_SHOULDER_Y_S">
<inertial>
<origin xyz="-0.00440976862014946 -0.0863620469295699 -7.58791862676134E-09" rpy="0 0 0" />
<mass value="0.563406026626801" />
<inertia
ixx="0.000327003834287551"
ixy="1.80574389667711E-05"
ixz="-7.28778136077729E-10"
iyy="0.000213830709361532"
iyz="1.49273668428957E-10"
izz="0.000297341029639189" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_Y_S.STL" />
</geometry>
<material
name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_SHOULDER_Y" type="revolute">
<origin xyz="-0.035 -0.1475 0" rpy="0 0 0" />
<parent link="R_SHOULDER_R_S" />
<child link="R_SHOULDER_Y_S" />
<axis xyz="0 -1 0" />
<limit lower="-3.14" upper="3.14" effort="80" velocity="3.8758" />
</joint>
<link name="R_ELBOW_R_S">
<inertial>
<origin xyz="-0.0335624237303424 -0.0603199974228191 -2.97736340082455E-07" rpy="0 0 0" />
<mass value="0.393571904406492" />
<inertia
ixx="0.000172771193862529"
ixy="-2.34549801867353E-05"
ixz="1.90560556668771E-09"
iyy="0.000155340245267897"
iyz="8.82493073602971E-09"
izz="0.00018104232734159" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_ELBOW_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_ELBOW_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_ELBOW_R" type="revolute">
<origin xyz="0.034 -0.1025 0" rpy="0 0 0" />
<parent link="R_SHOULDER_Y_S" />
<child link="R_ELBOW_R_S" />
<axis xyz="1 0 0" />
<limit lower="0" upper="2.05" effort="80" velocity="3.8758" />
</joint>
<link name="R_WRIST_P_S">
<inertial>
<origin xyz="-1.39656577968772E-10 -0.0675972614865965 0.0192005515655728" rpy="0 0 0" />
<mass value="0.442332465815497" />
<inertia
ixx="0.000476754055455896"
ixy="-4.61462558956188E-15"
ixz="-5.91762926004267E-18"
iyy="0.0001034665858505"
iyz="4.88426705501212E-05"
izz="0.000482956345754214" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.647058823529412 0.619607843137255 0.588235294117647 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_WRIST_P" type="revolute">
<origin xyz="-0.034 -0.0965 0" rpy="0 0 0" />
<parent link="R_ELBOW_R_S" />
<child link="R_WRIST_P_S" />
<axis xyz="0 -1 0" />
<limit lower="-3.14" upper="3.14" effort="50" velocity="4.71" />
</joint>
<link name="R_WRIST_Y_S">
<inertial>
<origin xyz="-0.00464135887330083 -5.06426456325926E-10 -0.0341253783666614" rpy="0 0 0" />
<mass value="0.235738477720019" />
<inertia
ixx="5.1068694045578E-05"
ixy="7.45771487459288E-16"
ixz="6.26195689233664E-06"
iyy="6.00636019624515E-05"
iyz="1.27828046342987E-16"
izz="5.32182903609188E-05" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.647058823529412 0.619607843137255 0.588235294117647 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_WRIST_Y" type="revolute">
<origin xyz="0 -0.1525 0.039" rpy="0 0 0" />
<parent link="R_WRIST_P_S" />
<child link="R_WRIST_Y_S" />
<axis xyz="0 0 1" />
<limit lower="-0.78" upper="0.78" effort="50" velocity="0.79" />
</joint>
<link name="R_WRIST_R_S">
<inertial>
<origin xyz="-0.0201642233698132 -0.11074968386657 -0.00598955339232021" rpy="0 0 0" />
<mass value="0.504366058218534" />
<inertia
ixx="0.000185559065855467"
ixy="5.75889766751509E-06"
ixz="2.40683898454438E-06"
iyy="0.000131246007872084"
iyz="1.60533277040148E-06"
izz="0.000271800951396149" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_R_S.STL" />
</geometry>
<material
name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_WRIST_R" type="revolute">
<origin xyz="0.03 0 -0.039" rpy="0 0 0" />
<parent link="R_WRIST_Y_S" />
<child link="R_WRIST_R_S" />
<axis xyz="1 0 0" />
<limit lower="-0.26" upper="1.57" effort="50" velocity="4.71" />
</joint>
<link name="R_FINGER_TIP">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="0"/>
<inertia ixx="1e-6" ixy="0" ixz="0" iyy="1e-6" iyz="0" izz="1e-6"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.004"/>
</geometry>
<material name="">
<color rgba="0 1 1 1"/> <!-- 绿色 -->
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.005"/>
</geometry>
</collision>
</link>
<joint name="R_FINGER_TIP_FIXED" type="fixed">
<origin xyz="0.00684256 -0.284077 0.00801525" rpy="0 0 0"/>
<parent link="R_WRIST_R_S"/>
<child link="R_FINGER_TIP"/>
<axis xyz="0 0 1"/>
</joint>
<link name="R_CAM">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="0"/>
<inertia ixx="1e-6" ixy="0" ixz="0" iyy="1e-6" iyz="0" izz="1e-6"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.004"/>
</geometry>
<material name="">
<color rgba="0 1 0 1"/> <!-- 绿色 -->
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.005"/>
</geometry>
</collision>
</link>
<joint name="R_CAM_FIXED" type="fixed">
<origin xyz="-0.01212 -0.17655 0.07506" rpy="-1.5707963267 0 3.1415926535"/>
<parent link="R_WRIST_R_S"/>
<child link="R_CAM"/>
<axis xyz="0 0 1"/>
</joint>
</robot>

281
public/models/left_arm.urdf Normal file
View File

@ -0,0 +1,281 @@
<?xml version="1.0" encoding="utf-8"?>
<robot name="left_arm">
<mujoco>
<compiler
meshdir="meshes"
balanceinertia="true"
discardvisual="false" />
</mujoco>
<link name="base_link">
<visual>
<geometry>
<cylinder radius="0.05" length="1.2"/>
</geometry>
<material name="gray">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="1.2"/>
</geometry>
</collision>
<inertial>
<mass value="20.0"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="1.0" ixy="0.0" ixz="0.0" iyy="1.0" iyz="0.0" izz="1.0"/>
</inertial>
</link>
<joint name="base_fixed" type="fixed">
<origin rpy="0 0 0.0" xyz="0 0 0.6"/>
<parent link="base_link"/>
<child link="PELVIS_S"/>
<axis xyz="0 0 0"/>
</joint>
<link name="PELVIS_S">
<inertial>
<origin xyz="3.78529087037144E-05 3.81781425684836E-07 0.0386396273530852" rpy="0 0 0" />
<mass value="2.10624590271277" />
<inertia ixx="0.00165706865324979" ixy="3.13663044821662E-09" ixz="6.84209533216046E-07" iyy="0.00136736758630241" iyz="-1.01014607878816E-10" izz="0.00168295663089359" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/PELVIS_S.STL" />
</geometry>
<material name="">
<color rgba="0.698039215686274 0.698039215686274 0.698039215686274 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/PELVIS_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_SHOULDER_P" type="revolute">
<origin xyz="0 0.0945 0.042" rpy="0 0 0" />
<parent link="PELVIS_S" />
<child link="L_SHOULDER_P_S" />
<axis xyz="0 1 0" />
<limit lower="-3.14" upper="3.14" effort="100" velocity="3.351" />
</joint>
<link name="L_SHOULDER_P_S">
<inertial>
<origin xyz="-0.00982258725282134 0.0704593083751867 1.15261874861217E-06" rpy="0 0 0" />
<mass value="0.880737519698403" />
<inertia ixx="0.000583190308378148" ixy="-1.53153074560473E-05" ixz="6.58466471754072E-09" iyy="0.000445532440067177" iyz="6.37001404038516E-09" izz="0.000465648071069952" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_P_S.STL" />
</geometry>
<material
name="">
<color rgba="0.898039215686275 0.917647058823529 0.929411764705882 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_SHOULDER_R" type="revolute">
<origin xyz="0.035 0.0765 0" rpy="0 0 0" />
<parent link="L_SHOULDER_P_S" />
<child link="L_SHOULDER_R_S" />
<axis xyz="1 0 0" />
<limit lower="-3.14" upper="3.14" effort="100" velocity="3.351" />
</joint>
<link name="L_SHOULDER_R_S">
<inertial>
<origin xyz="-0.0346025282975857 0.091739327988169 -1.67085072999562E-08" rpy="0 0 0" />
<mass value="0.594788424442483" />
<inertia ixx="0.000380970396712656" ixy="4.80751844573946E-05" ixz="-1.34913746586865E-11" iyy="0.000320962178597474" iyz="-1.00039763442409E-09" izz="0.000414771047901155" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_SHOULDER_Y" type="revolute">
<origin xyz="-0.035 0.1475 0" rpy="0 0 0" />
<parent link="L_SHOULDER_R_S" />
<child link="L_SHOULDER_Y_S" />
<axis xyz="0 1 0" />
<limit lower="-3.14" upper="3.14" effort="49" velocity="3.8758" />
</joint>
<link name="L_SHOULDER_Y_S">
<inertial>
<origin xyz="-0.00440976862014801 0.0863620459174068 9.50748668682166E-09" rpy="0 0 0" />
<mass value="0.563406026626801" />
<inertia ixx="0.000327003834287552" ixy="-1.8057438966771E-05" ixz="7.28778136267901E-10" iyy="0.000213830709361531" iyz="1.49273668517817E-10" izz="0.000297341029639189" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_SHOULDER_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_ELBOW_R" type="revolute">
<origin xyz="0.034 0.1025 0" rpy="0 0 0" />
<parent link="L_SHOULDER_Y_S" />
<child link="L_ELBOW_R_S" />
<axis xyz="1 0 0" />
<limit lower="-3.14" upper="3.14" effort="49" velocity="3.8758" />
</joint>
<link name="L_ELBOW_R_S">
<inertial>
<origin xyz="-0.0335624237303443 0.0603199964106564 2.99655911639718E-07" rpy="0 0 0" />
<mass value="0.393571904406493" />
<inertia ixx="0.00017277119386253" ixy="2.34549801867355E-05" ixz="-1.90560556659271E-09" iyy="0.000155340245267897" iyz="8.82493073600007E-09" izz="0.00018104232734159" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_ELBOW_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_ELBOW_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_WRIST_P" type="revolute">
<origin xyz="-0.034 0.0965 0" rpy="0 0 0" />
<parent link="L_ELBOW_R_S" />
<child link="L_WRIST_P_S" />
<axis xyz="0 1 0" />
<limit lower="-3.14" upper="3.14" effort="26" velocity="4.19" />
</joint>
<link name="L_WRIST_P_S">
<inertial>
<origin xyz="-1.39659173115092E-10 0.0675972604744393 0.019200551565574" rpy="0 0 0" />
<mass value="0.442332465815496" />
<inertia ixx="0.000476754055455895" ixy="-4.61505824713347E-15" ixz="6.03808112272229E-18" iyy="0.000103466585850499" iyz="-4.88426705501198E-05" izz="0.000482956345754213" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.698039215686274 0.698039215686274 0.698039215686274 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_WRIST_Y" type="revolute">
<origin xyz="0 0.1525 0.039" rpy="0 0 0" />
<parent link="L_WRIST_P_S" />
<child link="L_WRIST_Y_S" />
<axis xyz="0 0 1" />
<limit lower="-3.14" upper="3.14" effort="26" velocity="4.71" />
</joint>
<link name="L_WRIST_Y_S">
<inertial>
<origin xyz="-0.00464135887331864 -5.06426789392833E-10 -0.0341253783666609" rpy="0 0 0" />
<mass value="0.23573847772002" />
<inertia ixx="5.10686940455785E-05" ixy="7.45706654358429E-16" ixz="6.2619568923368E-06" iyy="6.00636019624519E-05" iyz="1.27989654155383E-16" izz="5.32182903609189E-05" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.647058823529412 0.619607843137255 0.588235294117647 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="L_WRIST_R" type="revolute">
<origin xyz="0.0258 0 -0.039" rpy="0 0 0" />
<parent link="L_WRIST_Y_S" />
<child link="L_WRIST_R_S" />
<axis xyz="1 0 0" />
<limit lower="-3.14" upper="3.14" effort="26" velocity="4.71" />
</joint>
<link name="L_WRIST_R_S">
<inertial>
<origin xyz="-0.0161477357620243 0.0955046558857351 -0.00499392489444117" rpy="0 0 0" />
<mass value="0.504894112043562" />
<inertia ixx="0.000186833711781125" ixy="-3.99488705622731E-06" ixz="-1.51920720398336E-06" iyy="0.000179960980467837" iyz="3.6992669535716E-06" izz="0.000280756101568308" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/L_WRIST_R_S.STL" />
</geometry>
</collision>
</link>
</robot>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,322 @@
<?xml version="1.0" encoding="utf-8"?>
<robot name="right_arm">
<mujoco>
<compiler
meshdir="meshes"
balanceinertia="true"
discardvisual="false" />
</mujoco>
<link name="PELVIS_S">
<inertial>
<origin xyz="3.78529e-05 3.81781e-07 0.0386396" rpy="0 0 0"/>
<mass value="2.10624590271277"/>
<inertia
ixx="0.00165706865324979" ixy="3.13663e-09" ixz="6.84210e-07"
iyy="0.00136736758630241" iyz="-1.01015e-10" izz="0.00168295663089359"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="meshes/PELVIS_S.STL"/>
</geometry>
<material name="light_gray">
<color rgba="0.698 0.698 0.698 1"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="meshes/PELVIS_S.STL"/>
</geometry>
</collision>
</link>
<link name="R_SHOULDER_P_S">
<inertial>
<origin xyz="-0.00982258725282141 -0.0704593093873431 -1.15069920925137E-06" rpy="0 0 0" />
<mass value="0.880737519698404" />
<inertia
ixx="0.000583190308378149"
ixy="1.53153074560471E-05"
ixz="-6.58466471736542E-09"
iyy="0.000445532440067178"
iyz="6.37001403998779E-09"
izz="0.000465648071069953" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin
xyz="0 0 0"
rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_SHOULDER_P" type="revolute">
<origin xyz="0 -0.0945 0.042" rpy="0 0 0" />
<parent link="PELVIS_S" />
<child link="R_SHOULDER_P_S" />
<axis xyz="0 -1 0" />
<limit lower="-1.57" upper="1.57" effort="120" velocity="3.351" />
</joint>
<link name="R_SHOULDER_R_S">
<inertial>
<origin xyz="-0.0346025282975784 -0.09173932900033 1.86280643132974E-08" rpy="0 0 0" />
<mass value="0.59478842444248" />
<inertia
ixx="0.000380970396712653" ixy="-4.80751844573946E-05" ixz="1.34913746041655E-11"
iyy="0.000320962178597472" iyz="-1.00039763417375E-09" izz="0.000414771047901152" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_SHOULDER_R" type="revolute">
<origin xyz="0.035 -0.0765 0" rpy="0 0 0" />
<parent link="R_SHOULDER_P_S" />
<child link="R_SHOULDER_R_S" />
<axis xyz="1 0 0" />
<limit lower="-2" upper="2" effort="120" velocity="3.351" />
</joint>
<link name="R_SHOULDER_Y_S">
<inertial>
<origin xyz="-0.00440976862014946 -0.0863620469295699 -7.58791862676134E-09" rpy="0 0 0" />
<mass value="0.563406026626801" />
<inertia
ixx="0.000327003834287551"
ixy="1.80574389667711E-05"
ixz="-7.28778136077729E-10"
iyy="0.000213830709361532"
iyz="1.49273668428957E-10"
izz="0.000297341029639189" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_SHOULDER_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_SHOULDER_Y" type="revolute">
<origin xyz="-0.035 -0.1475 0" rpy="0 0 0" />
<parent link="R_SHOULDER_R_S" />
<child link="R_SHOULDER_Y_S" />
<axis xyz="0 -1 0" />
<limit lower="0" upper="3.14" effort="80" velocity="3.8758" />
</joint>
<link name="R_ELBOW_R_S">
<inertial>
<origin xyz="-0.0335624237303424 -0.0603199974228191 -2.97736340082455E-07" rpy="0 0 0" />
<mass value="0.393571904406492" />
<inertia
ixx="0.000172771193862529"
ixy="-2.34549801867353E-05"
ixz="1.90560556668771E-09"
iyy="0.000155340245267897"
iyz="8.82493073602971E-09"
izz="0.00018104232734159" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_ELBOW_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_ELBOW_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_ELBOW_R" type="revolute">
<origin xyz="0.034 -0.1025 0" rpy="0 0 0" />
<parent link="R_SHOULDER_Y_S" />
<child link="R_ELBOW_R_S" />
<axis xyz="1 0 0" />
<limit lower="0" upper="2.18" effort="80" velocity="3.8758" />
</joint>
<link name="R_WRIST_P_S">
<inertial>
<origin xyz="-1.39656577968772E-10 -0.0675972614865965 0.0192005515655728" rpy="0 0 0" />
<mass value="0.442332465815497" />
<inertia
ixx="0.000476754055455896"
ixy="-4.61462558956188E-15"
ixz="-5.91762926004267E-18"
iyy="0.0001034665858505"
iyz="4.88426705501212E-05"
izz="0.000482956345754214" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_P_S.STL" />
</geometry>
<material name="">
<color rgba="0.647058823529412 0.619607843137255 0.588235294117647 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_P_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_WRIST_P" type="revolute">
<origin xyz="-0.034 -0.0965 0" rpy="0 0 0" />
<parent link="R_ELBOW_R_S" />
<child link="R_WRIST_P_S" />
<axis xyz="0 -1 0" />
<limit lower="-3.14" upper="0" effort="50" velocity="4.71" />
</joint>
<link name="R_WRIST_Y_S">
<inertial>
<origin xyz="-0.00464135887330083 -5.06426456325926E-10 -0.0341253783666614" rpy="0 0 0" />
<mass value="0.235738477720019" />
<inertia
ixx="5.1068694045578E-05"
ixy="7.45771487459288E-16"
ixz="6.26195689233664E-06"
iyy="6.00636019624515E-05"
iyz="1.27828046342987E-16"
izz="5.32182903609188E-05" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_Y_S.STL" />
</geometry>
<material name="">
<color rgba="0.647058823529412 0.619607843137255 0.588235294117647 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_Y_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_WRIST_Y" type="revolute">
<origin xyz="0 -0.1525 0.039" rpy="0 0 0" />
<parent link="R_WRIST_P_S" />
<child link="R_WRIST_Y_S" />
<axis xyz="0 0 1" />
<limit lower="0" upper="0.78" effort="50" velocity="0.79" />
</joint>
<link name="R_WRIST_R_S">
<inertial>
<origin xyz="-0.0201642233698132 -0.11074968386657 -0.00598955339232021" rpy="0 0 0" />
<mass value="0.204366058218534" />
<inertia
ixx="0.000185559065855467"
ixy="5.75889766751509E-06"
ixz="2.40683898454438E-06"
iyy="0.000131246007872084"
iyz="1.60533277040148E-06"
izz="0.000271800951396149" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_R_S.STL" />
</geometry>
<material name="">
<color rgba="0.890196078431372 0.890196078431372 0.913725490196078 1" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<mesh filename="meshes/R_WRIST_R_S.STL" />
</geometry>
</collision>
</link>
<joint name="R_WRIST_R" type="revolute">
<origin xyz="0.03 0 -0.039" rpy="0 0 0" />
<parent link="R_WRIST_Y_S" />
<child link="R_WRIST_R_S" />
<axis xyz="1 0 0" />
<limit lower="-0.26" upper="1.57" effort="50" velocity="4.71" />
</joint>
<link name="R_FINGER_TIP">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="0"/>
<inertia ixx="1e-6" ixy="0" ixz="0" iyy="1e-6" iyz="0" izz="1e-6"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.002"/>
</geometry>
<material name="">
<color rgba="0 1 0 1"/> <!-- 绿色 -->
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.005"/>
</geometry>
</collision>
</link>
<joint name="R_FINGER_TIP_FIXED" type="fixed">
<origin xyz="-0.01212 -0.17655 0.07506" rpy="3.14159 0 -1.5708"/>
<parent link="R_WRIST_R_S"/>
<child link="R_FINGER_TIP"/>
</joint>
</robot>

View File

@ -0,0 +1,133 @@
<template>
<div class="container">
<div class="joint-control" v-for="joint in jointList" :key="joint.name">
<div class="joint-header">
<span class="joint-name">{{ joint.displayName }}</span>
<span class="joint-type">{{ joint.type.toUpperCase() }}</span>
</div>
<div class="joint-info">
<span class="angle-display">{{ formatAngle(joint.value) }}</span>
<span class="limits">[{{ formatAngle(joint.min) }}, {{ formatAngle(joint.max) }}]</span>
</div>
<div class="slider-container">
<el-slider size="small" v-model="joint.value" :min="joint.min" :max="joint.max" :step="joint.step" @input="(value) => updateJointAngle(joint.name, value)"/>
<div class="slider-labels">
<span>{{ formatAngle(joint.min) }}</span>
<span>{{ formatAngle(joint.max) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
jointList: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['updateJointAngle'])
const updateJointAngle = (name, value) => {
emits('updateJointAngle', { name, value })
}
/**
* 角度格式化
*/
const formatAngle = (radians) => {
const degrees = (radians * 180 / Math.PI).toFixed(1)
return `${degrees}°`
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
.joint-control {
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
border: 1px solid #cfcfcf;
.joint-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.joint-name {
font-weight: 600;
font-size: 14px;
}
.joint-type {
font-size: 11px;
background: rgba(52, 152, 219, 0.3);
color: #3498db;
padding: 3px 8px;
border-radius: 12px;
font-weight: 500;
}
}
.joint-info {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 13px;
.angle-display {
color: #f1c40f;
font-weight: 600;
font-size: 14px;
}
.limits {
color: #95a5a6;
font-size: 11px;
font-family: monospace;
}
}
.slider-container {
padding: 0 5px;
.joint-slider {
width: 100%;
height: 8px;
-webkit-appearance: none;
background: #34495e;
border-radius: 4px;
outline: none;
margin-bottom: 8px;
cursor: pointer;
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #ecf0f1;
cursor: pointer;
border: 2px solid #3498db;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #7f8c8d;
margin-top: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,394 @@
<template>
<!-- 模板部分保持不变 -->
<div class="urdf-viewer-container">
<div ref="viewerContainer" class="urdf-viewer"></div>
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<p>正在解析URDF并加载模型...</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, reactive, computed } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import URDFLoader from 'urdf-loader'
const props = defineProps({
basePath: { type: String, required: true },
urdfPath: { type: String, required: true },
modelColor: { type: String, default: '#cbcbcb' },
showJointLimits: { type: Boolean, default: true }
})
const emits = defineEmits(['computedChange'])
const viewerContainer = ref(null)
const isLoading = ref(false)
// Three.js 核心对象
let scene = null
let camera = null
let renderer = null
let controls = null
let robot = null
// 关节控制相关状态
const jointControls = reactive([])
const availableJoints = ref({})
/**
* 从URDF XML中提取关节信息(使用浏览器原生DOMParser)
*/
const extractJointInfoFromURDF = (urdfXml) => {
try {
const joints = []
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(urdfXml, 'text/xml')
// 检查解析错误
const parseError = xmlDoc.getElementsByTagName('parsererror')
if (parseError.length > 0) {
throw new Error('XML解析错误: ' + parseError[0].textContent)
}
// 获取所有joint元素
const jointElements = xmlDoc.getElementsByTagName('joint')
for (let i = 0; i < jointElements.length; i++) {
const jointElement = jointElements[i]
const name = jointElement.getAttribute('name')
const type = jointElement.getAttribute('type')
// 只处理可动关节
if (type && ['revolute', 'continuous', 'prismatic'].includes(type)) {
const jointInfo = {
name: name,
displayName: name.replace(/_/g, ' '),
type: type,
value: 0,
step: 0.01
}
// 提取关节限制
const limitElements = jointElement.getElementsByTagName('limit')
if (limitElements.length > 0) {
const limit = limitElements[0]
const lower = limit.getAttribute('lower')
const upper = limit.getAttribute('upper')
const effort = limit.getAttribute('effort')
const velocity = limit.getAttribute('velocity')
// 处理旋转关节
if (type === 'revolute' || type === 'continuous') {
jointInfo.min = lower !== null ? parseFloat(lower) :
(type === 'revolute' ? -Math.PI : -Math.PI)
jointInfo.max = upper !== null ? parseFloat(upper) :
(type === 'revolute' ? Math.PI : Math.PI)
}
// 处理平移关节
else if (type === 'prismatic') {
jointInfo.min = lower !== null ? parseFloat(lower) : -1.0
jointInfo.max = upper !== null ? parseFloat(upper) : 1.0
jointInfo.step = 0.001
}
if (effort) jointInfo.maxEffort = parseFloat(effort)
if (velocity) jointInfo.maxVelocity = parseFloat(velocity)
} else {
// 如果没有限制,设置默认范围
if (type === 'continuous') {
jointInfo.min = -Math.PI
jointInfo.max = Math.PI
} else if (type === 'revolute') {
jointInfo.min = -Math.PI / 2
jointInfo.max = Math.PI / 2
} else if (type === 'prismatic') {
jointInfo.min = -0.5
jointInfo.max = 0.5
}
}
// 提取关节轴
const axisElements = jointElement.getElementsByTagName('axis')
if (axisElements.length > 0) {
const axis = axisElements[0].getAttribute('xyz')
if (axis) {
const axisValues = axis.split(' ').map(Number)
jointInfo.axis = new THREE.Vector3(...axisValues)
}
}
// 提取关节原点
const originElements = jointElement.getElementsByTagName('origin')
if (originElements.length > 0) {
const origin = originElements[0]
const xyz = origin.getAttribute('xyz')
const rpy = origin.getAttribute('rpy')
if (xyz) jointInfo.origin = xyz.split(' ').map(Number)
if (rpy) jointInfo.orientation = rpy.split(' ').map(Number)
}
joints.push(jointInfo)
}
}
// 按关节名称排序
joints.sort((a, b) => a.name.localeCompare(b.name))
console.log('从URDF提取的关节信息:', joints)
return joints
} catch (error) {
console.error('解析URDF失败:', error)
return []
}
}
/**
* 初始化Three.js场景
*/
const initScene = () => {
const container = viewerContainer.value
scene = new THREE.Scene()
scene.background = new THREE.Color(0xf0f0f0)
camera = new THREE.PerspectiveCamera(
75,
container.clientWidth / container.clientHeight,
0.01,
1000
)
camera.position.set(2, 2, 3)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(container.clientWidth, container.clientHeight)
container.appendChild(renderer.domElement)
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(10, 20, 10)
scene.add(directionalLight)
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
}
/**
* 加载URDF机器人模型
*/
const loadRobot = async () => {
isLoading.value = true
try {
// 清除现有模型
if (robot) {
scene.remove(robot)
jointControls.length = 0
availableJoints.value = {}
}
// 1. 加载URDF文件内容
const urdfUrl = `${props.basePath}${props.urdfPath}`
const response = await fetch(urdfUrl)
const urdfXml = await response.text()
// 2. 解析URDF提取关节信息(同步调用)
const extractedJoints = extractJointInfoFromURDF(urdfXml)
// 3. 使用URDFLoader加载模型
const manager = new THREE.LoadingManager()
const loader = new URDFLoader(manager)
loader.packages = { '': props.basePath }
robot = await new Promise((resolve, reject) => {
loader.load(urdfUrl, resolve, undefined, reject)
})
// 4. 坐标轴修正
robot.rotation.x = -Math.PI / 2
robot.rotation.z = -Math.PI / 2
robot.updateMatrixWorld(true)
// 应用颜色
robot.traverse((child) => {
if (child.isMesh && child.material) {
child.material.color.set(props.modelColor)
}
})
scene.add(robot)
// 5. 初始化关节控制系统
availableJoints.value = robot.joints
// 将提取的关节信息与加载的关节匹配
extractedJoints.forEach(jointInfo => {
if (availableJoints.value[jointInfo.name]) {
// 获取当前关节值
const currentValue = availableJoints.value[jointInfo.name].jointValue || 0
// 确保初始值在限制范围内
let initialValue = currentValue
if (props.showJointLimits) {
initialValue = Math.max(jointInfo.min, Math.min(jointInfo.max, currentValue))
}
// 添加到控制列表
jointControls.push({
...jointInfo,
value: initialValue
})
// 设置初始关节位置
availableJoints.value[jointInfo.name].setJointValue(initialValue)
} else {
console.warn(`关节 ${jointInfo.name} 在URDF模型中不存在或不可控`)
}
})
fitCameraToObject(robot)
} catch (error) {
console.error('加载URDF模型失败:', error)
} finally {
isLoading.value = false
}
}
/**
* 更新关节角度
*/
const updateJointAngle = (jointName, angle) => {
if (availableJoints.value[jointName]) {
try {
// 应用限制(如果启用)
let clampedAngle = angle
if (props.showJointLimits) {
const jointInfo = jointControls.find(j => j.name === jointName)
if (jointInfo) {
clampedAngle = Math.max(jointInfo.min, Math.min(jointInfo.max, angle))
}
}
// 应用关节角度
availableJoints.value[jointName].setJointValue(clampedAngle)
// 更新UI中的值
const jointControl = jointControls.find(j => j.name === jointName)
if (jointControl) {
jointControl.value = clampedAngle
}
} catch (error) {
console.error(`设置关节 ${jointName} 角度失败:`, error)
}
}
}
/**
* 调整相机视角
*/
const fitCameraToObject = (object) => {
const boundingBox = new THREE.Box3().setFromObject(object)
const center = boundingBox.getCenter(new THREE.Vector3())
const size = boundingBox.getSize(new THREE.Vector3())
const maxSize = Math.max(size.x, size.y, size.z)
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camera.fov) / 360))
const fitWidthDistance = fitHeightDistance / camera.aspect
const distance = 1.5 * Math.max(fitHeightDistance, fitWidthDistance)
const direction = new THREE.Vector3(0, 1, 1).normalize()
controls.target.copy(center)
camera.position.copy(center).add(direction.multiplyScalar(distance))
controls.update()
}
/**
* 动画循环
*/
const animate = () => {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
/**
* 窗口大小变化处理
*/
const onWindowResize = () => {
const container = viewerContainer.value
camera.aspect = container.clientWidth / container.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(container.clientWidth, container.clientHeight)
}
/**
* 清理资源
*/
const dispose = () => {
window.removeEventListener('resize', onWindowResize)
controls?.dispose()
renderer?.dispose()
}
// 监听属性变化
watch(
() => [props.basePath, props.urdfPath],
() => {
loadRobot()
}
)
watch(jointControls, (newVal) => {
emits('computedChange', newVal)
})
// 生命周期
onMounted(() => {
initScene()
loadRobot()
window.addEventListener('resize', onWindowResize)
animate()
})
onBeforeUnmount(() => {
dispose()
})
defineExpose({
updateJointAngle
})
</script>
<style scoped>
/* 样式部分保持不变 */
.urdf-viewer-container {
height: 100%;
width: 100%;
.urdf-viewer {
width: 100%;
height: 100%;
overflow: hidden;
}
}
.loading-overlay p {
font-size: 16px;
color: #ecf0f1;
}
</style>

View File

@ -1,565 +0,0 @@
<script setup>
import {useRoute} from "vue-router"
import dayjs from "dayjs";
import {deviceTeach, deviceUpdateProfile} from "@/api/device/register.js";
import {formatNumber} from "@/views/device/register/components/index.js";
import {convertNumbersToStringAndRemoveNulls} from "@/utils/fn.js";
const data = reactive({
form: {
workModule: 1,//工作模式
posStep: 1,//位置步进
postureStep: 1,//姿态步进
jointStep: 1,//关节步进
joint1: 0.000012,//关节1
joint2: 0.000012,//
joint3: 0.000012,//
joint4: 0.000012,//
joint5: 0.000012,//
joint6: 0.000012,//关节6
x: 11,//
y: 11,//
z: 11,//
rx: 11,//
ry: 11,//
rz: 11,//
arm: 1,//目标
coordinate: 1,//坐标系
},
speed: 20,//速度
zeroPos: {//零位姿态
x: 0,
y: 0,
z: 0,
rx: 0,
ry: 0,
rz: 0,
},
initPos: {
x: 1,
y: 1,
z: 1,
rx: 1,
ry: 1,
rz: 1,
},//初始姿态
armList: [
{
label: '机械臂1',
value: 1
},
{
label: '机械臂2',
value: 2
}
],
coordinateList: [
{
label: 'x轴',
value: 1
},
{
label: 'y轴',
value: 2
},
{
label: 'z轴',
value: 3
}
],
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
})
const {
form, time,
zeroPos,
initPos,
armList,
coordinateList
} = toRefs(data)
const route = useRoute()
let timer = null
onUnmounted(() => {
clearInterval(timer)
})
//获取机械臂参数
const getDeviceInfo = () => {
deviceTeach(devId.value).then((res) => {
let data = formatNumber(res.data)//字符类型转数字类型
Object.assign(form.value, data)
//设置初始位置
initPos.value.x = data.x
initPos.value.y = data.y
initPos.value.z = data.z
initPos.value.rx = data.rx
initPos.value.ry = data.ry
initPos.value.rz = data.rz
})
}
onMounted(() => {
getDeviceInfo()
const posControls = document.querySelectorAll(".btn")
//监听按钮点击事件
posControls.forEach((item) => {
item.addEventListener('click', (e) => {
const target = e.currentTarget.classList[1]
const vals = target.split("_")//x_0 rx_0
const pos = vals[0]
const calc = !!Number(vals[1])
if (calc) {
form.value[pos] += 0.01 * data.speed
} else {
form.value[pos] -= 0.01 * data.speed
}
})
})
//监听失焦事件
// const warp = document.querySelector(".mechanical-arm")
// warp.addEventListener('focusout', handleFocus, false)
timer = setInterval(() => {
time.value = dayjs().format("YYYY-MM-DD HH:mm:ss")
getDeviceInfo()
}, 1000)
})
watch(() => JSON.parse(JSON.stringify(form.value)), (newData, oldData) => {
// const change = findDifferences(oldData, newData)
const data = convertNumbersToStringAndRemoveNulls(newData)
deviceUpdateProfile({idDeDeviceRegistration: devId.value, ...data}).then((res)=>{
console.log(res)
})
})
const handleFocus = (e) => {
const type = e.target.type
if (type === "number") {
// console.log("数字框失焦")
}
}
const devId = computed(() => {
return route.path.split("/")[2]
})
const deviceModel = computed(() => {
return route.path.split("/")[1]
})
const handleResets = (type) => {
if (type === "zero") {
Object.assign(form.value, zeroPos.value)
return
}
if (type === "init") {
Object.assign(form.value, initPos.value)
}
}
</script>
<template>
<div class="mechanical-arm">
<div class="top flex between">
<div class="left " style="width: 38%">
<div class="card" style="height: 100%">
<div class="title">机械臂</div>
<div class="body">机械臂图片</div>
</div>
</div>
<div class="center" style="width: 22%">
<div class="card">
<div class="title">工作模式</div>
<div class="body">
<el-radio-group v-model="form.workModule">
<el-radio :value="1">真实机械臂</el-radio>
<el-radio :value="2">仿真机械臂</el-radio>
</el-radio-group>
</div>
</div>
<div class="card" style="margin-top: 10px">
<div class="title">步进模式</div>
<div class="mb10">
<div class="mb5 action-label">位置步进( mm )</div>
<el-input-number style="width: 100%;" v-model="form.posStep" :step="2"/>
</div>
<div class="mb10">
<div class="mb5 action-label">姿态步进( deg )</div>
<el-input-number style="width: 100%;" v-model="form.postureStep" :step="2"/>
</div>
<div>
<div class="mb5 action-label">关节步进( deg )</div>
<el-input-number style="width: 100%;" v-model="form.jointStep" :step="2"/>
</div>
</div>
</div>
<div class="right " style="width: 38%">
<div class="card" style="height: 100%">
<div class="title">位置控制</div>
<div class="body">
<div class="btn-wrap pos-control" style="transform: rotateX(40deg);height: calc(100% - 70px)">
<div class="btn z_0">
<span>Z-</span>
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn x_0">
<span> X-</span>
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn z_1">
<span>Z+</span>
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn y_0">
<span>Y-</span>
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn"></div>
<div class="btn y_1">
<span>Y+</span>
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn"></div>
<div class="btn x_1">
<span>X+</span>
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn"></div>
</div>
<div>
<div class="mb10 action-label">参考坐标系</div>
<el-select
v-model="form.coordinate"
>
<el-option
v-for="item in coordinateList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
</div>
</div>
<div class="bottom flex between">
<div class="left " style="width: 30%">
<div class="card" style="height: 100%">
<div class="title">机械臂位置姿态</div>
<div class="body">
<div class="flex mb10">
<div class="left flex1">
<div class="pos-title mb10">位置 ( m )</div>
<div class="mb10"> X: {{ form.x.toFixed(6) }}</div>
<div class="mb10"> Y: {{ form.y.toFixed(6) }}</div>
<div class="mb10"> Z: {{ form.z.toFixed(6) }}</div>
</div>
<div class="right flex1">
<div class="pos-title mb10">姿态 ( deg )</div>
<div class="mb10"> RX: {{ form.rx.toFixed(6) }}</div>
<div class="mb10"> RY: {{ form.ry.toFixed(6) }}</div>
<div class="mb10"> RZ: {{ form.rz.toFixed(6) }}</div>
</div>
</div>
<div>
<div class="mb10 action-label">目标</div>
<el-select
v-model="form.arm"
>
<el-option
v-for="item in armList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
</div>
<div class="center " style="width: 30%">
<div class="card" style="height: 100%">
<div class="title">关节控制单位( deg )</div>
<div class="body">
<div class="mb10 action-label-pos">
<div class="action-label">关节1</div>
<el-input-number v-model="form.joint1" :step="0.000001" :precision="6"/>
</div>
<div class="mb10 action-label-pos">
<div class="action-label">关节2</div>
<el-input-number v-model="form.joint2" :step="0.000001" :precision="6"/>
</div>
<div class="mb10 action-label-pos">
<div class="action-label">关节3</div>
<el-input-number v-model="form.joint3" :step="0.000001" :precision="6"/>
</div>
<div class="mb10 action-label-pos">
<div class="action-label">关节4</div>
<el-input-number v-model="form.joint4" :step="0.000001" :precision="6"/>
</div>
<div class="mb10 action-label-pos">
<div class="action-label">关节5</div>
<el-input-number v-model="form.joint5" :step="0.000001" :precision="6"/>
</div>
<div class="mb10 action-label-pos">
<div class="action-label">关节6</div>
<el-input-number v-model="form.joint6" :step="0.000001" :precision="6"/>
</div>
</div>
</div>
</div>
<div class="right " style="width: 38%">
<div class="card" style="height: 100%">
<div class="title">姿态控制</div>
<div class="body">
<div class="btn-wrap">
<div class="btn rz_0 c">
<span>Z-</span>
<svg t="1747126605366" class="turn-lb" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn rx_0 c">
<span>X-</span>
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn rz_1 c">
<span>Z+</span>
<svg t="1747126605366" class="turn-rb" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn ry_0 c">
<span> Y-</span>
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn"></div>
<div class="btn ry_1 c">
<span>Y+</span>
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn"></div>
<div class="btn rx_1 c">
<span> X+</span>
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
<path
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
fill="#4076ff" p-id="1462"></path>
</svg>
</div>
<div class="btn"></div>
</div>
</div>
</div>
</div>
</div>
<div class="actions flex align-center between">
<div class="left">
<el-button @click="handleResets('zero')">零位姿态</el-button>
<el-button @click="handleResets('init')">初始位姿</el-button>
</div>
<div class="center">
{{ time }}
</div>
<div class="right flex align-center">
<div style="width: 100px;user-select: none">速度</div>
<el-slider size="small" :show-tooltip="false" v-model="data.speed"/>
<div style="margin-left: 20px"> {{ data.speed }}%</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.mechanical-arm {
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.top {
padding: 10px 10px 0;
height: 48%;
.center {
display: flex;
flex-direction: column;
.card:last-child {
flex: 1;
}
}
}
.bottom {
padding: 0 10px;
height: 44%;
}
.actions {
height: 6%;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
padding: 0 10px;
.right {
width: 300px;
}
.center {
user-select: none;
}
}
.action-label {
color: #606266;
font-size: 14px;
user-select: none;
}
.action-label-pos {
display: flex;
align-items: center;
justify-content: space-between;
.el-input-number {
width: calc(100% - 50px);
}
}
.card {
border-radius: 8px;
border: 1px solid #e4e7ed;
padding: 10px;
.title {
color: #333;
font-size: 14px;
margin-bottom: 10px;
font-weight: 900;
user-select: none;
}
.body {
height: calc(100% - 29px);
overflow-y: auto;
.pos-title {
user-select: none;
}
.btn-wrap {
height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
justify-items: center;
align-items: center;
.btn {
cursor: pointer;
span {
user-select: none;
}
svg {
width: 60px;
height: 60px;
transform-origin: center center;
}
}
.turn-lb {
transform: rotate(-135deg);
}
.turn-rb {
transform: rotate(135deg);
}
.turn-left {
transform: rotate(-90deg);
}
.turn-right {
transform: rotate(90deg);
}
.turn-bottom {
transform: rotate(-180deg);
}
}
}
}
}
</style>

View File

@ -1,28 +1,10 @@
<script setup>
import { reactive, toRefs, onMounted, onUnmounted, watch, computed, ref } from "vue";
import { useRoute } from "vue-router";
import dayjs from "dayjs";
import { deviceTeach } from "@/api/device/register.js";
import { formatNumber } from "@/views/device/register/components/index.js";
import { convertNumbersToStringAndRemoveNulls } from "@/utils/fn.js";
import {
moveJ,
status
} from '@/api/device/arm';
import { reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
import UrdfViewer from "./components/UrdfViewer.vue";
import JointControl from "./components/JointControl.vue";
const data = reactive({
form: {
workModule: 1,
posStep: 1,
postureStep: 1,
jointStep: 1,
joint1: 0.000012,
joint2: 0.000012,
joint3: 0.000012,
joint4: 0.000012,
joint5: 0.000012,
joint6: 0.000012,
joint7: 0.000012,
x: 11,
y: 11,
z: 11,
@ -30,207 +12,153 @@ const data = reactive({
ry: 11,
rz: 11,
arm: 1,
coordinate: 1,
},
speed: 20,
zeroPos: {
x: 0,
y: 0,
z: 0,
rx: 0,
ry: 0,
rz: 0,
},
initPos: {
x: 1,
y: 1,
z: 1,
rx: 1,
ry: 1,
rz: 1,
},
armList: [
{ label: '目标1', value: 1 },
{ label: '目标2', value: 2 },
{ label: '目标3', value: 3 },
{ label: "目标1", value: 1 },
{ label: "目标2", value: 2 },
{ label: "目标3", value: 3 },
],
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
const terminalId = ref('25449ff3fcc7b27da5f69462c5efcec2');
const deviceId = ref('spk1');
const { form, time, zeroPos, initPos, armList } = toRefs(data);
const route = useRoute();
const { form, armList } = toRefs(data);
const leftJointList = ref(null);
const rightJointList = ref(null);
const computedChange = (val) => {
leftJointList.value = val.filter((item) => {
return item.name[0] === "L";
});
rightJointList.value = val.filter((item) => {
return item.name[0] === "R";
});
};
const urdfViewerRef = ref(null);
const updateJointAngle = ({ name, value }) => {
if (urdfViewerRef.value) {
urdfViewerRef.value.updateJointAngle(name, value);
}
};
const coordinateSystem = ref(null); // 用于获取容器元素
const buttonPositions = ref([]); // 存储按钮的动态位置
// 调整后的原始坐标(基于默认图片尺寸 1025x817px)
const initialPositions = [
{ target: "z_plus", x: 500, y: 80, label: "Z+" },
{ target: "rz_plus", x: 675, y: 124, label: "RZ+" },
{ target: "x_plus", x: 796, y: 245, label: "X+" },
{ target: "ry_plus", x: 840, y: 410, label: "RY+" },
{ target: "y_minus", x: 796, y: 570, label: "Y-" },
{ target: "ry_minus", x: 675, y: 696, label: "RY-" },
{ target: "z_minus", x: 500, y: 745, label: "Z-" },
{ target: "rx_minus", x: 345, y: 696, label: "RX-" },
{ target: "x_minus", x: 206, y: 570, label: "X-" },
{ target: "rx_plus", x: 180, y: 410, label: "RX+" },
{ target: "y_plus", x: 204, y: 245, label: "Y+" },
{ target: "rz_minus", x: 345, y: 124, label: "RZ-" },
const buttonDirections = [
{ target: "z_plus", direction: "top", label: "Z+" }, // left
{ target: "rz_plus", direction: "top-right", label: "RZ+" },
{ target: "x_plus", direction: "right", label: "X+" }, // bottom
{ target: "ry_plus", direction: "bottom-right", label: "RY+" },
{ target: "y_minus", direction: "bottom", label: "Y-" },
{ target: "ry_minus", direction: "bottom-left", label: "RY-" },
{ target: "z_minus", direction: "bottom", label: "Z-" },
{ target: "rx_minus", direction: "bottom-left", label: "RX-" },
{ target: "x_minus", direction: "left", label: "X-" },
{ target: "rx_plus", direction: "top-left", label: "RX+" },
{ target: "y_plus", direction: "left", label: "Y+" }, // right
{ target: "rz_minus", direction: "top-left", label: "RZ-" },
];
// 默认图片尺寸
const ORIGINAL_IMAGE_WIDTH = 1025;
const ORIGINAL_IMAGE_HEIGHT = 817;
let timer = null;
onUnmounted(() => {
clearInterval(timer);
});
const getDeviceInfo = () => {
deviceTeach(devId.value).then((res) => {
let dataRes = formatNumber(res.data);
Object.assign(form.value, dataRes);
initPos.value.x = dataRes.x;
initPos.value.y = dataRes.y;
initPos.value.z = dataRes.z;
initPos.value.rx = dataRes.rx;
initPos.value.ry = dataRes.ry;
initPos.value.rz = dataRes.rz;
});
};
const updateButtonPositions = () => {
if (coordinateSystem.value) {
const { width, height } = coordinateSystem.value.getBoundingClientRect();
// 计算图片的实际显示尺寸(考虑 background-size: contain)
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT; // 1025 / 817 ≈ 1.255
const containerAspectRatio = width / height;
const containerRect = coordinateSystem.value.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
let imageWidth, imageHeight, offsetX, offsetY;
// 计算图片的实际显示尺寸(考虑 background-size: contain)
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT;
const containerAspectRatio = containerWidth / containerHeight;
let imageWidth, imageHeight, imageLeft, imageTop;
if (containerAspectRatio > imageAspectRatio) {
// 容器较宽,高度限制图片大小,上下有留白
imageHeight = height;
imageWidth = height * imageAspectRatio;
offsetX = (width - imageWidth) / 2; // 水平居中
offsetY = 0; // 顶部对齐
imageHeight = containerHeight;
imageWidth = imageHeight * imageAspectRatio;
imageLeft = (containerWidth - imageWidth) / 2;
imageTop = 0;
} else {
// 容器较窄,宽度限制图片大小,左右有留白
imageWidth = width;
imageHeight = width / imageAspectRatio;
offsetX = 0; // 左对齐
offsetY = (height - imageHeight) / 2; // 垂直居中
imageWidth = containerWidth;
imageHeight = imageWidth / imageAspectRatio;
imageLeft = 0;
imageTop = (containerHeight - imageHeight) / 2;
}
// 计算缩放比例(基于图片实际显示尺寸)
const scaleX = imageWidth / ORIGINAL_IMAGE_WIDTH;
const scaleY = imageHeight / ORIGINAL_IMAGE_HEIGHT;
// 计算图片中心坐标
const centerX = imageLeft + imageWidth / 2;
const centerY = imageTop + imageHeight / 2;
// 计算圆的半径(根据图片尺寸的较小值比例确定,可调整比例系数)
const radiusScale = 0.85; // 半径系数,0-1之间,越大半径越大
const radius = Math.min(imageWidth, imageHeight) * radiusScale / 2;
// 定义方位对应的角度(弧度制),top为0度,顺时针增加
const directionAngles = {
"top": 0,
"top-right": Math.PI / 6, // 30度
"right": Math.PI / 3, // 60度
"bottom-right": Math.PI / 2, // 90度
"bottom": 2 * Math.PI / 3, // 120度
"bottom-left": 5 * Math.PI / 6, // 150度
"left": Math.PI, // 180度
"top-left": 7 * Math.PI / 6, // 210度
};
// 调整角度以均匀分布12个按钮(360/12=30度,即Math.PI/6弧度)
const totalButtons = buttonDirections.length;
const angleStep = 2 * Math.PI / totalButtons;
// 更新按钮位置
buttonPositions.value = initialPositions.map((pos) => ({
...pos,
left: pos.x * scaleX + offsetX, // 按比例缩放 x 坐标并加上水平偏移
top: pos.y * scaleY + offsetY, // 按比例缩放 y 坐标并加上垂直偏移
}));
buttonPositions.value = buttonDirections.map((btn, index) => {
// 计算每个按钮的角度(从top开始顺时针排列)
const angle = index * angleStep;
// 计算按钮的坐标
const x = centerX + radius * Math.sin(angle);
const y = centerY - radius * Math.cos(angle);
return {
...btn,
left: x,
top: y,
};
});
}
};
onMounted(() => {
getDeviceInfo();
// 初始计算按钮位置
updateButtonPositions();
// 添加按钮事件监听
const posControls = document.querySelectorAll(".pos-btn");
posControls.forEach((item) => {
item.addEventListener('click', (e) => {
const target = e.currentTarget.dataset.target;
const [pos, direction] = target.split("_");
const step = (pos === 'x' || pos === 'y' || pos === 'z') ? form.value.posStep : form.value.postureStep;
const delta = direction === 'plus' ? step : -step;
form.value[pos] += delta * (data.speed / 100);
});
});
// 监听窗口大小变化,更新按钮位置
window.addEventListener('resize', updateButtonPositions);
window.addEventListener("resize", updateButtonPositions);
});
onUnmounted(() => {
window.removeEventListener('resize', updateButtonPositions);
window.removeEventListener("resize", updateButtonPositions);
});
watch(() => JSON.parse(JSON.stringify(form.value)), (newData) => {
const dataToSend = convertNumbersToStringAndRemoveNulls(newData);
moveJ({
"deviceId": deviceId.value,
"terminalId": terminalId.value,
"acc": 0.8,
"vel": 0.8,
"cmds": [
{
"jointName": "R_SHOULDER_P",
"rad": dataToSend.joint1,
"vel": 1.0
},
{
"jointName": "R_SHOULDER_R",
"rad": dataToSend.joint2,
"vel": 1.0
},
{
"jointName": "R_SHOULDER_Y",
"rad": dataToSend.joint3,
"vel": 1.0
},
{
"jointName": "R_ELBOW_R",
"rad": dataToSend.joint4,
"vel": 1.0
},
{
"jointName": "R_WRIST_P",
"rad": dataToSend.joint5,
"vel": 1.0
},
{
"jointName": "R_WRIST_Y",
"rad": dataToSend.joint6,
"vel": 1.0
},
{
"jointName": "R_WRIST_R",
"rad": dataToSend.joint7,
"vel": 1.0
}
]
})
});
const devId = computed(() => {
return route.path.split("/")[2];
});
const handleResets = (type) => {
if (type === "zero") {
Object.assign(form.value, zeroPos.value);
return;
}
if (type === "init") {
Object.assign(form.value, initPos.value);
}
};
</script>
<template>
<div class="mechanical-arm">
<div class="top card">
<div class="controls flex align-center" style="justify-content: flex-start;">
<div class="controls flex align-center" style="justify-content: flex-start">
<div class="control-label">目标</div>
<el-select v-model="form.arm" placeholder="选择目标" style="width: 100px; margin-right: 20px;">
<el-option v-for="item in armList" :key="item.value" :label="item.label" :value="item.value" />
<el-select
v-model="form.arm"
placeholder="选择目标"
style="width: 100px; margin-right: 20px"
>
<el-option
v-for="item in armList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button @click="handleResets('zero')" style="margin-right: 10px;">零位姿态</el-button>
<el-button @click="handleResets('zero')" style="margin-right: 10px"
>零位姿态</el-button
>
<el-button @click="handleResets('init')">初始位置</el-button>
</div>
<div class="pose-info flex align-center">
@ -242,69 +170,52 @@ const handleResets = (type) => {
<div class="pose-item">RZ: {{ form.rz.toFixed(6) }} deg</div>
</div>
</div>
<div class="bottom flex">
<div class="left card" style="flex: 1; position: relative;">
<div class="title">姿态与位置控制</div>
<div class="coordinate-system" ref="coordinateSystem">
<div class="control-buttons">
<button
v-for="(pos, index) in buttonPositions"
:key="index"
class="pos-btn"
:data-target="pos.target"
:style="{ left: `${pos.left}px`, top: `${pos.top}px` }"
>
{{ pos.label }}
</button>
<div class="bottom">
<div class="left">
<UrdfViewer
ref="urdfViewerRef"
:base-path="'/public/models/'"
:urdf-path="'dual_arm.urdf'"
:model-color="'#8a8ae9'"
@computedChange="computedChange"
/>
</div>
<div class="right">
<div class="top">
<div class="secondary-title">
<div class="label">左手</div>
<div class="label">右手</div>
</div>
<div class="jointControl-container">
<JointControl
:jointList="leftJointList"
@updateJointAngle="updateJointAngle"
/>
<JointControl
:jointList="rightJointList"
@updateJointAngle="updateJointAngle"
/>
</div>
</div>
</div>
<div class="right card" style="flex: 1;">
<div class="title">关节控制 (deg)</div>
<div class="input-list">
<div class="input-item">
<span>关节1</span>
<el-input-number v-model="form.joint1" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节2</span>
<el-input-number v-model="form.joint2" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节3</span>
<el-input-number v-model="form.joint3" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节4</span>
<el-input-number v-model="form.joint4" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节5</span>
<el-input-number v-model="form.joint5" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节6</span>
<el-input-number v-model="form.joint6" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节7</span>
<el-input-number v-model="form.joint7" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节步进</span>
<el-input-number v-model="form.jointStep" :step="0.1" :precision="1" />
<div class="bottom">
<div class="left card" style="position: relative">
<div class="coordinate-system" ref="coordinateSystem">
<div class="control-buttons">
<button
v-for="(pos, index) in buttonPositions"
:key="index"
class="pos-btn"
:data-target="pos.target"
:style="{ left: `${pos.left}px`, top: `${pos.top}px` }"
>
{{ pos.label }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer card flex align-center between">
<div class="time">{{ time }}</div>
<div class="speed flex align-center">
<span>速度</span>
<el-slider v-model="data.speed" :min="0" :max="100" style="width: 200px; margin: 0 10px;" />
<span>{{ data.speed }}%</span>
</div>
</div>
</div>
</template>
@ -341,95 +252,97 @@ const handleResets = (type) => {
}
.bottom {
flex: 1;
gap: 20px;
.title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
user-select: none;
}
height: calc(100% - 120px);
display: flex;
gap: 12px;
.left {
display: flex;
flex-direction: column;
position: relative;
.coordinate-system {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background-color: #FFF;
background-image: url('@/assets/images/arm.png');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
.control-buttons {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
.pos-btn {
position: absolute;
width: 40px;
height: 40px;
background: #4076ff;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%); /* 按钮中心对齐坐标 */
}
}
}
flex: 2;
height: 100%;
}
.right {
.input-list {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
.input-item {
.top {
flex: 1;
border: 1px solid #e4e7ed;
padding: 0 8px;
overflow: hidden;
.container-title {
font-size: 16px;
text-align: center;
}
.secondary-title {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: space-around;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
span {
width: 80px;
user-select: none;
}
.el-input-number {
width: calc(100% - 90px);
}
.jointControl-container {
display: flex;
gap: 10px;
height: calc(100% - 75px);
overflow: auto;
}
}
}
}
.footer {
height: 50px;
.bottom {
flex: 1;
.time {
font-size: 14px;
}
.card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
height: 100%;
display: flex;
flex-direction: column;
.speed {
gap: 10px;
.coordinate-system {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background-color: #fff;
background-image: url("@/assets/images/arm.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
span {
user-select: none;
.control-buttons {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
.pos-btn {
position: absolute;
width: 40px;
height: 40px;
background: #4076ff;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%); /* 按钮中心对齐坐标 */
}
}
}
}
}
}
}