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", "socket.io-client": "^4.8.1",
"sockjs-client": "^1.6.1", "sockjs-client": "^1.6.1",
"splitpanes": "3.1.5", "splitpanes": "3.1.5",
"three": "^0.181.2",
"urdf-loader": "^0.12.6",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "3.4.31", "vue": "3.4.31",
"vue-cropper": "1.1.1", "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> <script setup>
import { reactive, toRefs, onMounted, onUnmounted, watch, computed, ref } from "vue"; import { reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
import { useRoute } from "vue-router"; import UrdfViewer from "./components/UrdfViewer.vue";
import dayjs from "dayjs"; import JointControl from "./components/JointControl.vue";
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';
const data = reactive({ const data = reactive({
form: { 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, x: 11,
y: 11, y: 11,
z: 11, z: 11,
@ -30,207 +12,153 @@ const data = reactive({
ry: 11, ry: 11,
rz: 11, rz: 11,
arm: 1, 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: [ armList: [
{ label: '目标1', value: 1 }, { label: "目标1", value: 1 },
{ label: '目标2', value: 2 }, { label: "目标2", value: 2 },
{ label: '目标3', value: 3 }, { label: "目标3", value: 3 },
], ],
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
}); });
const terminalId = ref('25449ff3fcc7b27da5f69462c5efcec2');
const deviceId = ref('spk1'); const { form, armList } = toRefs(data);
const { form, time, zeroPos, initPos, armList } = toRefs(data);
const route = useRoute(); 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 coordinateSystem = ref(null); //
const buttonPositions = ref([]); // const buttonPositions = ref([]); //
// 1025x817px const buttonDirections = [
const initialPositions = [ { target: "z_plus", direction: "top", label: "Z+" }, // left
{ target: "z_plus", x: 500, y: 80, label: "Z+" }, { target: "rz_plus", direction: "top-right", label: "RZ+" },
{ target: "rz_plus", x: 675, y: 124, label: "RZ+" }, { target: "x_plus", direction: "right", label: "X+" }, // bottom
{ target: "x_plus", x: 796, y: 245, label: "X+" }, { target: "ry_plus", direction: "bottom-right", label: "RY+" },
{ target: "ry_plus", x: 840, y: 410, label: "RY+" }, { target: "y_minus", direction: "bottom", label: "Y-" },
{ target: "y_minus", x: 796, y: 570, label: "Y-" }, { target: "ry_minus", direction: "bottom-left", label: "RY-" },
{ target: "ry_minus", x: 675, y: 696, label: "RY-" }, { target: "z_minus", direction: "bottom", label: "Z-" },
{ target: "z_minus", x: 500, y: 745, label: "Z-" }, { target: "rx_minus", direction: "bottom-left", label: "RX-" },
{ target: "rx_minus", x: 345, y: 696, label: "RX-" }, { target: "x_minus", direction: "left", label: "X-" },
{ target: "x_minus", x: 206, y: 570, label: "X-" }, { target: "rx_plus", direction: "top-left", label: "RX+" },
{ target: "rx_plus", x: 180, y: 410, label: "RX+" }, { target: "y_plus", direction: "left", label: "Y+" }, // right
{ target: "y_plus", x: 204, y: 245, label: "Y+" }, { target: "rz_minus", direction: "top-left", label: "RZ-" },
{ target: "rz_minus", x: 345, y: 124, label: "RZ-" },
]; ];
// //
const ORIGINAL_IMAGE_WIDTH = 1025; const ORIGINAL_IMAGE_WIDTH = 1025;
const ORIGINAL_IMAGE_HEIGHT = 817; 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 = () => { const updateButtonPositions = () => {
if (coordinateSystem.value) { if (coordinateSystem.value) {
const { width, height } = coordinateSystem.value.getBoundingClientRect(); const containerRect = coordinateSystem.value.getBoundingClientRect();
// background-size: contain const containerWidth = containerRect.width;
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT; // 1025 / 817 1.255 const containerHeight = containerRect.height;
const containerAspectRatio = width / 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) { if (containerAspectRatio > imageAspectRatio) {
// imageHeight = containerHeight;
imageHeight = height; imageWidth = imageHeight * imageAspectRatio;
imageWidth = height * imageAspectRatio; imageLeft = (containerWidth - imageWidth) / 2;
offsetX = (width - imageWidth) / 2; // imageTop = 0;
offsetY = 0; //
} else { } else {
// imageWidth = containerWidth;
imageWidth = width; imageHeight = imageWidth / imageAspectRatio;
imageHeight = width / imageAspectRatio; imageLeft = 0;
offsetX = 0; // imageTop = (containerHeight - imageHeight) / 2;
offsetY = (height - imageHeight) / 2; //
} }
// //
const scaleX = imageWidth / ORIGINAL_IMAGE_WIDTH; const centerX = imageLeft + imageWidth / 2;
const scaleY = imageHeight / ORIGINAL_IMAGE_HEIGHT; const centerY = imageTop + imageHeight / 2;
//
const radiusScale = 0.85; // 0-1
const radius = Math.min(imageWidth, imageHeight) * radiusScale / 2;
// top0
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
};
// 12360/12=30Math.PI/6
const totalButtons = buttonDirections.length;
const angleStep = 2 * Math.PI / totalButtons;
// //
buttonPositions.value = initialPositions.map((pos) => ({ buttonPositions.value = buttonDirections.map((btn, index) => {
...pos, // top
left: pos.x * scaleX + offsetX, // x const angle = index * angleStep;
top: pos.y * scaleY + offsetY, // y //
})); const x = centerX + radius * Math.sin(angle);
const y = centerY - radius * Math.cos(angle);
return {
...btn,
left: x,
top: y,
};
});
} }
}; };
onMounted(() => { onMounted(() => {
getDeviceInfo();
//
updateButtonPositions(); 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(() => { 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> </script>
<template> <template>
<div class="mechanical-arm"> <div class="mechanical-arm">
<div class="top card"> <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> <div class="control-label">目标</div>
<el-select v-model="form.arm" placeholder="选择目标" style="width: 100px; margin-right: 20px;"> <el-select
<el-option v-for="item in armList" :key="item.value" :label="item.label" :value="item.value" /> 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-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> <el-button @click="handleResets('init')">初始位置</el-button>
</div> </div>
<div class="pose-info flex align-center"> <div class="pose-info flex align-center">
@ -242,9 +170,35 @@ const handleResets = (type) => {
<div class="pose-item">RZ: {{ form.rz.toFixed(6) }} deg</div> <div class="pose-item">RZ: {{ form.rz.toFixed(6) }} deg</div>
</div> </div>
</div> </div>
<div class="bottom flex"> <div class="bottom">
<div class="left card" style="flex: 1; position: relative;"> <div class="left">
<div class="title">姿态与位置控制</div> <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 class="bottom">
<div class="left card" style="position: relative">
<div class="coordinate-system" ref="coordinateSystem"> <div class="coordinate-system" ref="coordinateSystem">
<div class="control-buttons"> <div class="control-buttons">
<button <button
@ -259,50 +213,7 @@ const handleResets = (type) => {
</div> </div>
</div> </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>
<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>
</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> </div>
</div> </div>
@ -341,20 +252,59 @@ const handleResets = (type) => {
} }
.bottom { .bottom {
flex: 1; height: calc(100% - 120px);
gap: 20px; display: flex;
gap: 12px;
.title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
user-select: none;
}
.left { .left {
flex: 2;
height: 100%;
}
.right {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
.top {
flex: 1;
border: 1px solid #e4e7ed;
padding: 0 8px;
overflow: hidden;
.container-title {
font-size: 16px;
text-align: center;
}
.secondary-title {
display: flex;
justify-content: space-around;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.jointControl-container {
display: flex;
gap: 10px;
height: calc(100% - 75px);
overflow: auto;
}
}
.bottom {
flex: 1;
.card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
.coordinate-system { .coordinate-system {
flex: 1; flex: 1;
@ -362,8 +312,8 @@ const handleResets = (type) => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
background-color: #FFF; background-color: #fff;
background-image: url('@/assets/images/arm.png'); background-image: url("@/assets/images/arm.png");
background-size: contain; background-size: contain;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -393,43 +343,6 @@ const handleResets = (type) => {
} }
} }
} }
.right {
.input-list {
display: flex;
flex-direction: column;
gap: 10px;
.input-item {
display: flex;
align-items: center;
justify-content: space-between;
span {
width: 80px;
user-select: none;
}
.el-input-number {
width: calc(100% - 90px);
}
}
}
}
}
.footer {
height: 50px;
.time {
font-size: 14px;
}
.speed {
gap: 10px;
span {
user-select: none;
} }
} }
} }