feat: 模型控制
This commit is contained in:
parent
30c7f3c3e0
commit
85bf6feccc
2
.env
2
.env
@ -1,3 +1,3 @@
|
|||||||
VITE_DEVICE_ID = 'agv_src2200'
|
VITE_DEVICE_ID = 'agv_src2200'
|
||||||
|
|
||||||
VITE_SERVICE_PORT = 3000
|
VITE_SERVICE_PORT = 5175
|
||||||
@ -30,9 +30,9 @@ COPY server/ ./server/
|
|||||||
# 从构建阶段拷贝前端产物
|
# 从构建阶段拷贝前端产物
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 5175
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV VITE_SERVICE_PORT=3000
|
ENV VITE_SERVICE_PORT=5175
|
||||||
|
|
||||||
CMD ["node", "server/index.js"]
|
CMD ["node", "server/index.js"]
|
||||||
@ -5,10 +5,10 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: inspection-host-computer
|
container_name: inspection-host-computer
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000" # 映射端口
|
- "5175:5175" # 映射端口
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- VITE_SERVICE_PORT=3000 # 传给 Express 的环境变量
|
- VITE_SERVICE_PORT=5175 # 传给 Express 的环境变量
|
||||||
restart: unless-stopped # 容器意外停止后自动重启
|
restart: unless-stopped # 容器意外停止后自动重启
|
||||||
# 如果需要挂载日志或配置,可以加 volumes
|
# 如果需要挂载日志或配置,可以加 volumes
|
||||||
# volumes:
|
# volumes:
|
||||||
|
|||||||
@ -23,8 +23,11 @@
|
|||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
|
"three": "^0.181.2",
|
||||||
|
"urdf-loader": "^0.12.6",
|
||||||
"vue": "^3.5.38",
|
"vue": "^3.5.38",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.1.0",
|
||||||
|
"xacro-parser": "^0.3.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
|
|||||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@ -41,12 +41,21 @@ importers:
|
|||||||
pinia-plugin-persistedstate:
|
pinia-plugin-persistedstate:
|
||||||
specifier: ^4.7.1
|
specifier: ^4.7.1
|
||||||
version: 4.7.1(pinia@3.0.4(vue@3.5.38))
|
version: 4.7.1(pinia@3.0.4(vue@3.5.38))
|
||||||
|
three:
|
||||||
|
specifier: ^0.181.2
|
||||||
|
version: 0.181.2
|
||||||
|
urdf-loader:
|
||||||
|
specifier: ^0.12.6
|
||||||
|
version: 0.12.7(three@0.181.2)
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.38
|
specifier: ^3.5.38
|
||||||
version: 3.5.38
|
version: 3.5.38
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(vue@3.5.38))(vite@8.1.0(@types/node@26.0.0)(sass@1.101.0)(yaml@2.9.0))(vue@3.5.38)
|
version: 5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(vue@3.5.38))(vite@8.1.0(@types/node@26.0.0)(sass@1.101.0)(yaml@2.9.0))(vue@3.5.38)
|
||||||
|
xacro-parser:
|
||||||
|
specifier: ^0.3.11
|
||||||
|
version: 0.3.11
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.7
|
specifier: ^6.0.7
|
||||||
@ -963,6 +972,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
|
resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
expr-eval-fork@3.0.3:
|
||||||
|
resolution: {integrity: sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg==}
|
||||||
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@ -2045,6 +2058,9 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
three@0.181.2:
|
||||||
|
resolution: {integrity: sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@ -2168,6 +2184,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
|
resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
urdf-loader@0.12.7:
|
||||||
|
resolution: {integrity: sha512-9RQDhWt4x9K6R3uHjv68NLwX+JDm3A2XjhksrMgq6xl9NJcwqV21+htHU0cbG619hnqH1d4mUMFuqHo22R8DMA==}
|
||||||
|
peerDependencies:
|
||||||
|
three: '>=0.152.0'
|
||||||
|
|
||||||
urix@0.1.0:
|
urix@0.1.0:
|
||||||
resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
|
resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
|
||||||
deprecated: Please see https://github.com/lydell/urix#deprecated
|
deprecated: Please see https://github.com/lydell/urix#deprecated
|
||||||
@ -2300,6 +2321,9 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
xacro-parser@0.3.11:
|
||||||
|
resolution: {integrity: sha512-zyRzHmf6/BySLZQP77zp6iEtdryq1g7tWtfwj/28GOPyf9ijtPvouivOAeaA/yAu0/Bwh20DtA4x+BrYiV6qKQ==}
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -3282,6 +3306,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
expr-eval-fork@3.0.3: {}
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 2.0.0
|
accepts: 2.0.0
|
||||||
@ -4487,6 +4513,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sax: 1.6.0
|
sax: 1.6.0
|
||||||
|
|
||||||
|
three@0.181.2: {}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
@ -4660,6 +4688,10 @@ snapshots:
|
|||||||
has-value: 0.3.1
|
has-value: 0.3.1
|
||||||
isobject: 3.0.1
|
isobject: 3.0.1
|
||||||
|
|
||||||
|
urdf-loader@0.12.7(three@0.181.2):
|
||||||
|
dependencies:
|
||||||
|
three: 0.181.2
|
||||||
|
|
||||||
urix@0.1.0: {}
|
urix@0.1.0: {}
|
||||||
|
|
||||||
use@3.1.1: {}
|
use@3.1.1: {}
|
||||||
@ -4799,6 +4831,12 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
xacro-parser@0.3.11:
|
||||||
|
dependencies:
|
||||||
|
expr-eval-fork: 3.0.3
|
||||||
|
yaml: 2.9.0
|
||||||
|
yargs: 17.7.3
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yaml@2.9.0: {}
|
yaml@2.9.0: {}
|
||||||
|
|||||||
465
public/inspection/elfin10/elfin10.urdf.xacro
Normal file
465
public/inspection/elfin10/elfin10.urdf.xacro
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
<robot name="elfin10" xmlns:xacro="http://wiki.ros.org/xacro">
|
||||||
|
|
||||||
|
<xacro:arg name="use_real_hardware" default="true" />
|
||||||
|
<xacro:arg name="use_fake_hardware" default="false" />
|
||||||
|
|
||||||
|
<!-- Load TRANSMISSION file: -->
|
||||||
|
<xacro:include filename="file://$(find elfin_description)/elfin10_transmission.xacro" />
|
||||||
|
<!-- Load GAZEBO/ROS2_CONTROL file: -->
|
||||||
|
<xacro:include filename="file://$(find elfin_description)/elfin10_gazebo.xacro" />
|
||||||
|
|
||||||
|
<xacro:property name="PI" value="3.1415926535897931"/>
|
||||||
|
|
||||||
|
<xacro:property name="density" value="3.09"/>
|
||||||
|
<!-- <xacro:macro name="elfin10"> -->
|
||||||
|
|
||||||
|
<!-- TRANSMISSION: -->
|
||||||
|
<xacro:elfin10_transmission />
|
||||||
|
<!-- GAZEBO - ROS2_CONTROL: -->
|
||||||
|
<xacro:elfin10_gazebo
|
||||||
|
use_real_hardware="$(arg use_real_hardware)"
|
||||||
|
use_fake_hardware="$(arg use_fake_hardware)"/>
|
||||||
|
|
||||||
|
<link name="world"/>
|
||||||
|
|
||||||
|
<joint name="world_base" type="fixed">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 -1.5708" />
|
||||||
|
<parent
|
||||||
|
link="world" />
|
||||||
|
<child
|
||||||
|
link="elfin_base_link" />
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name="elfin_base_link"/>
|
||||||
|
|
||||||
|
<joint name="elfin_base_joint" type="fixed">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 ${PI/2}" />
|
||||||
|
<parent
|
||||||
|
link="elfin_base_link" />
|
||||||
|
<child
|
||||||
|
link="elfin_base" />
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link
|
||||||
|
name="elfin_base">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="-0.00226511349377665 4.35718066723588E-06 0.0325128562537015"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="1.66727740473724" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.004269288474386"
|
||||||
|
ixy="6.02518130623876E-08"
|
||||||
|
ixz="-6.92087963593712E-05"
|
||||||
|
iyy="0.00385585336301628"
|
||||||
|
iyz="-5.51423740998761E-08"
|
||||||
|
izz="0.0068204473327682" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_base.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_base.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<link
|
||||||
|
name="elfin_link1">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="7.61162863471406E-07 -0.00300107309756385 -0.0432743915921636"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="6.3589066662676" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.0391771076676677"
|
||||||
|
ixy="5.46819690041203E-08"
|
||||||
|
ixz="4.18972540516346E-07"
|
||||||
|
iyy="0.0381555138034643"
|
||||||
|
iyz="0.00661206821297882"
|
||||||
|
izz="0.0211340101406971" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link1.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link1.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint
|
||||||
|
name="elfin_joint1"
|
||||||
|
type="revolute">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0.26"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<parent
|
||||||
|
link="elfin_base" />
|
||||||
|
<child
|
||||||
|
link="elfin_link1" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 1" />
|
||||||
|
<limit
|
||||||
|
lower="-3.14"
|
||||||
|
upper="3.14"
|
||||||
|
effort="420"
|
||||||
|
velocity="1.57" />
|
||||||
|
</joint>
|
||||||
|
<link
|
||||||
|
name="elfin_link2">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="-1.29217317440473E-07 0.204406209458582 0.145205671136993"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="5.51072381215317" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.140886684844275"
|
||||||
|
ixy="-1.98882818733046E-05"
|
||||||
|
ixz="1.28793862225363E-06"
|
||||||
|
iyy="0.00904523320332792"
|
||||||
|
iyz="0.00556623571013066"
|
||||||
|
izz="0.143882970137024" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link2.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link2.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint
|
||||||
|
name="elfin_joint2"
|
||||||
|
type="revolute">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="1.5708 0 0" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link1" />
|
||||||
|
<child
|
||||||
|
link="elfin_link2" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 1" />
|
||||||
|
<limit
|
||||||
|
lower="-2.35"
|
||||||
|
upper="2.35"
|
||||||
|
effort="420"
|
||||||
|
velocity="1.57" />
|
||||||
|
</joint>
|
||||||
|
<link
|
||||||
|
name="elfin_link3">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="0.0357493010718184 -2.74303992683623E-06 -0.00441107040289549"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="3.25128088148268" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.00712273850158261"
|
||||||
|
ixy="4.14573354600121E-07"
|
||||||
|
ixz="-0.00240614658407673"
|
||||||
|
iyy="0.0131139013038093"
|
||||||
|
iyz="-1.53858261863385E-07"
|
||||||
|
izz="0.0123861174875263" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link3.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link3.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint
|
||||||
|
name="elfin_joint3"
|
||||||
|
type="revolute">
|
||||||
|
<origin
|
||||||
|
xyz="0 0.48 0"
|
||||||
|
rpy="-3.1416 0 1.5708" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link2" />
|
||||||
|
<child
|
||||||
|
link="elfin_link3" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 1" />
|
||||||
|
<limit
|
||||||
|
lower="-2.61"
|
||||||
|
upper="2.61"
|
||||||
|
effort="200"
|
||||||
|
velocity="1.57" />
|
||||||
|
</joint>
|
||||||
|
<link
|
||||||
|
name="elfin_link4">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="3.8972274666868E-06 -0.065883038527668 0.300149807420176"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="2.45076028364132" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.0366043963749035"
|
||||||
|
ixy="2.14481800816891E-07"
|
||||||
|
ixz="1.74370308297691E-06"
|
||||||
|
iyy="0.0331005868546169"
|
||||||
|
iyz="0.00876542795872678"
|
||||||
|
izz="0.00689303506560623" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link4.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link4.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint
|
||||||
|
name="elfin_joint4"
|
||||||
|
type="revolute">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="1.5708 0 1.5708" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link3" />
|
||||||
|
<child
|
||||||
|
link="elfin_link4" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 1" />
|
||||||
|
<limit
|
||||||
|
lower="-3.14"
|
||||||
|
upper="3.14"
|
||||||
|
effort="200"
|
||||||
|
velocity="1.57" />
|
||||||
|
</joint>
|
||||||
|
<link
|
||||||
|
name="elfin_link5">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="5.37084413168812E-06 -0.0262437975997385 -0.00353971828905618"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="1.39234539037872" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.0033625821599978"
|
||||||
|
ixy="2.21090591006111E-07"
|
||||||
|
ixz="-5.5969383984273E-07"
|
||||||
|
iyy="0.00182229285631246"
|
||||||
|
iyz="0.000639319386824375"
|
||||||
|
izz="0.00292731816323284" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link5.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link5.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint
|
||||||
|
name="elfin_joint5"
|
||||||
|
type="revolute">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0.52"
|
||||||
|
rpy="-1.5708 0 0" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link4" />
|
||||||
|
<child
|
||||||
|
link="elfin_link5" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 1" />
|
||||||
|
<limit
|
||||||
|
lower="-2.56"
|
||||||
|
upper="2.56"
|
||||||
|
effort="69"
|
||||||
|
velocity="1.57" />
|
||||||
|
</joint>
|
||||||
|
<link
|
||||||
|
name="elfin_link6">
|
||||||
|
<inertial>
|
||||||
|
<origin
|
||||||
|
xyz="-8.45231478862452E-06 -0.000388392932234965 0.144636177508013"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<mass
|
||||||
|
value="0.297803050744826" />
|
||||||
|
<inertia
|
||||||
|
ixx="0.000151919382929214"
|
||||||
|
ixy="-1.8598001023495E-09"
|
||||||
|
ixz="6.95653480101813E-08"
|
||||||
|
iyy="0.000155217204136347"
|
||||||
|
iyz="-6.63866631888914E-08"
|
||||||
|
izz="0.00027009779924127" />
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link6.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material
|
||||||
|
name="">
|
||||||
|
<color
|
||||||
|
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<geometry>
|
||||||
|
<mesh
|
||||||
|
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link6.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint
|
||||||
|
name="elfin_joint6"
|
||||||
|
type="revolute">
|
||||||
|
<origin
|
||||||
|
xyz="0 0 0"
|
||||||
|
rpy="1.5708 0 0" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link5" />
|
||||||
|
<child
|
||||||
|
link="elfin_link6" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 1" />
|
||||||
|
<limit
|
||||||
|
lower="-3.14"
|
||||||
|
upper="3.14"
|
||||||
|
effort="69"
|
||||||
|
velocity="1.57" />
|
||||||
|
</joint>
|
||||||
|
<link
|
||||||
|
name="elfin_end_link"/>
|
||||||
|
<joint
|
||||||
|
name="elfin_end_joint"
|
||||||
|
type="fixed">
|
||||||
|
<origin
|
||||||
|
xyz="0 0.0 0.1735"
|
||||||
|
rpy="0 0 0" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link6" />
|
||||||
|
<child
|
||||||
|
link="elfin_end_link" />
|
||||||
|
<axis
|
||||||
|
xyz="0 0 0" />
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name="elfin_dummy_gripper"/>
|
||||||
|
|
||||||
|
<joint name="elfin_dummy_joint" type="fixed">
|
||||||
|
<origin
|
||||||
|
xyz="0 0.109 0"
|
||||||
|
rpy="${-PI/2} ${-PI/2} 0" />
|
||||||
|
<parent
|
||||||
|
link="elfin_link6" />
|
||||||
|
<child
|
||||||
|
link="elfin_dummy_gripper" />
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
</robot>
|
||||||
134
public/inspection/elfin10/elfin10_gazebo.xacro
Normal file
134
public/inspection/elfin10/elfin10_gazebo.xacro
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
|
||||||
|
|
||||||
|
<xacro:macro name="elfin10_gazebo"
|
||||||
|
params="use_fake_hardware:=false
|
||||||
|
use_real_hardware:=true">
|
||||||
|
|
||||||
|
<gazebo>
|
||||||
|
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
|
||||||
|
<robot_sim_type>gazebo_ros2_control/GazeboSystem</robot_sim_type>
|
||||||
|
<parameters>$(find elfin10_ros2_gazebo)/config/elfin_arm_controller.yaml</parameters>
|
||||||
|
</plugin>
|
||||||
|
</gazebo>
|
||||||
|
|
||||||
|
<ros2_control name="GazeboSystem" type="system">
|
||||||
|
|
||||||
|
<hardware>
|
||||||
|
<xacro:if value="${use_real_hardware}">
|
||||||
|
<plugin>elfin_hardware_interface/ElfinHWInterface</plugin>
|
||||||
|
</xacro:if>
|
||||||
|
|
||||||
|
<xacro:unless value="${use_real_hardware}">
|
||||||
|
<xacro:if value="${use_fake_hardware}">
|
||||||
|
<plugin>fake_components/GenericSystem</plugin>
|
||||||
|
</xacro:if>
|
||||||
|
|
||||||
|
<xacro:unless value="${use_fake_hardware}">
|
||||||
|
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
|
||||||
|
</xacro:unless>
|
||||||
|
</xacro:unless>
|
||||||
|
</hardware>
|
||||||
|
|
||||||
|
|
||||||
|
<joint name="elfin_joint1">
|
||||||
|
<command_interface name="position">
|
||||||
|
<param name="min">-3.14</param>
|
||||||
|
<param name="max">3.14</param>
|
||||||
|
</command_interface>
|
||||||
|
<command_interface name="velocity">
|
||||||
|
<param name="min">-1.57</param>
|
||||||
|
<param name="max">1.57</param>
|
||||||
|
</command_interface>
|
||||||
|
<state_interface name="position"/>
|
||||||
|
<state_interface name="velocity"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="elfin_joint2">
|
||||||
|
<command_interface name="position">
|
||||||
|
<param name="min">-2.35</param>
|
||||||
|
<param name="max">2.35</param>
|
||||||
|
</command_interface>
|
||||||
|
<command_interface name="velocity">
|
||||||
|
<param name="min">-1.57</param>
|
||||||
|
<param name="max">1.57</param>
|
||||||
|
</command_interface>
|
||||||
|
<state_interface name="position"/>
|
||||||
|
<state_interface name="velocity"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="elfin_joint3">
|
||||||
|
<command_interface name="position">
|
||||||
|
<param name="min">-2.61</param>
|
||||||
|
<param name="max">2.61</param>
|
||||||
|
</command_interface>
|
||||||
|
<command_interface name="velocity">
|
||||||
|
<param name="min">-1.57</param>
|
||||||
|
<param name="max">1.57</param>
|
||||||
|
</command_interface>
|
||||||
|
<state_interface name="position"/>
|
||||||
|
<state_interface name="velocity"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="elfin_joint4">
|
||||||
|
<command_interface name="position">
|
||||||
|
<param name="min">-3.14</param>
|
||||||
|
<param name="max">3.14</param>
|
||||||
|
</command_interface>
|
||||||
|
<command_interface name="velocity">
|
||||||
|
<param name="min">-1.57</param>
|
||||||
|
<param name="max">1.57</param>
|
||||||
|
</command_interface>
|
||||||
|
<state_interface name="position"/>
|
||||||
|
<state_interface name="velocity"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="elfin_joint5">
|
||||||
|
<command_interface name="position">
|
||||||
|
<param name="min">-2.56</param>
|
||||||
|
<param name="max">2.56</param>
|
||||||
|
</command_interface>
|
||||||
|
<command_interface name="velocity">
|
||||||
|
<param name="min">-1.57</param>
|
||||||
|
<param name="max">1.57</param>
|
||||||
|
</command_interface>
|
||||||
|
<state_interface name="position"/>
|
||||||
|
<state_interface name="velocity"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="elfin_joint6">
|
||||||
|
<command_interface name="position">
|
||||||
|
<param name="min">-3.14</param>
|
||||||
|
<param name="max">3.14</param>
|
||||||
|
</command_interface>
|
||||||
|
<command_interface name="velocity">
|
||||||
|
<param name="min">-1.57</param>
|
||||||
|
<param name="max">1.57</param>
|
||||||
|
</command_interface>
|
||||||
|
<state_interface name="position"/>
|
||||||
|
<state_interface name="velocity"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
</ros2_control>
|
||||||
|
|
||||||
|
<gazebo reference="elfin_link2">
|
||||||
|
<visual name='visual'>
|
||||||
|
<material>
|
||||||
|
<script>
|
||||||
|
<uri>file://media/materials/scripts/gazebo.material</uri>
|
||||||
|
<name>Gazebo/Grey</name>
|
||||||
|
</script>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
</gazebo>
|
||||||
|
|
||||||
|
<gazebo reference="elfin_link4">
|
||||||
|
<visual name='visual'>
|
||||||
|
<material>
|
||||||
|
<script>
|
||||||
|
<uri>file://media/materials/scripts/gazebo.material</uri>
|
||||||
|
<name>Gazebo/Grey</name>
|
||||||
|
</script>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
</gazebo>
|
||||||
|
|
||||||
|
</xacro:macro>
|
||||||
|
|
||||||
|
</robot>
|
||||||
69
public/inspection/elfin10/elfin10_transmission.xacro
Normal file
69
public/inspection/elfin10/elfin10_transmission.xacro
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
<robot xmlns:xacro="http://wiki.ros.org/xacro">
|
||||||
|
|
||||||
|
<xacro:macro name="elfin10_transmission" >
|
||||||
|
|
||||||
|
<transmission name="elfin_joint1_trans">
|
||||||
|
<type>transmission_interface/SimpleTransmission</type>
|
||||||
|
<joint name="elfin_joint1">
|
||||||
|
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||||
|
</joint>
|
||||||
|
<actuator name="elfin_joint1_motor">
|
||||||
|
<mechanicalReduction>1</mechanicalReduction>
|
||||||
|
</actuator>
|
||||||
|
</transmission>
|
||||||
|
|
||||||
|
<transmission name="elfin_joint2_trans">
|
||||||
|
<type>transmission_interface/SimpleTransmission</type>
|
||||||
|
<joint name="elfin_joint2">
|
||||||
|
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||||
|
</joint>
|
||||||
|
<actuator name="elfin_joint2_motor">
|
||||||
|
<mechanicalReduction>1</mechanicalReduction>
|
||||||
|
</actuator>
|
||||||
|
</transmission>
|
||||||
|
|
||||||
|
<transmission name="elfin_joint3_trans">
|
||||||
|
<type>transmission_interface/SimpleTransmission</type>
|
||||||
|
<joint name="elfin_joint3">
|
||||||
|
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||||
|
</joint>
|
||||||
|
<actuator name="elfin_joint3_motor">
|
||||||
|
<mechanicalReduction>1</mechanicalReduction>
|
||||||
|
</actuator>
|
||||||
|
</transmission>
|
||||||
|
|
||||||
|
<transmission name="elfin_joint4_trans">
|
||||||
|
<type>transmission_interface/SimpleTransmission</type>
|
||||||
|
<joint name="elfin_joint4">
|
||||||
|
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||||
|
</joint>
|
||||||
|
<actuator name="elfin_joint4_motor">
|
||||||
|
<mechanicalReduction>1</mechanicalReduction>
|
||||||
|
</actuator>
|
||||||
|
</transmission>
|
||||||
|
|
||||||
|
<transmission name="elfin_joint5_trans">
|
||||||
|
<type>transmission_interface/SimpleTransmission</type>
|
||||||
|
<joint name="elfin_joint5">
|
||||||
|
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||||
|
</joint>
|
||||||
|
<actuator name="elfin_joint5_motor">
|
||||||
|
<mechanicalReduction>1</mechanicalReduction>
|
||||||
|
</actuator>
|
||||||
|
</transmission>
|
||||||
|
|
||||||
|
<transmission name="elfin_joint6_trans">
|
||||||
|
<type>transmission_interface/SimpleTransmission</type>
|
||||||
|
<joint name="elfin_joint6">
|
||||||
|
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||||
|
</joint>
|
||||||
|
<actuator name="elfin_joint6_motor">
|
||||||
|
<mechanicalReduction>1</mechanicalReduction>
|
||||||
|
</actuator>
|
||||||
|
</transmission>
|
||||||
|
|
||||||
|
</xacro:macro>
|
||||||
|
|
||||||
|
</robot>
|
||||||
BIN
public/inspection/elfin10/meshes/elfin10/elfin_base.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_base.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_end_link.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_end_link.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link1.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link1.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link2.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link2.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link3.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link3.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link4.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link4.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link5.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link5.STL
Normal file
Binary file not shown.
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link6.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link6.STL
Normal file
Binary file not shown.
461
src/adapters/URDFAdapter.js
Normal file
461
src/adapters/URDFAdapter.js
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
/**
|
||||||
|
* URDF Adapter
|
||||||
|
* Converts urdf-loaders result to unified model
|
||||||
|
*/
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { UnifiedRobotModel, Link, Joint, JointLimits, VisualGeometry, CollisionGeometry, InertialProperties } from '@/models/UnifiedRobotModel.js';
|
||||||
|
|
||||||
|
export class URDFAdapter {
|
||||||
|
/**
|
||||||
|
* Convert urdf-loaders robot object to unified model
|
||||||
|
* @param {THREE.Group} robot - Robot object returned by urdf-loaders
|
||||||
|
* @param {string} urdfXML - Original URDF XML content (optional, for extracting inertial data)
|
||||||
|
* @returns {UnifiedRobotModel}
|
||||||
|
*/
|
||||||
|
static convert(robot, urdfXML = null) {
|
||||||
|
const model = new UnifiedRobotModel();
|
||||||
|
model.name = robot.name || 'robot';
|
||||||
|
model.threeObject = robot;
|
||||||
|
|
||||||
|
// Mark model type as URDF
|
||||||
|
if (!robot.userData) robot.userData = {};
|
||||||
|
robot.userData.type = 'urdf';
|
||||||
|
|
||||||
|
if (!robot.links || !robot.joints) {
|
||||||
|
console.warn('URDF model missing links or joints information');
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If XML provided, parse inertial data
|
||||||
|
let inertialData = {};
|
||||||
|
if (urdfXML) {
|
||||||
|
inertialData = this.parseInertialFromXML(urdfXML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert links
|
||||||
|
Object.values(robot.links).forEach(urdfLink => {
|
||||||
|
const link = this.convertLink(urdfLink);
|
||||||
|
|
||||||
|
// If urdf-loader didn't parse inertial, get from XML
|
||||||
|
if (!link.inertial && inertialData[urdfLink.name]) {
|
||||||
|
link.inertial = inertialData[urdfLink.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addLink(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert joints
|
||||||
|
Object.values(robot.joints).forEach(urdfJoint => {
|
||||||
|
const joint = this.convertJoint(urdfJoint);
|
||||||
|
model.addJoint(joint);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If XML provided, supplement effort and velocity from XML (urdf-loaders may not have parsed)
|
||||||
|
if (urdfXML) {
|
||||||
|
this.supplementJointLimitsFromXML(model, urdfXML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find root link (link that is not a child of any joint)
|
||||||
|
const allChildren = new Set(
|
||||||
|
Array.from(model.joints.values()).map(j => j.child).filter(c => c)
|
||||||
|
);
|
||||||
|
const rootLinks = Array.from(model.links.keys()).filter(
|
||||||
|
name => !allChildren.has(name)
|
||||||
|
);
|
||||||
|
if (rootLinks.length > 0) {
|
||||||
|
model.rootLink = rootLinks[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhance materials for better lighting (MuJoCo style)
|
||||||
|
this.enhanceMaterials(robot);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance materials for better lighting (MuJoCo style)
|
||||||
|
* Applies consistent shininess and specular properties to all materials
|
||||||
|
* Directly modifies materials in place to ensure changes persist
|
||||||
|
*/
|
||||||
|
static enhanceMaterials(robotObject) {
|
||||||
|
robotObject.traverse((child) => {
|
||||||
|
if (child.isMesh && child.material) {
|
||||||
|
// Handle material arrays
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
child.material = child.material.map(mat => {
|
||||||
|
return this.enhanceSingleMaterial(mat);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
child.material = this.enhanceSingleMaterial(child.material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance a single material with better lighting properties
|
||||||
|
* Returns enhanced material (may be cloned or modified in place)
|
||||||
|
* Saves original properties for lighting toggle
|
||||||
|
*/
|
||||||
|
static enhanceSingleMaterial(material) {
|
||||||
|
if (material.isMeshPhongMaterial || material.isMeshStandardMaterial) {
|
||||||
|
// Save original properties if not already saved (for lighting toggle)
|
||||||
|
if (material.userData.originalShininess === undefined) {
|
||||||
|
material.userData.originalShininess = material.shininess !== undefined ? material.shininess : 30;
|
||||||
|
// IMPORTANT: Save original color to preserve URDF material colors
|
||||||
|
if (!material.userData.originalColor && material.color) {
|
||||||
|
material.userData.originalColor = material.color.clone();
|
||||||
|
}
|
||||||
|
// Save original specular - if material had no specular, save null
|
||||||
|
if (!material.specular) {
|
||||||
|
material.userData.originalSpecular = null;
|
||||||
|
} else if (material.specular.isColor) {
|
||||||
|
const spec = material.specular;
|
||||||
|
if (spec.r < 0.1 && spec.g < 0.1 && spec.b < 0.1) {
|
||||||
|
material.userData.originalSpecular = null; // Likely default
|
||||||
|
} else {
|
||||||
|
material.userData.originalSpecular = spec.clone();
|
||||||
|
}
|
||||||
|
} else if (typeof material.specular === 'number') {
|
||||||
|
if (material.specular === 0x111111 || material.specular < 0x111111) {
|
||||||
|
material.userData.originalSpecular = null;
|
||||||
|
} else {
|
||||||
|
material.userData.originalSpecular = new THREE.Color(material.specular);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
material.userData.originalSpecular = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: Preserve original color from URDF material definitions
|
||||||
|
// Do not modify material.color - urdf-loaders already sets the correct color
|
||||||
|
|
||||||
|
// Apply enhanced lighting (default enabled)
|
||||||
|
// Increase shininess for better highlights
|
||||||
|
if (material.shininess === undefined || material.shininess < 50) {
|
||||||
|
material.shininess = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhance specular reflection - ensure it's a Color object with proper values
|
||||||
|
if (!material.specular) {
|
||||||
|
material.specular = new THREE.Color(0.3, 0.3, 0.3);
|
||||||
|
} else if (material.specular.isColor) {
|
||||||
|
// If it's already a Color object, update values
|
||||||
|
if (material.specular.r < 0.2 || material.specular.g < 0.2 || material.specular.b < 0.2) {
|
||||||
|
material.specular.setRGB(0.3, 0.3, 0.3);
|
||||||
|
}
|
||||||
|
} else if (typeof material.specular === 'number') {
|
||||||
|
// Convert number to Color object
|
||||||
|
if (material.specular < 0x333333) {
|
||||||
|
material.specular = new THREE.Color(0.3, 0.3, 0.3);
|
||||||
|
} else {
|
||||||
|
// Convert hex to Color
|
||||||
|
material.specular = new THREE.Color(material.specular);
|
||||||
|
if (material.specular.r < 0.2) {
|
||||||
|
material.specular.setRGB(0.3, 0.3, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark material as needing update
|
||||||
|
material.needsUpdate = true;
|
||||||
|
return material;
|
||||||
|
} else if (material.type === 'MeshBasicMaterial') {
|
||||||
|
// Convert MeshBasicMaterial to MeshPhongMaterial
|
||||||
|
const oldMaterial = material;
|
||||||
|
const envMap = typeof window !== 'undefined' && window.app?.sceneManager?.environmentManager?.getEnvironmentMap();
|
||||||
|
const newMaterial = new THREE.MeshPhongMaterial({
|
||||||
|
color: oldMaterial.color,
|
||||||
|
map: oldMaterial.map,
|
||||||
|
transparent: oldMaterial.transparent,
|
||||||
|
opacity: oldMaterial.opacity,
|
||||||
|
side: oldMaterial.side,
|
||||||
|
shininess: 50,
|
||||||
|
specular: new THREE.Color(0.3, 0.3, 0.3),
|
||||||
|
envMap: envMap || null,
|
||||||
|
reflectivity: envMap ? 0.3 : 0
|
||||||
|
});
|
||||||
|
// Save original properties for lighting toggle
|
||||||
|
newMaterial.userData.originalShininess = 30;
|
||||||
|
newMaterial.userData.originalSpecular = null; // MeshBasicMaterial had no specular
|
||||||
|
return newMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertLink(urdfLink) {
|
||||||
|
const link = new Link(urdfLink.name);
|
||||||
|
link.threeObject = urdfLink;
|
||||||
|
|
||||||
|
// Convert inertial properties
|
||||||
|
if (urdfLink.inertial) {
|
||||||
|
link.inertial = this.convertInertial(urdfLink.inertial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: urdf-loaders has already converted visual and collision to Three.js objects
|
||||||
|
// We mainly extract metadata, actual meshes are in threeObject
|
||||||
|
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertJoint(urdfJoint) {
|
||||||
|
// URDF joint object has jointType property (not type)
|
||||||
|
const jointType = urdfJoint.jointType || urdfJoint.type || 'fixed';
|
||||||
|
const joint = new Joint(urdfJoint.name, jointType);
|
||||||
|
|
||||||
|
// Extract parent name (urdf-loader returns Three.js object reference)
|
||||||
|
joint.parent = urdfJoint.parent?.name || null;
|
||||||
|
|
||||||
|
// child may not be in urdfJoint.child, but in Three.js children array
|
||||||
|
// In Three.js scene graph, Joint is parent node, Child Link is Joint's child node
|
||||||
|
if (urdfJoint.child && urdfJoint.child.name) {
|
||||||
|
joint.child = urdfJoint.child.name;
|
||||||
|
} else if (urdfJoint.children && urdfJoint.children.length > 0) {
|
||||||
|
// Find Link object from children array
|
||||||
|
const childLink = urdfJoint.children.find(child =>
|
||||||
|
child.isURDFLink || child.type === 'URDFLink'
|
||||||
|
);
|
||||||
|
if (childLink) {
|
||||||
|
joint.child = childLink.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joint.threeObject = urdfJoint;
|
||||||
|
|
||||||
|
// Convert origin
|
||||||
|
if (urdfJoint.origin) {
|
||||||
|
joint.origin = {
|
||||||
|
xyz: urdfJoint.origin.xyz || [0, 0, 0],
|
||||||
|
rpy: urdfJoint.origin.rpy || [0, 0, 0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert axis
|
||||||
|
if (urdfJoint.axis) {
|
||||||
|
joint.axis = { xyz: urdfJoint.axis.xyz || [0, 0, 1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert limits
|
||||||
|
if (urdfJoint.limit) {
|
||||||
|
joint.limits = this.convertLimits(urdfJoint.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current value
|
||||||
|
if (urdfJoint.angle !== undefined) {
|
||||||
|
joint.currentValue = urdfJoint.angle;
|
||||||
|
} else if (urdfJoint.jointValue !== undefined) {
|
||||||
|
joint.currentValue = urdfJoint.jointValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return joint;
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertLimits(limit) {
|
||||||
|
const limits = new JointLimits();
|
||||||
|
if (limit.lower !== undefined) limits.lower = limit.lower;
|
||||||
|
if (limit.upper !== undefined) limits.upper = limit.upper;
|
||||||
|
if (limit.effort !== undefined) limits.effort = limit.effort;
|
||||||
|
if (limit.velocity !== undefined) limits.velocity = limit.velocity;
|
||||||
|
return limits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supplement joint effort and velocity information from URDF XML
|
||||||
|
* (because urdf-loaders may not have parsed these attributes)
|
||||||
|
*/
|
||||||
|
static supplementJointLimitsFromXML(model, urdfXML) {
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(urdfXML, 'text/xml');
|
||||||
|
|
||||||
|
const jointElements = doc.querySelectorAll('joint');
|
||||||
|
jointElements.forEach(jointEl => {
|
||||||
|
const jointName = jointEl.getAttribute('name');
|
||||||
|
const joint = model.joints.get(jointName);
|
||||||
|
|
||||||
|
if (joint) {
|
||||||
|
const limitEl = jointEl.querySelector('limit');
|
||||||
|
if (limitEl) {
|
||||||
|
if (!joint.limits) {
|
||||||
|
joint.limits = new JointLimits();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read effort and velocity
|
||||||
|
const effort = limitEl.getAttribute('effort');
|
||||||
|
const velocity = limitEl.getAttribute('velocity');
|
||||||
|
|
||||||
|
if (effort !== null) {
|
||||||
|
joint.limits.effort = parseFloat(effort);
|
||||||
|
}
|
||||||
|
if (velocity !== null) {
|
||||||
|
joint.limits.velocity = parseFloat(velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also ensure lower and upper are correctly set
|
||||||
|
const lower = limitEl.getAttribute('lower');
|
||||||
|
const upper = limitEl.getAttribute('upper');
|
||||||
|
if (lower !== null) {
|
||||||
|
joint.limits.lower = parseFloat(lower);
|
||||||
|
}
|
||||||
|
if (upper !== null) {
|
||||||
|
joint.limits.upper = parseFloat(upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to supplement joint limit information from XML:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertInertial(inertial) {
|
||||||
|
const props = new InertialProperties();
|
||||||
|
if (inertial.mass !== undefined) props.mass = inertial.mass;
|
||||||
|
if (inertial.origin) {
|
||||||
|
props.origin = {
|
||||||
|
xyz: inertial.origin.xyz || [0, 0, 0],
|
||||||
|
rpy: inertial.origin.rpy || [0, 0, 0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (inertial.ixx !== undefined) props.ixx = inertial.ixx;
|
||||||
|
if (inertial.iyy !== undefined) props.iyy = inertial.iyy;
|
||||||
|
if (inertial.izz !== undefined) props.izz = inertial.izz;
|
||||||
|
if (inertial.ixy !== undefined) props.ixy = inertial.ixy;
|
||||||
|
if (inertial.ixz !== undefined) props.ixz = inertial.ixz;
|
||||||
|
if (inertial.iyz !== undefined) props.iyz = inertial.iyz;
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse inertial data from URDF XML
|
||||||
|
* @param {string} xmlContent - URDF XML content
|
||||||
|
* @returns {Object} Mapping from link names to inertial data
|
||||||
|
*/
|
||||||
|
static parseInertialFromXML(xmlContent) {
|
||||||
|
const inertialData = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
|
||||||
|
|
||||||
|
// Get all link elements
|
||||||
|
const links = xmlDoc.getElementsByTagName('link');
|
||||||
|
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
const linkElement = links[i];
|
||||||
|
const linkName = linkElement.getAttribute('name');
|
||||||
|
|
||||||
|
// Find inertial element
|
||||||
|
const inertialElement = linkElement.getElementsByTagName('inertial')[0];
|
||||||
|
if (!inertialElement) continue;
|
||||||
|
|
||||||
|
const inertial = new InertialProperties();
|
||||||
|
|
||||||
|
// Parse mass
|
||||||
|
const massElement = inertialElement.getElementsByTagName('mass')[0];
|
||||||
|
if (massElement) {
|
||||||
|
inertial.mass = parseFloat(massElement.getAttribute('value'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse origin
|
||||||
|
const originElement = inertialElement.getElementsByTagName('origin')[0];
|
||||||
|
if (originElement) {
|
||||||
|
const xyz = originElement.getAttribute('xyz');
|
||||||
|
const rpy = originElement.getAttribute('rpy');
|
||||||
|
inertial.origin = {
|
||||||
|
xyz: xyz ? xyz.split(' ').map(parseFloat) : [0, 0, 0],
|
||||||
|
rpy: rpy ? rpy.split(' ').map(parseFloat) : [0, 0, 0]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
inertial.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse inertia
|
||||||
|
const inertiaElement = inertialElement.getElementsByTagName('inertia')[0];
|
||||||
|
if (inertiaElement) {
|
||||||
|
inertial.ixx = parseFloat(inertiaElement.getAttribute('ixx')) || 0;
|
||||||
|
inertial.iyy = parseFloat(inertiaElement.getAttribute('iyy')) || 0;
|
||||||
|
inertial.izz = parseFloat(inertiaElement.getAttribute('izz')) || 0;
|
||||||
|
inertial.ixy = parseFloat(inertiaElement.getAttribute('ixy')) || 0;
|
||||||
|
inertial.ixz = parseFloat(inertiaElement.getAttribute('ixz')) || 0;
|
||||||
|
inertial.iyz = parseFloat(inertiaElement.getAttribute('iyz')) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inertialData[linkName] = inertial;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse URDF XML inertial data:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inertialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set joint angle (using urdf-loaders' setJointValue method)
|
||||||
|
* Reference URDFClasses.js setJointValue implementation
|
||||||
|
*/
|
||||||
|
static setJointAngle(joint, angle, ignoreLimits = false) {
|
||||||
|
joint.currentValue = angle;
|
||||||
|
|
||||||
|
// URDF format: use urdf-loader's setJointValue method
|
||||||
|
if (joint.threeObject) {
|
||||||
|
// If ignoring limits, temporarily modify URDF joint object's limit values
|
||||||
|
let originalLimits = null;
|
||||||
|
if (ignoreLimits && joint.threeObject.limit) {
|
||||||
|
originalLimits = {
|
||||||
|
lower: joint.threeObject.limit.lower,
|
||||||
|
upper: joint.threeObject.limit.upper
|
||||||
|
};
|
||||||
|
joint.threeObject.limit.lower = -Math.PI * 2;
|
||||||
|
joint.threeObject.limit.upper = Math.PI * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer setJointValue method (urdf-loader's standard method)
|
||||||
|
if (typeof joint.threeObject.setJointValue === 'function') {
|
||||||
|
joint.threeObject.setJointValue(angle);
|
||||||
|
|
||||||
|
// Restore original limits
|
||||||
|
if (originalLimits && joint.threeObject.limit) {
|
||||||
|
joint.threeObject.limit.lower = originalLimits.lower;
|
||||||
|
joint.threeObject.limit.upper = originalLimits.upper;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (typeof joint.threeObject.setAngle === 'function') {
|
||||||
|
joint.threeObject.setAngle(angle);
|
||||||
|
|
||||||
|
// Restore original limits
|
||||||
|
if (originalLimits && joint.threeObject.limit) {
|
||||||
|
joint.threeObject.limit.lower = originalLimits.lower;
|
||||||
|
joint.threeObject.limit.upper = originalLimits.upper;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original limits (if none of the above methods executed)
|
||||||
|
if (originalLimits && joint.threeObject.limit) {
|
||||||
|
joint.threeObject.limit.lower = originalLimits.lower;
|
||||||
|
joint.threeObject.limit.upper = originalLimits.upper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none available, manually set rotation
|
||||||
|
// Note: For URDF, we usually shouldn't reach here, as urdf-loader should provide setJointValue
|
||||||
|
console.warn(`Joint ${joint.name} has no setJointValue or setAngle method, attempting manual setup`);
|
||||||
|
if (joint.type === 'revolute' || joint.type === 'continuous') {
|
||||||
|
const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(0, 0, 1);
|
||||||
|
// Note: joint.threeObject may not be a Three.js object, cannot directly set rotation
|
||||||
|
if (joint.threeObject.rotation) {
|
||||||
|
joint.threeObject.rotation.setFromAxisAngle(axis, angle);
|
||||||
|
}
|
||||||
|
} else if (joint.type === 'prismatic') {
|
||||||
|
const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(1, 0, 0);
|
||||||
|
if (joint.threeObject.position) {
|
||||||
|
joint.threeObject.position.copy(axis.multiplyScalar(angle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Joint ${joint.name} has no threeObject`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
624
src/adapters/XacroAdapter.js
Normal file
624
src/adapters/XacroAdapter.js
Normal file
@ -0,0 +1,624 @@
|
|||||||
|
/**
|
||||||
|
* Xacro Adapter
|
||||||
|
*
|
||||||
|
* Parses ROS Xacro files and converts them to URDF format using xacro-parser.
|
||||||
|
*
|
||||||
|
* Supported Features:
|
||||||
|
* - Property definitions and substitutions (xacro:property)
|
||||||
|
* - Macro definitions with parameters (xacro:macro)
|
||||||
|
* - Conditional blocks (xacro:if, xacro:unless)
|
||||||
|
* - File inclusions (xacro:include) with package:// and relative paths
|
||||||
|
* - Python-style boolean constants (True/False)
|
||||||
|
* - Arithmetic expressions and property evaluation
|
||||||
|
*
|
||||||
|
* Compatibility:
|
||||||
|
* - ROS Jade and later (inOrder=true, requirePrefix=true, localProperties=true)
|
||||||
|
* - Automatically injects True/False constants for compatibility with ROS xacro files
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Upload all related xacro files (main file and included files)
|
||||||
|
* - The adapter automatically resolves file inclusions from the uploaded file map
|
||||||
|
* - Mesh files are resolved using package:// paths or relative paths
|
||||||
|
*/
|
||||||
|
import { XacroParser } from 'xacro-parser';
|
||||||
|
import { URDFAdapter } from './URDFAdapter.js';
|
||||||
|
|
||||||
|
export class XacroAdapter {
|
||||||
|
/**
|
||||||
|
* Parse xacro content and convert to unified model
|
||||||
|
* @param {string} xacroContent - Xacro file content
|
||||||
|
* @param {string} fileName - Xacro file name (for working path)
|
||||||
|
* @param {Map} fileMap - File map (path -> File object)
|
||||||
|
* @param {File} file - Original file object (optional)
|
||||||
|
* @returns {Promise<UnifiedRobotModel>}
|
||||||
|
*/
|
||||||
|
static async parse(xacroContent, fileName, fileMap = null, file = null) {
|
||||||
|
try {
|
||||||
|
// Create xacro parser
|
||||||
|
const parser = new XacroParser();
|
||||||
|
|
||||||
|
// Configure parser for ROS Jade and later (default settings)
|
||||||
|
parser.inOrder = true;
|
||||||
|
parser.requirePrefix = true;
|
||||||
|
parser.localProperties = true;
|
||||||
|
|
||||||
|
// Set working path (directory where xacro file is located)
|
||||||
|
const workingPath = fileName.includes('/')
|
||||||
|
? fileName.substring(0, fileName.lastIndexOf('/') + 1)
|
||||||
|
: '';
|
||||||
|
parser.workingPath = workingPath;
|
||||||
|
|
||||||
|
// Inject Python-style boolean constants as xacro properties
|
||||||
|
// Some xacro files use True/False (capitalized) in conditions
|
||||||
|
xacroContent = this.injectBooleanConstants(xacroContent);
|
||||||
|
|
||||||
|
// Extract and set xacro arguments with their default values
|
||||||
|
// Also add common ROS arguments that might be used without definition
|
||||||
|
const xacroArgs = this.extractXacroArguments(xacroContent);
|
||||||
|
|
||||||
|
// Add common ROS arguments with sensible defaults if not already defined
|
||||||
|
if (!xacroArgs.hasOwnProperty('DEBUG')) {
|
||||||
|
xacroArgs.DEBUG = 'false';
|
||||||
|
}
|
||||||
|
if (!xacroArgs.hasOwnProperty('SELF_COLLIDE')) {
|
||||||
|
xacroArgs.SELF_COLLIDE = 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.arguments = xacroArgs;
|
||||||
|
|
||||||
|
// If fileMap provided, setup custom file loader
|
||||||
|
if (fileMap) {
|
||||||
|
parser.getFileContents = async (path) => {
|
||||||
|
return await this.loadFileFromMap(path, fileMap, workingPath);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse xacro to URDF XML
|
||||||
|
const urdfXML = await parser.parse(xacroContent);
|
||||||
|
|
||||||
|
|
||||||
|
// Convert XMLDocument to string
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
let urdfString = serializer.serializeToString(urdfXML);
|
||||||
|
|
||||||
|
// Clean up the XML string - remove empty xmlns attributes that might cause issues
|
||||||
|
urdfString = urdfString.replace(/\sxmlns=""/g, '');
|
||||||
|
|
||||||
|
// Re-parse the URDF string with DOMParser to create a clean XMLDocument
|
||||||
|
// This removes any xacro-specific nodes that urdf-loader might not handle
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
const cleanUrdfXML = domParser.parseFromString(urdfString, 'text/xml');
|
||||||
|
|
||||||
|
// Check for parsing errors
|
||||||
|
const parseError = cleanUrdfXML.querySelector('parsererror');
|
||||||
|
if (parseError) {
|
||||||
|
console.error('[XacroAdapter] XML parsing error:', parseError.textContent);
|
||||||
|
throw new Error('Generated URDF has invalid XML: ' + parseError.textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove problematic elements that urdf-loader might not handle well
|
||||||
|
// Remove gazebo elements (plugins, sensors, etc.)
|
||||||
|
const gazeboElements = cleanUrdfXML.querySelectorAll('gazebo');
|
||||||
|
gazeboElements.forEach(el => el.parentNode?.removeChild(el));
|
||||||
|
|
||||||
|
// Remove transmission elements (these are for Gazebo/ROS control)
|
||||||
|
const transmissionElements = cleanUrdfXML.querySelectorAll('transmission');
|
||||||
|
transmissionElements.forEach(el => el.parentNode?.removeChild(el));
|
||||||
|
|
||||||
|
// Remove visual/collision elements with empty geometry tags
|
||||||
|
this.removeEmptyGeometry(cleanUrdfXML);
|
||||||
|
|
||||||
|
// Clean up empty text nodes and comments that might cause issues
|
||||||
|
this.cleanXMLNodes(cleanUrdfXML.documentElement);
|
||||||
|
|
||||||
|
// Convert the clean XMLDocument back to string for URDFLoader
|
||||||
|
const finalUrdfString = serializer.serializeToString(cleanUrdfXML);
|
||||||
|
|
||||||
|
// Now use existing URDF loading infrastructure
|
||||||
|
// Import URDFLoader dynamically
|
||||||
|
const urdfModule = await import('urdf-loader');
|
||||||
|
const URDFLoader = urdfModule.URDFLoader || urdfModule.default || urdfModule;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const loader = new URDFLoader();
|
||||||
|
loader.parseCollision = true;
|
||||||
|
|
||||||
|
// Extract directory where URDF file is located
|
||||||
|
const urdfDir = workingPath;
|
||||||
|
|
||||||
|
// If file map provided, setup resource loader (same as URDF loading)
|
||||||
|
if (fileMap) {
|
||||||
|
// Parse URDF content, find all used package names
|
||||||
|
const packages = this.extractPackagesFromURDF(finalUrdfString);
|
||||||
|
|
||||||
|
// Build package map
|
||||||
|
const packageMap = {};
|
||||||
|
packages.forEach(pkg => {
|
||||||
|
packageMap[pkg] = pkg;
|
||||||
|
});
|
||||||
|
packageMap[''] = '';
|
||||||
|
|
||||||
|
loader.packages = packageMap;
|
||||||
|
|
||||||
|
// Set URL Modifier (same as URDF loading)
|
||||||
|
const urlModifier = (url) => {
|
||||||
|
// Handle blob URLs
|
||||||
|
if (url.startsWith('blob:')) {
|
||||||
|
const blobMatch = url.match(/^blob:https?:\/\/[^\/]+\/(.+)$/);
|
||||||
|
if (blobMatch && blobMatch[1]) {
|
||||||
|
const fileName = blobMatch[1];
|
||||||
|
if (/\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp|dae|stl|obj|gltf|glb)$/i.test(fileName)) {
|
||||||
|
url = fileName;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTextureFile = /\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp)$/i.test(url);
|
||||||
|
const isMeshFile = /\.(dae|stl|obj|gltf|glb)$/i.test(url);
|
||||||
|
|
||||||
|
let meshPath = url;
|
||||||
|
|
||||||
|
// Remove http:// or https:// prefix
|
||||||
|
if (meshPath.startsWith('http://') || meshPath.startsWith('https://')) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(meshPath);
|
||||||
|
meshPath = urlObj.pathname;
|
||||||
|
if (meshPath.startsWith('/')) {
|
||||||
|
meshPath = meshPath.substring(1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid URL, use as-is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove package:// prefix
|
||||||
|
if (meshPath.startsWith('package://')) {
|
||||||
|
meshPath = meshPath.replace(/^package:\/\//, '');
|
||||||
|
const parts = meshPath.split('/');
|
||||||
|
if (parts.length > 1) {
|
||||||
|
meshPath = parts.slice(1).join('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading ./
|
||||||
|
meshPath = meshPath.replace(/^\.\//, '');
|
||||||
|
|
||||||
|
// Handle relative paths
|
||||||
|
let normalizedPath = meshPath;
|
||||||
|
if (meshPath.includes('../')) {
|
||||||
|
const parts = meshPath.split('/');
|
||||||
|
const resolvedParts = [];
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part === '..') {
|
||||||
|
resolvedParts.pop();
|
||||||
|
} else if (part !== '.' && part !== '') {
|
||||||
|
resolvedParts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizedPath = resolvedParts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build full path
|
||||||
|
const fullPath = urdfDir + normalizedPath;
|
||||||
|
const altPath = urdfDir + meshPath;
|
||||||
|
|
||||||
|
// Find file in fileMap
|
||||||
|
let matchedFile = fileMap.get(fullPath);
|
||||||
|
|
||||||
|
if (!matchedFile && altPath !== fullPath) {
|
||||||
|
matchedFile = fileMap.get(altPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedFile) {
|
||||||
|
matchedFile = fileMap.get(normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedFile) {
|
||||||
|
matchedFile = fileMap.get(meshPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedFile) {
|
||||||
|
const targetFileName = normalizedPath.split('/').pop() || meshPath.split('/').pop();
|
||||||
|
for (const [key, file] of fileMap.entries()) {
|
||||||
|
const keyFileName = key.split('/').pop();
|
||||||
|
if (keyFileName === targetFileName) {
|
||||||
|
matchedFile = file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedFile) {
|
||||||
|
const bloburl = URL.createObjectURL(matchedFile);
|
||||||
|
return bloburl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
loader.manager.setURLModifier(urlModifier);
|
||||||
|
|
||||||
|
// Custom loadMeshCb (same as URDF loading)
|
||||||
|
const originalLoadMeshCb = loader.loadMeshCb || loader.defaultMeshLoader.bind(loader);
|
||||||
|
loader.loadMeshCb = (path, manager, done) => {
|
||||||
|
this.findFileInMapByPath(path, fileMap, urdfDir).then(file => {
|
||||||
|
if (file) {
|
||||||
|
const ext = (file.name || path).toLowerCase().split('.').pop();
|
||||||
|
this.loadMeshFileAsync(file, ext, manager).then(meshObject => {
|
||||||
|
if (meshObject) {
|
||||||
|
done(meshObject, null);
|
||||||
|
} else {
|
||||||
|
done(null, new Error(`Failed to load mesh file: ${path}`));
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(`Failed to load mesh: ${path}`, err);
|
||||||
|
done(null, err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let stlPath = path
|
||||||
|
stlPath = stlPath.replace('file:///', 'inspection/elfin10')
|
||||||
|
console.log(stlPath, manager, done)
|
||||||
|
originalLoadMeshCb(path, manager, done);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(`Failed to find file: ${path}`, error);
|
||||||
|
originalLoadMeshCb(path, manager, done);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary URL for URDF content
|
||||||
|
const blob = new Blob([finalUrdfString], { type: 'text/xml' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
loader.load(url, (robot) => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
// Convert to unified model using URDFAdapter
|
||||||
|
try {
|
||||||
|
const model = URDFAdapter.convert(robot, finalUrdfString);
|
||||||
|
resolve(model);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[XacroAdapter] URDF conversion error:', error);
|
||||||
|
reject(new Error('URDF conversion failed: ' + error.message));
|
||||||
|
}
|
||||||
|
}, undefined, (error) => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.error('[XacroAdapter] URDF loading error:', error);
|
||||||
|
reject(new Error('URDF loading failed: ' + (error.message || error)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[XacroAdapter] Xacro parsing error:', error);
|
||||||
|
throw new Error('Xacro parsing failed: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load file from fileMap for xacro includes
|
||||||
|
* @param {string} path - File path
|
||||||
|
* @param {Map} fileMap - File map
|
||||||
|
* @param {string} workingPath - Working directory
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static async loadFileFromMap(path, fileMap, workingPath) {
|
||||||
|
console.log('path', path)
|
||||||
|
console.log('fileMap', fileMap)
|
||||||
|
console.log('workingPath', workingPath)
|
||||||
|
// Clean path - remove leading slash if present
|
||||||
|
let cleanPath = path;
|
||||||
|
// if (cleanPath.startsWith('/')) {
|
||||||
|
// cleanPath = cleanPath.substring(1);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Remove leading ./
|
||||||
|
// cleanPath = cleanPath.replace(/^\.\//, '');
|
||||||
|
cleanPath = cleanPath.split('///')[1];
|
||||||
|
console.log('cleanPath', cleanPath)
|
||||||
|
// Try different path combinations
|
||||||
|
const possiblePaths = [
|
||||||
|
cleanPath,
|
||||||
|
workingPath + cleanPath,
|
||||||
|
path,
|
||||||
|
workingPath + path.replace(/^\/+/, ''),
|
||||||
|
// Also try without the first directory component
|
||||||
|
cleanPath.includes('/') ? cleanPath.substring(cleanPath.indexOf('/') + 1) : cleanPath,
|
||||||
|
];
|
||||||
|
console.log('possiblePaths', possiblePaths)
|
||||||
|
// Try each path
|
||||||
|
for (const tryPath of possiblePaths) {
|
||||||
|
console.log('tryPath', tryPath)
|
||||||
|
const file = fileMap.get(tryPath);
|
||||||
|
if (file) {
|
||||||
|
console.log(12345)
|
||||||
|
const content = await file.text();
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try filename only match
|
||||||
|
const fileName = cleanPath.split('/').pop();
|
||||||
|
for (const [key, file] of fileMap.entries()) {
|
||||||
|
const keyFileName = key.split('/').pop();
|
||||||
|
if (keyFileName === fileName) {
|
||||||
|
const content = await file.text();
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[XacroAdapter] Cannot find included file:', path);
|
||||||
|
throw new Error(`Cannot find included file: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove visual/collision elements with empty geometry tags
|
||||||
|
* xacro-parser may not fully expand conditional geometry (xacro:if)
|
||||||
|
* We'll just remove these empty visual elements rather than try to fix them
|
||||||
|
* @param {XMLDocument} xmlDoc - XML document to clean
|
||||||
|
*/
|
||||||
|
static removeEmptyGeometry(xmlDoc) {
|
||||||
|
// Find all geometry elements
|
||||||
|
const geometryElements = xmlDoc.querySelectorAll('geometry');
|
||||||
|
|
||||||
|
geometryElements.forEach(geom => {
|
||||||
|
// Check if geometry is empty (no child elements)
|
||||||
|
if (!geom.children || geom.children.length === 0) {
|
||||||
|
// Find the parent visual or collision element
|
||||||
|
let parent = geom.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
const grandParent = parent.parentNode;
|
||||||
|
if (grandParent) {
|
||||||
|
if (parent.nodeName === 'visual' || parent.nodeName === 'collision') {
|
||||||
|
// Remove empty visual/collision elements
|
||||||
|
grandParent.removeChild(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively clean XML nodes - remove empty text nodes and comments
|
||||||
|
* @param {Element} element - Element to clean
|
||||||
|
*/
|
||||||
|
static cleanXMLNodes(element) {
|
||||||
|
if (!element || !element.childNodes) return;
|
||||||
|
|
||||||
|
const nodesToRemove = [];
|
||||||
|
|
||||||
|
// Collect nodes to remove
|
||||||
|
for (let i = 0; i < element.childNodes.length; i++) {
|
||||||
|
const node = element.childNodes[i];
|
||||||
|
|
||||||
|
// Remove comments
|
||||||
|
if (node.nodeType === Node.COMMENT_NODE) {
|
||||||
|
nodesToRemove.push(node);
|
||||||
|
}
|
||||||
|
// Remove empty or whitespace-only text nodes
|
||||||
|
else if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
if (!node.nodeValue || node.nodeValue.trim() === '') {
|
||||||
|
nodesToRemove.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recursively clean element nodes
|
||||||
|
else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
this.cleanXMLNodes(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove collected nodes
|
||||||
|
nodesToRemove.forEach(node => {
|
||||||
|
element.removeChild(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract package names from URDF content
|
||||||
|
* @param {string} urdfContent - URDF content
|
||||||
|
* @returns {Set<string>}
|
||||||
|
*/
|
||||||
|
static extractPackagesFromURDF(urdfContent) {
|
||||||
|
const packages = new Set();
|
||||||
|
const packageRegex = /package:\/\/([^\/]+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = packageRegex.exec(urdfContent)) !== null) {
|
||||||
|
packages.add(match[1]);
|
||||||
|
}
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find file in fileMap by path
|
||||||
|
* @param {string} path - File path
|
||||||
|
* @param {Map} fileMap - File map
|
||||||
|
* @param {string} urdfDir - URDF directory
|
||||||
|
* @returns {Promise<File|null>}
|
||||||
|
*/
|
||||||
|
static async findFileInMapByPath(path, fileMap, urdfDir) {
|
||||||
|
let meshPath = path;
|
||||||
|
|
||||||
|
// Remove blob: prefix
|
||||||
|
meshPath = meshPath.replace(/^blob:[^\/]+\//, '');
|
||||||
|
|
||||||
|
// Remove package:// prefix
|
||||||
|
if (meshPath.startsWith('package://')) {
|
||||||
|
meshPath = meshPath.replace(/^package:\/\//, '');
|
||||||
|
const parts = meshPath.split('/');
|
||||||
|
if (parts.length > 1) {
|
||||||
|
meshPath = parts.slice(1).join('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading ./
|
||||||
|
meshPath = meshPath.replace(/^\.\//, '');
|
||||||
|
|
||||||
|
// Build full path
|
||||||
|
const fullPath = urdfDir + meshPath;
|
||||||
|
|
||||||
|
// Try full path
|
||||||
|
let file = fileMap.get(fullPath);
|
||||||
|
if (file) return file;
|
||||||
|
|
||||||
|
// Try path without directory
|
||||||
|
file = fileMap.get(meshPath);
|
||||||
|
if (file) return file;
|
||||||
|
|
||||||
|
// Try filename match
|
||||||
|
const targetFileName = meshPath.split('/').pop();
|
||||||
|
for (const [key, f] of fileMap.entries()) {
|
||||||
|
const keyFileName = key.split('/').pop();
|
||||||
|
if (keyFileName === targetFileName) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load mesh file asynchronously
|
||||||
|
* @param {File} file - File object
|
||||||
|
* @param {string} ext - File extension
|
||||||
|
* @param {THREE.LoadingManager} manager - Loading manager
|
||||||
|
* @returns {Promise<THREE.Object3D>}
|
||||||
|
*/
|
||||||
|
static async loadMeshFileAsync(file, ext, manager) {
|
||||||
|
const THREE = await import('three');
|
||||||
|
const blobUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let meshObject = null;
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'stl': {
|
||||||
|
const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
|
||||||
|
const stlLoader = new STLLoader(manager);
|
||||||
|
const stlGeometry = await new Promise((resolve, reject) => {
|
||||||
|
stlLoader.load(blobUrl, resolve, undefined, reject);
|
||||||
|
});
|
||||||
|
const stlMaterial = new THREE.MeshPhongMaterial();
|
||||||
|
meshObject = new THREE.Mesh(stlGeometry, stlMaterial);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'dae': {
|
||||||
|
const { ColladaLoader } = await import('three/examples/jsm/loaders/ColladaLoader.js');
|
||||||
|
const colladaLoader = new ColladaLoader(manager);
|
||||||
|
const colladaModel = await new Promise((resolve, reject) => {
|
||||||
|
colladaLoader.load(blobUrl, resolve, undefined, reject);
|
||||||
|
});
|
||||||
|
meshObject = colladaModel.scene;
|
||||||
|
|
||||||
|
// Remove lights
|
||||||
|
if (meshObject && meshObject.traverse) {
|
||||||
|
const lightsToRemove = [];
|
||||||
|
meshObject.traverse(child => {
|
||||||
|
if (child.isLight) {
|
||||||
|
lightsToRemove.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lightsToRemove.forEach(light => {
|
||||||
|
if (light.parent) {
|
||||||
|
light.parent.remove(light);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'obj': {
|
||||||
|
const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
|
||||||
|
const objLoader = new OBJLoader(manager);
|
||||||
|
meshObject = await new Promise((resolve, reject) => {
|
||||||
|
objLoader.load(blobUrl, resolve, undefined, reject);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'gltf':
|
||||||
|
case 'glb': {
|
||||||
|
const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
|
||||||
|
const gltfLoader = new GLTFLoader(manager);
|
||||||
|
const gltfModel = await new Promise((resolve, reject) => {
|
||||||
|
gltfLoader.load(blobUrl, resolve, undefined, reject);
|
||||||
|
});
|
||||||
|
meshObject = gltfModel.scene;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unsupported file format: ${ext}`);
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
return meshObject;
|
||||||
|
} catch (error) {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
console.error(`Failed to load mesh file: ${file.name}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject Python-style boolean constants (True/False) into xacro content
|
||||||
|
* Some xacro files use capitalized True/False in conditional expressions
|
||||||
|
* The parameters are passed as strings ("True", "False") but compared to constants
|
||||||
|
* We define the constants to match the string values for comparison to work
|
||||||
|
* @param {string} xacroContent - Original xacro content
|
||||||
|
* @returns {string} - Modified xacro content with boolean constants defined
|
||||||
|
*/
|
||||||
|
static injectBooleanConstants(xacroContent) {
|
||||||
|
// Find the robot tag opening
|
||||||
|
const robotTagMatch = xacroContent.match(/(<robot[^>]*>)/);
|
||||||
|
if (!robotTagMatch) {
|
||||||
|
return xacroContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define True/False as string properties for comparison
|
||||||
|
// When mirror_dae="True" is passed, it becomes the string "True"
|
||||||
|
// We want ${mirror_dae == True} to work, so True must also be "True"
|
||||||
|
const booleanProperties = `
|
||||||
|
<!-- Injected by XacroAdapter: Python-style boolean constants -->
|
||||||
|
<xacro:property name="True" value="True"/>
|
||||||
|
<xacro:property name="False" value="False"/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const insertPosition = robotTagMatch.index + robotTagMatch[0].length;
|
||||||
|
return xacroContent.substring(0, insertPosition) +
|
||||||
|
booleanProperties +
|
||||||
|
xacroContent.substring(insertPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract xacro arguments and their default values from xacro content
|
||||||
|
* Xacro files can define arguments using <xacro:arg name="..." default="..."/>
|
||||||
|
* These need to be provided to the parser via parser.arguments
|
||||||
|
* @param {string} xacroContent - Xacro file content
|
||||||
|
* @returns {Object} - Object with argument names as keys and default values
|
||||||
|
*/
|
||||||
|
static extractXacroArguments(xacroContent) {
|
||||||
|
const args = {};
|
||||||
|
|
||||||
|
// Match <xacro:arg name="NAME" default="VALUE"/>
|
||||||
|
// Also match <arg name="NAME" default="VALUE"/> (without xacro: prefix)
|
||||||
|
const argPattern = /<(?:xacro:)?arg\s+name=["']([^"']+)["'](?:\s+default=["']([^"']*)["'])?/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = argPattern.exec(xacroContent)) !== null) {
|
||||||
|
const argName = match[1];
|
||||||
|
const defaultValue = match[2] || 'false'; // Default to 'false' if no default specified
|
||||||
|
args[argName] = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
553
src/controllers/FileHandler.js
Normal file
553
src/controllers/FileHandler.js
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
/**
|
||||||
|
* FileHandler - File handling module
|
||||||
|
* Handles file drag-drop, loading, type detection, etc.
|
||||||
|
*/
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { ModelLoaderFactory } from '../loaders/ModelLoaderFactory.js';
|
||||||
|
import { readFileContent, getFileFromEntry, getFileTypeFromExtension, getFileDisplayType } from '../utils/FileUtils.js';
|
||||||
|
|
||||||
|
export class FileHandler {
|
||||||
|
constructor() {
|
||||||
|
this.fileMap = new Map();
|
||||||
|
this.availableModels = [];
|
||||||
|
this.currentModelFile = null;
|
||||||
|
this.onModelLoaded = null; // Callback function
|
||||||
|
this.usdViewerManager = null; // USD viewer manager (lazy loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set USD viewer manager
|
||||||
|
*/
|
||||||
|
setUSDViewerManager(manager) {
|
||||||
|
this.usdViewerManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set USD viewer initializer callback (for lazy loading)
|
||||||
|
*/
|
||||||
|
setUSDViewerInitializer(initializer) {
|
||||||
|
this.usdViewerInitializer = initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup file drag-drop
|
||||||
|
*/
|
||||||
|
setupFileDrop() {
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
const preventDefaults = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
body.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
let dragCounter = 0;
|
||||||
|
body.addEventListener('dragenter', (e) => {
|
||||||
|
dragCounter++;
|
||||||
|
const dropZone = document.getElementById('drop-zone');
|
||||||
|
if (dropZone) dropZone.classList.add('drag-over');
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
body.addEventListener('dragleave', (e) => {
|
||||||
|
dragCounter--;
|
||||||
|
if (dragCounter === 0) {
|
||||||
|
const dropZone = document.getElementById('drop-zone');
|
||||||
|
if (dropZone) dropZone.classList.remove('drag-over');
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
body.addEventListener('drop', (e) => {
|
||||||
|
dragCounter = 0;
|
||||||
|
const dropZone = document.getElementById('drop-zone');
|
||||||
|
if (dropZone) dropZone.classList.remove('drag-over');
|
||||||
|
this.handleDrop(e);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle file drop
|
||||||
|
*/
|
||||||
|
async handleDrop(e) {
|
||||||
|
const items = e.dataTransfer.items;
|
||||||
|
if (!items || items.length === 0) return;
|
||||||
|
|
||||||
|
this.fileMap.clear();
|
||||||
|
|
||||||
|
const entries = [];
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
if (item.webkitGetAsEntry) {
|
||||||
|
const entry = item.webkitGetAsEntry();
|
||||||
|
if (entry) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
await this.processEntries(entries);
|
||||||
|
} else {
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
for (const file of files) {
|
||||||
|
const path = file.webkitRelativePath || file.name;
|
||||||
|
this.fileMap.set(path, file);
|
||||||
|
// Only add file.name as a separate key if there's no webkitRelativePath
|
||||||
|
// This prevents duplicate entries in the file tree
|
||||||
|
if (!file.webkitRelativePath) {
|
||||||
|
this.fileMap.set(file.name, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadableFiles = await this.findAllLoadableFiles(Array.from(files));
|
||||||
|
if (loadableFiles.length > 0) {
|
||||||
|
this.availableModels = loadableFiles;
|
||||||
|
this.onFilesLoaded?.(loadableFiles);
|
||||||
|
await this.loadFileOrMesh(loadableFiles[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process file system entries
|
||||||
|
*/
|
||||||
|
async processEntries(entries) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile) {
|
||||||
|
const file = await getFileFromEntry(entry);
|
||||||
|
const path = entry.fullPath || entry.name;
|
||||||
|
this.fileMap.set(path, file);
|
||||||
|
files.push(file);
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
const dirFiles = await this.readDirectory(entry);
|
||||||
|
files.push(...dirFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
const loadableFiles = await this.findAllLoadableFiles(files);
|
||||||
|
|
||||||
|
if (loadableFiles.length === 0) {
|
||||||
|
this.onFilesLoaded?.([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.availableModels = loadableFiles;
|
||||||
|
this.onFilesLoaded?.(loadableFiles);
|
||||||
|
|
||||||
|
if (loadableFiles.length > 0) {
|
||||||
|
await this.loadFileOrMesh(loadableFiles[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively read directory
|
||||||
|
*/
|
||||||
|
async readDirectory(dirEntry) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = dirEntry.createReader();
|
||||||
|
|
||||||
|
const readEntries = () => {
|
||||||
|
reader.readEntries(async (entries) => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
resolve(files);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile) {
|
||||||
|
const file = await getFileFromEntry(entry);
|
||||||
|
const path = entry.fullPath || entry.name;
|
||||||
|
this.fileMap.set(path, file);
|
||||||
|
files.push(file);
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
const subFiles = await this.readDirectory(entry);
|
||||||
|
files.push(...subFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readEntries();
|
||||||
|
}, reject);
|
||||||
|
};
|
||||||
|
|
||||||
|
readEntries();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all loadable files
|
||||||
|
*/
|
||||||
|
async findAllLoadableFiles(files) {
|
||||||
|
const supportedExtensions = {
|
||||||
|
model: ['urdf', 'xacro', 'xml', 'usd', 'usda', 'usdc', 'usdz'],
|
||||||
|
mesh: ['dae', 'stl', 'obj', 'collada']
|
||||||
|
};
|
||||||
|
const loadableFiles = [];
|
||||||
|
|
||||||
|
const checkPromises = files.map(async (file) => {
|
||||||
|
const ext = file.name.toLowerCase().split('.').pop();
|
||||||
|
|
||||||
|
if (supportedExtensions.model.includes(ext)) {
|
||||||
|
if (ext === 'xml') {
|
||||||
|
try {
|
||||||
|
const content = await readFileContent(file);
|
||||||
|
const fileType = ModelLoaderFactory.detectFileType(file.name, content);
|
||||||
|
|
||||||
|
if (!fileType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
type: fileType,
|
||||||
|
path: file.webkitRelativePath || file.name,
|
||||||
|
category: 'model'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to read XML file: ${file.name}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fileType = getFileTypeFromExtension(ext);
|
||||||
|
return {
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
type: fileType,
|
||||||
|
path: file.webkitRelativePath || file.name,
|
||||||
|
category: 'model'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (supportedExtensions.mesh.includes(ext)) {
|
||||||
|
return {
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
type: 'mesh',
|
||||||
|
path: file.webkitRelativePath || file.name,
|
||||||
|
category: 'mesh'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(checkPromises);
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
if (result) {
|
||||||
|
loadableFiles.push(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadableFiles.sort((a, b) => {
|
||||||
|
if (a.category !== b.category) {
|
||||||
|
return a.category === 'model' ? -1 : 1;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadableFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load file or mesh
|
||||||
|
*/
|
||||||
|
async loadFileOrMesh(fileInfo) {
|
||||||
|
if (fileInfo.category === 'model') {
|
||||||
|
await this.loadFile(fileInfo.file);
|
||||||
|
} else if (fileInfo.category === 'mesh') {
|
||||||
|
await this.loadMeshAsModel(fileInfo.file, fileInfo.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load model file
|
||||||
|
*/
|
||||||
|
async loadFile(file) {
|
||||||
|
this.currentModelFile = file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileName = file.name.toLowerCase();
|
||||||
|
|
||||||
|
// Get the full path from fileMap
|
||||||
|
let fullPath = file.name;
|
||||||
|
for (const [path, f] of this.fileMap.entries()) {
|
||||||
|
if (f === file) {
|
||||||
|
fullPath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle USD format files (all USD formats use WASM)
|
||||||
|
const isUSD = fileName.endsWith('.usd') || fileName.endsWith('.usda') ||
|
||||||
|
fileName.endsWith('.usdc') || fileName.endsWith('.usdz');
|
||||||
|
if (isUSD) {
|
||||||
|
// Ensure USD viewer is initialized
|
||||||
|
if (!this.usdViewerManager && this.usdViewerInitializer) {
|
||||||
|
try {
|
||||||
|
await this.usdViewerInitializer();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('USD viewer initialization failed:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fileMap.size === 0) {
|
||||||
|
this.fileMap.set(file.name, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// USD files need File object passed
|
||||||
|
const model = await ModelLoaderFactory.loadModel(
|
||||||
|
'usd',
|
||||||
|
null,
|
||||||
|
fullPath,
|
||||||
|
this.fileMap,
|
||||||
|
file,
|
||||||
|
{ usdViewerManager: this.usdViewerManager }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onModelLoaded?.(model, file, false, null);
|
||||||
|
document.getElementById('drop-zone')?.classList.remove('show');
|
||||||
|
document.getElementById('drop-zone')?.classList.remove('drag-over');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other files, read text content
|
||||||
|
const content = await readFileContent(file);
|
||||||
|
|
||||||
|
// Detect if USDC binary format (based on content)
|
||||||
|
if (this.isUSDCBinaryContent(content)) {
|
||||||
|
console.error('Cannot load USDC binary format, please convert to USDZ or USDA');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileType = ModelLoaderFactory.detectFileType(file.name, content);
|
||||||
|
|
||||||
|
if (!fileType) {
|
||||||
|
console.error(`${window.i18n.t('unsupportedFormat')}: ${file.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fileMap.size === 0) {
|
||||||
|
this.fileMap.set(file.name, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = await ModelLoaderFactory.loadModel(
|
||||||
|
fileType,
|
||||||
|
content,
|
||||||
|
fullPath, // Use full path instead of just file.name
|
||||||
|
this.fileMap,
|
||||||
|
file,
|
||||||
|
{ usdViewerManager: this.usdViewerManager }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notify model loaded (pass null as snapshot, let main.js create it)
|
||||||
|
this.onModelLoaded?.(model, file, false, null);
|
||||||
|
|
||||||
|
document.getElementById('drop-zone')?.classList.remove('show');
|
||||||
|
document.getElementById('drop-zone')?.classList.remove('drag-over');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load file:', error);
|
||||||
|
|
||||||
|
// If error message contains USDC related content
|
||||||
|
if (error.message && (error.message.includes('USDC') || error.message.includes('binary format'))) {
|
||||||
|
console.error('Cannot load USDC binary format, please convert to USDZ or USDA');
|
||||||
|
} else {
|
||||||
|
console.error(`${window.i18n.t('loadFailed')}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove snapshot if exists
|
||||||
|
const snapshot = document.getElementById('canvas-snapshot');
|
||||||
|
if (snapshot?.parentNode) {
|
||||||
|
snapshot.parentNode.removeChild(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if file content is USDC binary format
|
||||||
|
*/
|
||||||
|
isUSDCBinaryContent(content) {
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect USDC binary format characteristics
|
||||||
|
// 1. Starts with "PXR-USDC" (USDC file magic bytes)
|
||||||
|
if (content.startsWith('PXR-USDC')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check first 100 bytes, if contains many null bytes, likely binary
|
||||||
|
const firstBytes = content.substring(0, 100);
|
||||||
|
const nullCount = (firstBytes.match(/\x00/g) || []).length;
|
||||||
|
if (nullCount > 10) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check if valid ASCII USD file
|
||||||
|
// Valid USDA files usually start with #usda or contain def keywords
|
||||||
|
if (content.includes('#usda') ||
|
||||||
|
(content.includes('def ') && content.includes('{'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. File starts with non-printable characters (excluding newline, carriage return, tab, space)
|
||||||
|
const firstChar = content.charCodeAt(0);
|
||||||
|
if (firstChar < 32 && firstChar !== 10 && firstChar !== 13 && firstChar !== 9 && firstChar !== 32) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create loading snapshot
|
||||||
|
*/
|
||||||
|
createLoadingSnapshot() {
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
if (!canvas) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataURL = canvas.toDataURL('image/png');
|
||||||
|
const snapshot = document.createElement('div');
|
||||||
|
snapshot.id = 'canvas-snapshot';
|
||||||
|
snapshot.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url(${dataURL});
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const canvasContainer = document.getElementById('canvas-container');
|
||||||
|
if (canvasContainer) {
|
||||||
|
canvasContainer.appendChild(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create snapshot:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove loading snapshot
|
||||||
|
*/
|
||||||
|
removeLoadingSnapshot(snapshot) {
|
||||||
|
if (!snapshot || !snapshot.parentNode) return;
|
||||||
|
|
||||||
|
snapshot.style.transition = 'opacity 0.3s ease';
|
||||||
|
snapshot.style.opacity = '0';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (snapshot.parentNode) {
|
||||||
|
snapshot.parentNode.removeChild(snapshot);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load single mesh file as model
|
||||||
|
*/
|
||||||
|
async loadMeshAsModel(file, fileName) {
|
||||||
|
try {
|
||||||
|
const meshObject = await ModelLoaderFactory.loadMeshFileDirect(file, fileName);
|
||||||
|
|
||||||
|
if (!meshObject) {
|
||||||
|
throw new Error(window.i18n.t('cannotLoadMesh'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mesh materials support lighting and shadows
|
||||||
|
meshObject.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
if (child.material?.type === 'MeshBasicMaterial') {
|
||||||
|
const oldMaterial = child.material;
|
||||||
|
child.material = new THREE.MeshPhongMaterial({
|
||||||
|
color: oldMaterial.color,
|
||||||
|
map: oldMaterial.map,
|
||||||
|
transparent: oldMaterial.transparent,
|
||||||
|
opacity: oldMaterial.opacity,
|
||||||
|
side: oldMaterial.side,
|
||||||
|
shininess: 30
|
||||||
|
});
|
||||||
|
if (child.material.map) {
|
||||||
|
child.material.map.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const simpleMeshModel = {
|
||||||
|
name: fileName,
|
||||||
|
rootLink: 'mesh_root',
|
||||||
|
links: new Map([
|
||||||
|
['mesh_root', {
|
||||||
|
name: 'mesh_root',
|
||||||
|
threeObject: meshObject,
|
||||||
|
visuals: [{
|
||||||
|
threeObject: meshObject,
|
||||||
|
geometry: { mesh: fileName }
|
||||||
|
}],
|
||||||
|
inertial: null
|
||||||
|
}]
|
||||||
|
]),
|
||||||
|
joints: new Map(),
|
||||||
|
threeObject: meshObject
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentModelFile = file;
|
||||||
|
this.onModelLoaded?.(simpleMeshModel, file, true, null);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load mesh file:', error);
|
||||||
|
|
||||||
|
const snapshot = document.getElementById('canvas-snapshot');
|
||||||
|
if (snapshot?.parentNode) {
|
||||||
|
snapshot.parentNode.removeChild(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file map
|
||||||
|
*/
|
||||||
|
getFileMap() {
|
||||||
|
return this.fileMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models list
|
||||||
|
*/
|
||||||
|
getAvailableModels() {
|
||||||
|
return this.availableModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current model file
|
||||||
|
*/
|
||||||
|
getCurrentModelFile() {
|
||||||
|
return this.currentModelFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
181
src/models/UnifiedRobotModel.js
Normal file
181
src/models/UnifiedRobotModel.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Unified robot model data interface
|
||||||
|
* All formats (URDF, MJCF, USD) are converted to this unified format
|
||||||
|
*/
|
||||||
|
export class UnifiedRobotModel {
|
||||||
|
constructor() {
|
||||||
|
this.name = '';
|
||||||
|
this.links = new Map(); // Map<name, Link>
|
||||||
|
this.joints = new Map(); // Map<name, Joint>
|
||||||
|
this.materials = new Map(); // Map<name, Material>
|
||||||
|
this.constraints = new Map(); // Map<name, Constraint> - for parallel mechanism constraints
|
||||||
|
this.rootLink = null; // Root link name
|
||||||
|
this.threeObject = null; // Three.js object (if available)
|
||||||
|
}
|
||||||
|
|
||||||
|
addLink(link) {
|
||||||
|
this.links.set(link.name, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
addJoint(joint) {
|
||||||
|
this.joints.set(joint.name, joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
addConstraint(constraint) {
|
||||||
|
this.constraints.set(constraint.name, constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLink(name) {
|
||||||
|
return this.links.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getJoint(name) {
|
||||||
|
return this.joints.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConstraint(name) {
|
||||||
|
return this.constraints.get(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link interface
|
||||||
|
*/
|
||||||
|
export class Link {
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.visuals = []; // VisualGeometry[]
|
||||||
|
this.collisions = []; // CollisionGeometry[]
|
||||||
|
this.inertial = null; // InertialProperties
|
||||||
|
this.threeObject = null; // Three.js object
|
||||||
|
this.userData = {}; // User-defined data (for adapters to store additional information)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VisualGeometry interface
|
||||||
|
*/
|
||||||
|
export class VisualGeometry {
|
||||||
|
constructor() {
|
||||||
|
this.name = '';
|
||||||
|
this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
|
||||||
|
this.geometry = null; // GeometryType
|
||||||
|
this.material = null; // Material
|
||||||
|
this.threeObject = null; // Three.js Mesh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CollisionGeometry interface
|
||||||
|
*/
|
||||||
|
export class CollisionGeometry {
|
||||||
|
constructor() {
|
||||||
|
this.name = '';
|
||||||
|
this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
|
||||||
|
this.geometry = null; // GeometryType
|
||||||
|
this.threeObject = null; // Three.js Mesh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeometryType interface
|
||||||
|
*/
|
||||||
|
export class GeometryType {
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type; // 'box' | 'sphere' | 'cylinder' | 'mesh'
|
||||||
|
this.size = null; // Size parameters (varies by type)
|
||||||
|
this.filename = null; // Mesh file path (if mesh type)
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
const cloned = new GeometryType(this.type);
|
||||||
|
cloned.size = this.size ? { ...this.size } : null;
|
||||||
|
cloned.filename = this.filename;
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InertialProperties interface
|
||||||
|
*/
|
||||||
|
export class InertialProperties {
|
||||||
|
constructor() {
|
||||||
|
this.mass = 0;
|
||||||
|
this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
|
||||||
|
this.ixx = 0;
|
||||||
|
this.iyy = 0;
|
||||||
|
this.izz = 0;
|
||||||
|
this.ixy = 0;
|
||||||
|
this.ixz = 0;
|
||||||
|
this.iyz = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joint interface
|
||||||
|
*/
|
||||||
|
export class Joint {
|
||||||
|
constructor(name, type) {
|
||||||
|
this.name = name;
|
||||||
|
this.type = type; // 'revolute' | 'prismatic' | 'fixed' | 'continuous'
|
||||||
|
this.parent = null; // Parent link name
|
||||||
|
this.child = null; // Child link name
|
||||||
|
this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
|
||||||
|
this.axis = { xyz: [0, 0, 1] }; // Default z-axis
|
||||||
|
this.limits = null; // JointLimits
|
||||||
|
this.currentValue = 0; // Current joint value
|
||||||
|
this.threeObject = null; // Three.js object (if available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JointLimits interface
|
||||||
|
*/
|
||||||
|
export class JointLimits {
|
||||||
|
constructor() {
|
||||||
|
this.lower = -Math.PI;
|
||||||
|
this.upper = Math.PI;
|
||||||
|
this.effort = null;
|
||||||
|
this.velocity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Material interface
|
||||||
|
*/
|
||||||
|
export class Material {
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.color = { r: 0.8, g: 0.8, b: 0.8 };
|
||||||
|
this.texture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constraint interface - for describing closed-chain constraints of parallel mechanisms
|
||||||
|
* Supports MuJoCo equality constraint types
|
||||||
|
*/
|
||||||
|
export class Constraint {
|
||||||
|
constructor(name, type) {
|
||||||
|
this.name = name;
|
||||||
|
this.type = type; // 'connect' | 'weld' | 'joint' | 'tendon' | 'distance'
|
||||||
|
|
||||||
|
// Constraint objects (may be body, geom, joint, etc. depending on type)
|
||||||
|
this.body1 = null;
|
||||||
|
this.body2 = null;
|
||||||
|
this.anchor = null; // Connection point coordinates
|
||||||
|
this.torquescale = null; // Torque scale
|
||||||
|
|
||||||
|
// Joint constraint specific properties
|
||||||
|
this.joint1 = null;
|
||||||
|
this.joint2 = null;
|
||||||
|
this.polycoef = null; // Polynomial coefficients [a0, a1, a2, a3, a4]
|
||||||
|
|
||||||
|
// Visualization object
|
||||||
|
this.threeObject = null; // Three.js object for displaying constraint
|
||||||
|
|
||||||
|
// Original data (for debugging)
|
||||||
|
this.userData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
144
src/utils/FileUtils.js
Normal file
144
src/utils/FileUtils.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* File operation utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file content as text
|
||||||
|
*/
|
||||||
|
export function readFileContent(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => resolve(e.target.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get File object from file system entry
|
||||||
|
*/
|
||||||
|
export function getFileFromEntry(entry) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
entry.file(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively read directory
|
||||||
|
*/
|
||||||
|
export async function readDirectory(dirEntry, fileMap) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = dirEntry.createReader();
|
||||||
|
|
||||||
|
function readEntries() {
|
||||||
|
reader.readEntries(async (entries) => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
resolve(files);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile) {
|
||||||
|
const file = await getFileFromEntry(entry);
|
||||||
|
const path = entry.fullPath || entry.name;
|
||||||
|
fileMap.set(path, file);
|
||||||
|
files.push(file);
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
const subFiles = await readDirectory(entry, fileMap);
|
||||||
|
files.push(...subFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readEntries();
|
||||||
|
}, reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
readEntries();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file type from extension
|
||||||
|
*/
|
||||||
|
export function getFileTypeFromExtension(ext) {
|
||||||
|
const typeMap = {
|
||||||
|
'urdf': 'urdf',
|
||||||
|
'xacro': 'xacro',
|
||||||
|
'xml': 'mjcf',
|
||||||
|
'usd': 'usd',
|
||||||
|
'usda': 'usd',
|
||||||
|
'usdc': 'usd',
|
||||||
|
'usdz': 'usd'
|
||||||
|
};
|
||||||
|
return typeMap[ext] || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file display type
|
||||||
|
*/
|
||||||
|
export function getFileDisplayType(ext, fileName) {
|
||||||
|
const modelExts = ['urdf', 'xacro', 'xml', 'usd', 'usda', 'usdc', 'usdz'];
|
||||||
|
const meshExts = ['dae', 'stl', 'obj', 'collada'];
|
||||||
|
|
||||||
|
if (modelExts.includes(ext)) {
|
||||||
|
return 'model';
|
||||||
|
} else if (meshExts.includes(ext)) {
|
||||||
|
return 'mesh';
|
||||||
|
}
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize path
|
||||||
|
*/
|
||||||
|
export function normalizePath(path) {
|
||||||
|
if (!path) return '';
|
||||||
|
return path.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const detectFileType = (fileName, content) => {
|
||||||
|
const ext = fileName.toLowerCase().split('.').pop();
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'urdf':
|
||||||
|
return 'urdf';
|
||||||
|
case 'xacro':
|
||||||
|
return 'xacro';
|
||||||
|
case 'xml':
|
||||||
|
if (content) {
|
||||||
|
if (isValidRobotMJCF(content)) {
|
||||||
|
return 'mjcf';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case 'usd':
|
||||||
|
case 'usda':
|
||||||
|
case 'usdc':
|
||||||
|
case 'usdz':
|
||||||
|
return 'usd';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidRobotMJCF = (content) => {
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.includes('<mujoco')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasJoints = content.includes('<joint');
|
||||||
|
const hasActuators = content.includes('<actuator');
|
||||||
|
|
||||||
|
if (hasJoints || hasActuators) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@ -30,7 +30,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Chassis from './chassis/Chassis.vue';
|
import Chassis from './chassis/Chassis.vue';
|
||||||
import MechanicalArm from './MechanicalArm.vue';
|
import MechanicalArm from './arm/MechanicalArm.vue';
|
||||||
|
|
||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
const weekText = ref('')
|
const weekText = ref('')
|
||||||
@ -63,7 +63,7 @@ const animateBinary = () => {
|
|||||||
const activePage = ref('chassis')
|
const activePage = ref('chassis')
|
||||||
const handleBtn = (page) => {
|
const handleBtn = (page) => {
|
||||||
activePage.value = page
|
activePage.value = page
|
||||||
emits("activePage", page);
|
// emits("activePage", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
let clockTimer, binaryTimer;
|
let clockTimer, binaryTimer;
|
||||||
@ -147,7 +147,7 @@ onUnmounted(() => {
|
|||||||
gap: 12px ;
|
gap: 12px ;
|
||||||
|
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
background: rgba(0, 60, 140, 0.5);
|
background: #003c8c80;
|
||||||
border: 1px solid rgba(0, 194, 255, 0.35);
|
border: 1px solid rgba(0, 194, 255, 0.35);
|
||||||
color: #a0d8f0;
|
color: #a0d8f0;
|
||||||
padding: 6px 20px;
|
padding: 6px 20px;
|
||||||
@ -173,7 +173,7 @@ onUnmounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-family: 'Orbitron', monospace;
|
font-family: 'Orbitron', monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: rgba(0, 194, 255, 0.3);
|
color: #00c2ff4d;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -1,385 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="robot-teach-container">
|
|
||||||
<!-- 主体内容区 -->
|
|
||||||
<div class="main-wrap">
|
|
||||||
<!-- 左侧数据面板 -->
|
|
||||||
<div class="left-panel">
|
|
||||||
<!-- TCP 笛卡尔坐标 -->
|
|
||||||
<div class="panel-card">
|
|
||||||
<div class="row dual-row">
|
|
||||||
<div class="coord-item">
|
|
||||||
<span class="key">X</span>
|
|
||||||
<el-input v-model.number="tcp.x" suffix="mm" />
|
|
||||||
</div>
|
|
||||||
<div class="coord-item">
|
|
||||||
<span class="key">RX</span>
|
|
||||||
<el-input v-model.number="tcp.rx" suffix="°" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row dual-row">
|
|
||||||
<div class="coord-item">
|
|
||||||
<span class="key">Y</span>
|
|
||||||
<el-input v-model.number="tcp.y" suffix="mm" />
|
|
||||||
</div>
|
|
||||||
<div class="coord-item">
|
|
||||||
<span class="key">RY</span>
|
|
||||||
<el-input v-model.number="tcp.ry" suffix="°" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row dual-row">
|
|
||||||
<div class="coord-item">
|
|
||||||
<span class="key">Z</span>
|
|
||||||
<el-input v-model.number="tcp.z" suffix="mm" />
|
|
||||||
</div>
|
|
||||||
<div class="coord-item">
|
|
||||||
<span class="key">RZ</span>
|
|
||||||
<el-input v-model.number="tcp.rz" suffix="°" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 关节 J1-J6 -->
|
|
||||||
<div class="panel-card joint-card">
|
|
||||||
<div class="joint-row" v-for="(item, idx) in jointList" :key="idx">
|
|
||||||
<span class="joint-key">{{ item.name }}</span>
|
|
||||||
<el-input v-model.number="item.val" />
|
|
||||||
<div class="btn-group">
|
|
||||||
<el-button size="small">-</el-button>
|
|
||||||
<div class="range-tip">{{ item.range }}</div>
|
|
||||||
<el-button size="small">+</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 速度滑块 -->
|
|
||||||
<div class="speed-bar">
|
|
||||||
<span class="label">速度</span>
|
|
||||||
<el-slider v-model="speed" :min="1" :max="100" />
|
|
||||||
<div class="speed-num">
|
|
||||||
<el-button size="small">-</el-button>
|
|
||||||
<span>{{ speed }} %</span>
|
|
||||||
<el-button size="small">+</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 中间3D机械臂空白区域 -->
|
|
||||||
<div class="center-view">
|
|
||||||
<div class="robot-3d-box">
|
|
||||||
<!-- 此处后续接入Three.js/URDF机械臂模型,当前留白 -->
|
|
||||||
<div class="axis-line x-axis"></div>
|
|
||||||
<div class="axis-line y-axis"></div>
|
|
||||||
<div class="axis-line z-axis"></div>
|
|
||||||
<div class="empty-tip">机械臂3D可视化区域</div>
|
|
||||||
</div>
|
|
||||||
<!-- 底部功能按钮 -->
|
|
||||||
<div class="center-bottom-btns">
|
|
||||||
<el-button type="success" size="large">零力示教</el-button>
|
|
||||||
<el-button size="large">回到原点</el-button>
|
|
||||||
<el-button size="large">Z轴对齐</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧摇杆操作面板 -->
|
|
||||||
<div class="right-panel">
|
|
||||||
<!-- Z轴加减 -->
|
|
||||||
<div class="z-ctrl">
|
|
||||||
<el-button circle>+</el-button>
|
|
||||||
<el-button circle>Z</el-button>
|
|
||||||
<el-button circle>-</el-button>
|
|
||||||
</div>
|
|
||||||
<!-- XY方向摇杆 -->
|
|
||||||
<div class="joystick">
|
|
||||||
<div class="joy-top">+X</div>
|
|
||||||
<div class="joy-row">
|
|
||||||
<div class="joy-left">-Y</div>
|
|
||||||
<div class="joy-center"></div>
|
|
||||||
<div class="joy-right">+Y</div>
|
|
||||||
</div>
|
|
||||||
<div class="joy-bottom">-X</div>
|
|
||||||
</div>
|
|
||||||
<!-- RZ旋转 -->
|
|
||||||
<div class="rz-ctrl">
|
|
||||||
<el-button circle><-</el-button>
|
|
||||||
<el-button circle>RZ</el-button>
|
|
||||||
<el-button circle>>+</el-button>
|
|
||||||
</div>
|
|
||||||
<!-- RX RY摇杆 -->
|
|
||||||
<div class="joystick">
|
|
||||||
<div class="joy-top">+RX</div>
|
|
||||||
<div class="joy-row">
|
|
||||||
<div class="joy-left">-RY</div>
|
|
||||||
<div class="joy-center"></div>
|
|
||||||
<div class="joy-right">+RY</div>
|
|
||||||
</div>
|
|
||||||
<div class="joy-bottom">-RX</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
// 顶部控制
|
|
||||||
const coordMode = ref('TCP')
|
|
||||||
const baseCoord = ref('Base')
|
|
||||||
const useTool = ref(false)
|
|
||||||
const stepMode = ref('continuous')
|
|
||||||
|
|
||||||
// TCP坐标数据
|
|
||||||
const tcp = ref({
|
|
||||||
x: 46.691,
|
|
||||||
y: 385.321,
|
|
||||||
z: 141.877,
|
|
||||||
rx: -174.595,
|
|
||||||
ry: 4.212,
|
|
||||||
rz: -72.937
|
|
||||||
})
|
|
||||||
|
|
||||||
// 6关节数据
|
|
||||||
const jointList = ref([
|
|
||||||
{ name: 'J1', val: 84.605, range: '-360°~360°' },
|
|
||||||
{ name: 'J2', val: -7.826, range: '-135°~135°' },
|
|
||||||
{ name: 'J3', val: 136.306, range: '-153°~153°' },
|
|
||||||
{ name: 'J4', val: -6.773, range: '-360°~360°' },
|
|
||||||
{ name: 'J5', val: 30.087, range: '-180°~180°' },
|
|
||||||
{ name: 'J6', val: -16.567, range: '-360°~360°' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 速度
|
|
||||||
const speed = ref(56)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.robot-teach-container {
|
|
||||||
height: 100%;
|
|
||||||
background: $bg-deep;
|
|
||||||
color: $text;
|
|
||||||
font-family: $font-b;
|
|
||||||
overflow: hidden;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
// 主体三栏布局
|
|
||||||
.main-wrap {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 320px 1fr 320px;
|
|
||||||
gap: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 左侧面板
|
|
||||||
.left-panel {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: $bg-body;
|
|
||||||
|
|
||||||
.panel-card {
|
|
||||||
margin-top: 20px;
|
|
||||||
background: $bg-card;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 14px;
|
|
||||||
box-shadow: $shadow;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
gap: 10px;
|
|
||||||
&.dual-row {
|
|
||||||
.coord-item {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.coord-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
.key {
|
|
||||||
width: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
background: #2d323e;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.joint-card {
|
|
||||||
.joint-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
gap: 8px;
|
|
||||||
.joint-key {
|
|
||||||
width: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
:deep(.el-input) {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
background: #2d323e;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.btn-group {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
.range-tip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: $text-gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.speed-bar {
|
|
||||||
background: $bg-card;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 14px;
|
|
||||||
box-shadow: $shadow;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
.speed-num {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中间3D区域
|
|
||||||
.center-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
.robot-3d-box {
|
|
||||||
flex: 1;
|
|
||||||
background: $bg-card;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: $shadow;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.empty-tip {
|
|
||||||
color: $text-gray;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
// 坐标轴示意线
|
|
||||||
.axis-line {
|
|
||||||
position: absolute;
|
|
||||||
&.x-axis {
|
|
||||||
width: 200px;
|
|
||||||
height: 2px;
|
|
||||||
background: #f56c6c;
|
|
||||||
left: 50%;
|
|
||||||
top: 60%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
&.y-axis {
|
|
||||||
width: 200px;
|
|
||||||
height: 2px;
|
|
||||||
background: #67c23a;
|
|
||||||
left: 50%;
|
|
||||||
top: 60%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
&.z-axis {
|
|
||||||
width: 2px;
|
|
||||||
height: 220px;
|
|
||||||
background: #409eff;
|
|
||||||
left: 50%;
|
|
||||||
top: 10%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.center-bottom-btns {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右侧摇杆面板
|
|
||||||
.right-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.z-ctrl, .rz-ctrl {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joystick {
|
|
||||||
width: 140px;
|
|
||||||
height: 140px;
|
|
||||||
background: $bg-card;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 0 1px $shadow;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px;
|
|
||||||
.joy-top, .joy-bottom {
|
|
||||||
color: $primary;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.joy-row {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
.joy-left { color: #67c23a; }
|
|
||||||
.joy-right { color: #f56c6c; }
|
|
||||||
.joy-center {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #333842;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-btns {
|
|
||||||
margin-top: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// element-plus深色输入框覆盖
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
background-color: #2d323e;
|
|
||||||
box-shadow: $shadow;
|
|
||||||
.el-input__inner {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.el-slider__runway) {
|
|
||||||
background: #333842;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
403
src/views/arm/MechanicalArm.vue
Normal file
403
src/views/arm/MechanicalArm.vue
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
<template>
|
||||||
|
<div class="robot-teach-container">
|
||||||
|
<!-- 主体内容区 -->
|
||||||
|
<div class="main-wrap">
|
||||||
|
<!-- 左侧数据面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<!-- TCP 笛卡尔坐标 -->
|
||||||
|
<div class="panel-card">
|
||||||
|
<div class="row dual-row">
|
||||||
|
<div class="coord-item">
|
||||||
|
<span class="key">X</span>
|
||||||
|
<el-input v-model.number="tcp.x" suffix="mm" />
|
||||||
|
</div>
|
||||||
|
<div class="coord-item">
|
||||||
|
<span class="key">RX</span>
|
||||||
|
<el-input v-model.number="tcp.rx" suffix="°" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row dual-row">
|
||||||
|
<div class="coord-item">
|
||||||
|
<span class="key">Y</span>
|
||||||
|
<el-input v-model.number="tcp.y" suffix="mm" />
|
||||||
|
</div>
|
||||||
|
<div class="coord-item">
|
||||||
|
<span class="key">RY</span>
|
||||||
|
<el-input v-model.number="tcp.ry" suffix="°" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row dual-row">
|
||||||
|
<div class="coord-item">
|
||||||
|
<span class="key">Z</span>
|
||||||
|
<el-input v-model.number="tcp.z" suffix="mm" />
|
||||||
|
</div>
|
||||||
|
<div class="coord-item">
|
||||||
|
<span class="key">RZ</span>
|
||||||
|
<el-input v-model.number="tcp.rz" suffix="°" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="joint-list">
|
||||||
|
<div class="title">关节控制</div>
|
||||||
|
<div v-for="joint in jointControls" :key="joint.name" class="joint-item">
|
||||||
|
<label :for="joint.name">{{ joint.name }}</label>
|
||||||
|
<el-slider v-model="joint.value" :min="joint.min" :max="joint.max" :step="0.01" @input="updateJoint(joint.name, joint.value)"/>
|
||||||
|
<span class="angle">{{ (joint.value * 180 / Math.PI).toFixed(1) }}°</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="speed-container">
|
||||||
|
<div class="label">速度:</div>
|
||||||
|
<el-slider v-model="speed" :min="1" :max="100" />
|
||||||
|
<div class="proportion">{{ speed }} %</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间3D机械臂空白区域 -->
|
||||||
|
<div class="center-view">
|
||||||
|
<UrdfView ref="urdfViewerRef" />
|
||||||
|
<!-- 底部功能按钮 -->
|
||||||
|
<!-- <div class="center-bottom-btns">
|
||||||
|
<el-button type="success" size="large">零力示教</el-button>
|
||||||
|
<el-button size="large">回到原点</el-button>
|
||||||
|
<el-button size="large">Z轴对齐</el-button>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧摇杆操作面板 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="controller">
|
||||||
|
<div class="title">末端控制</div>
|
||||||
|
<div class="axis-group">
|
||||||
|
<!-- X Axis -->
|
||||||
|
<div class="axis-row x">
|
||||||
|
<div class="axis-label x">X</div>
|
||||||
|
<div class="axis-slider-wrap">
|
||||||
|
<div class="axis-name">前后平移</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" circle>
|
||||||
|
<el-icon size="24"><Remove /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" circle >
|
||||||
|
<el-icon size="24"><CirclePlus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Y Axis -->
|
||||||
|
<div class="axis-row y">
|
||||||
|
<div class="axis-label y">Y</div>
|
||||||
|
<div class="axis-slider-wrap">
|
||||||
|
<div class="axis-name">左右平移</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" circle>
|
||||||
|
<el-icon size="24"><Remove /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" circle >
|
||||||
|
<el-icon size="24"><CirclePlus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Z Axis -->
|
||||||
|
<div class="axis-row z">
|
||||||
|
<div class="axis-label z">Z</div>
|
||||||
|
<div class="axis-slider-wrap">
|
||||||
|
<div class="axis-name">上下平移</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" circle>
|
||||||
|
<el-icon size="24"><Remove /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" circle >
|
||||||
|
<el-icon size="24"><CirclePlus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controller">
|
||||||
|
<div class="title">姿态控制</div>
|
||||||
|
<div class="axis-group">
|
||||||
|
<!-- X Axis -->
|
||||||
|
<div class="axis-row x">
|
||||||
|
<div class="axis-label x">X</div>
|
||||||
|
<div class="axis-slider-wrap">
|
||||||
|
<div class="axis-name">前后平移</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" circle>
|
||||||
|
<el-icon size="24"><Remove /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" circle >
|
||||||
|
<el-icon size="24"><CirclePlus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Y Axis -->
|
||||||
|
<div class="axis-row y">
|
||||||
|
<div class="axis-label y">Y</div>
|
||||||
|
<div class="axis-slider-wrap">
|
||||||
|
<div class="axis-name">左右平移</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" circle>
|
||||||
|
<el-icon size="24"><Remove /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" circle >
|
||||||
|
<el-icon size="24"><CirclePlus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Z Axis -->
|
||||||
|
<div class="axis-row z">
|
||||||
|
<div class="axis-label z">Z</div>
|
||||||
|
<div class="axis-slider-wrap">
|
||||||
|
<div class="axis-name">上下平移</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" circle>
|
||||||
|
<el-icon size="24"><Remove /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" circle >
|
||||||
|
<el-icon size="24"><CirclePlus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
|
import UrdfView from './UrdfView.vue'
|
||||||
|
import { Remove, CirclePlus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const urdfViewerRef = ref()
|
||||||
|
const jointControls = ref()
|
||||||
|
|
||||||
|
// TCP坐标数据
|
||||||
|
const tcp = ref({
|
||||||
|
x: 46.691,
|
||||||
|
y: 385.321,
|
||||||
|
z: 141.877,
|
||||||
|
rx: -174.595,
|
||||||
|
ry: 4.212,
|
||||||
|
rz: -72.937
|
||||||
|
})
|
||||||
|
|
||||||
|
// 速度
|
||||||
|
const speed = ref(56)
|
||||||
|
|
||||||
|
const updateJoint = (jointName, angle) => {
|
||||||
|
urdfViewerRef.value.updateJoint(jointName, angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
jointControls.value = urdfViewerRef.value.jointControls
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.robot-teach-container {
|
||||||
|
height: 100%;
|
||||||
|
background: $bg-deep;
|
||||||
|
color: $text;
|
||||||
|
font-family: $font-b;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
// 主体三栏布局
|
||||||
|
.main-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// 左侧面板
|
||||||
|
.left-panel {
|
||||||
|
width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
// background: #030729;
|
||||||
|
background: $bg-body;
|
||||||
|
border-right: 1px solid rgba(0, 195, 255, 0.08);
|
||||||
|
|
||||||
|
.panel-card {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joint-list {
|
||||||
|
margin-top: 24px;
|
||||||
|
// background: #161933;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.joint-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(0, 195, 255, 0.08);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #0a0b10;
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 120px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range] {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.angle {
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.label,
|
||||||
|
.proportion {
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中间3D区域
|
||||||
|
.center-view {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧摇杆面板
|
||||||
|
.right-panel {
|
||||||
|
width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
// background: #030729;
|
||||||
|
background: $bg-body;
|
||||||
|
border-left: 1px solid rgba(0, 195, 255, 0.08);
|
||||||
|
|
||||||
|
.controller {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.axis-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #0a0b10;
|
||||||
|
border: 1px solid #1c2a52;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #2a3f7a;
|
||||||
|
background: #3b82f608;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis-label {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x {
|
||||||
|
background: #f8717126;
|
||||||
|
color: #f87171;
|
||||||
|
border: 1px solid #f871714d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y {
|
||||||
|
background: #34d39926;
|
||||||
|
color: #34d399;
|
||||||
|
border: 1px solid #34d3994d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z {
|
||||||
|
background: #38bdf826;
|
||||||
|
color: #38bdf8;
|
||||||
|
border: 1px solid #38bdf84d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.axis-slider-wrap {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// element-plus深色输入框覆盖
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background-color: #2d323e;
|
||||||
|
box-shadow: $shadow;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-slider__runway) {
|
||||||
|
background: #333842;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
src/views/arm/UrdfView.vue
Normal file
237
src/views/arm/UrdfView.vue
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div class="elfin-viewer">
|
||||||
|
<div ref="viewerContainer" class="viewer-container">
|
||||||
|
<!-- Three.js 渲染 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick, reactive } from 'vue';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
||||||
|
|
||||||
|
import { XacroAdapter } from '@/adapters/XacroAdapter.js'
|
||||||
|
|
||||||
|
const viewerContainer = ref(null);
|
||||||
|
|
||||||
|
let scene, camera, renderer, controls;
|
||||||
|
|
||||||
|
const animationId = ref(null);
|
||||||
|
const jointControls = reactive([]);
|
||||||
|
|
||||||
|
// ============ 场景初始化 ============
|
||||||
|
const initScene = () => {
|
||||||
|
const container = viewerContainer.value;
|
||||||
|
const width = container.clientWidth || 800;
|
||||||
|
const height = container.clientHeight || 600;
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color('#060a12');
|
||||||
|
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||||
|
camera.position.set(2, 2, 3);
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1.2;
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.1;
|
||||||
|
controls.target.set(0, 0.5, 0);
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
const ambient = new THREE.AmbientLight(0x404060, 0.6);
|
||||||
|
scene.add(ambient);
|
||||||
|
const main = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
main.position.set(5, 10, 7);
|
||||||
|
main.castShadow = true;
|
||||||
|
scene.add(main);
|
||||||
|
const fill = new THREE.DirectionalLight(0x8888ff, 0.4);
|
||||||
|
fill.position.set(-5, 0, 5);
|
||||||
|
scene.add(fill);
|
||||||
|
const rim = new THREE.DirectionalLight(0xffffff, 0.3);
|
||||||
|
rim.position.set(0, -5, 5);
|
||||||
|
scene.add(rim);
|
||||||
|
// const grid = new THREE.GridHelper(3, 20, 0x444466, 0x333355);
|
||||||
|
// grid.position.y = -0.5;
|
||||||
|
// scene.add(grid);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 关节控制(无需修改) ============
|
||||||
|
const updateJoint = (jointName, angle) => {
|
||||||
|
if (!robotInstance) return;
|
||||||
|
|
||||||
|
const joint = robotInstance.joints[jointName];
|
||||||
|
|
||||||
|
if (!joint) {
|
||||||
|
console.warn(`未找到关节: ${jointName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 使用 setJointValue 触发变换矩阵更新
|
||||||
|
if (joint._jointType === 'revolute' || joint._jointType === 'continuous') {
|
||||||
|
joint.setJointValue(angle);
|
||||||
|
} else if (joint._jointType === 'prismatic') {
|
||||||
|
joint.setJointValue(angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步滑块显示值
|
||||||
|
const control = jointControls.find(c => c.name === jointName);
|
||||||
|
if (control) control.value = angle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetJoints = () => {
|
||||||
|
for (const joint of jointControls) {
|
||||||
|
joint.value = 0;
|
||||||
|
updateJoint(joint.name, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileMap = new Map()
|
||||||
|
const loadFiles = async () => {
|
||||||
|
const fileNames = [
|
||||||
|
'elfin10.urdf.xacro',
|
||||||
|
'elfin10_gazebo.xacro',
|
||||||
|
'elfin10_transmission.xacro',
|
||||||
|
'meshes/elfin10/elfin_base.STL',
|
||||||
|
'meshes/elfin10/elfin_end_link.STL',
|
||||||
|
'meshes/elfin10/elfin_link1.STL',
|
||||||
|
'meshes/elfin10/elfin_link2.STL',
|
||||||
|
'meshes/elfin10/elfin_link3.STL',
|
||||||
|
'meshes/elfin10/elfin_link4.STL',
|
||||||
|
'meshes/elfin10/elfin_link5.STL',
|
||||||
|
'meshes/elfin10/elfin_link6.STL'
|
||||||
|
]
|
||||||
|
const fetchPromises = fileNames.map(async (name) => {
|
||||||
|
const url = `/inspection/elfin10/${name}`
|
||||||
|
const response = await fetch(url)
|
||||||
|
const blob = await response.blob()
|
||||||
|
return new File([blob], name, { type: blob.type })
|
||||||
|
})
|
||||||
|
const fileList = await Promise.all(fetchPromises)
|
||||||
|
fileList.forEach(item => {
|
||||||
|
fileMap.set('elfin10/' + item.name, item);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentModel = ref()
|
||||||
|
let robotInstance = null; // 保存 URDFRobot 对象
|
||||||
|
|
||||||
|
const loadRobot = async () => {
|
||||||
|
const xacroUrl = `/inspection/elfin10/elfin10.urdf.xacro`;
|
||||||
|
|
||||||
|
const response = await fetch(xacroUrl);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const xacroContent = await response.text();
|
||||||
|
const file = fileMap.get('elfin10/elfin10.urdf.xacro')
|
||||||
|
const model = await XacroAdapter.parse(xacroContent, file.name, fileMap, file);
|
||||||
|
|
||||||
|
const robot = model.threeObject; // 这就是 URDFRobot
|
||||||
|
|
||||||
|
|
||||||
|
// 将机器人添加到场景
|
||||||
|
scene.add(robot); // ⚠️ 注意:是 robot.scene,不是 robot
|
||||||
|
robot.rotation.x = -Math.PI / 2;
|
||||||
|
currentModel.value = model
|
||||||
|
|
||||||
|
robotInstance = robot;
|
||||||
|
|
||||||
|
// 获取关节条目数组
|
||||||
|
let jointEntries = [];
|
||||||
|
if (robot.joints instanceof Map) {
|
||||||
|
jointEntries = [...robot.joints.entries()];
|
||||||
|
} else if (typeof robot.joints === 'object' && robot.joints !== null) {
|
||||||
|
jointEntries = Object.entries(robot.joints);
|
||||||
|
} else if (Array.isArray(robot.joints)) {
|
||||||
|
jointEntries = robot.joints.map((j, i) => [i, j]); // 索引作为名称
|
||||||
|
} else {
|
||||||
|
console.error('未知的 joints 结构:', robot.joints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充关节控制列表
|
||||||
|
for (const [name, joint] of jointEntries) {
|
||||||
|
if (joint._jointType === 'fixed') continue;
|
||||||
|
const limits = joint.limits || {};
|
||||||
|
let lower = limits.lower !== undefined ? limits.lower : -Math.PI;
|
||||||
|
let upper = limits.upper !== undefined ? limits.upper : Math.PI;
|
||||||
|
// 如果是连续关节,范围设为 -PI ~ PI
|
||||||
|
if (joint._jointType === 'continuous') {
|
||||||
|
lower = -Math.PI;
|
||||||
|
upper = Math.PI;
|
||||||
|
}
|
||||||
|
// 初始角度一般为 0
|
||||||
|
jointControls.push({
|
||||||
|
name,
|
||||||
|
min: lower,
|
||||||
|
max: upper,
|
||||||
|
value: 0, // 因为 joint.angle 初始为 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 动画循环 ============
|
||||||
|
const animate = () => {
|
||||||
|
animationId.value = requestAnimationFrame(animate);
|
||||||
|
controls?.update();
|
||||||
|
renderer?.render(scene, camera);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 生命周期 ============
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
await loadFiles()
|
||||||
|
initScene();
|
||||||
|
await loadRobot();
|
||||||
|
animate();
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (animationId.value) cancelAnimationFrame(animationId.value);
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
renderer?.dispose();
|
||||||
|
controls?.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
const container = viewerContainer.value;
|
||||||
|
if (!container || !camera || !renderer) return;
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ updateJoint, resetJoints, jointControls });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.elfin-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0d0d1a;
|
||||||
|
|
||||||
|
.viewer-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -216,7 +216,7 @@ onBeforeUnmount(() => { })
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: $bg-deep;
|
background: #030729;
|
||||||
color: $text;
|
color: $text;
|
||||||
font-family: $font-b;
|
font-family: $font-b;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -241,7 +241,7 @@ onBeforeUnmount(() => { })
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background: $bg-card;
|
background: #030729;
|
||||||
border-bottom: 1px solid $border;
|
border-bottom: 1px solid $border;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user