news 2026/10/13 1:17:37

ROS SLAM仿真入门:Gazebo建图与导航实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ROS SLAM仿真入门:Gazebo建图与导航实战指南

1. 从零搭建SLAM仿真环境:为什么我不建议你直接上真机

刚接触ROS机器人开发那会儿,我跟很多人的想法一样——恨不得立刻买一台带激光雷达的移动底盘,跑通建图和导航,拍个视频发朋友圈。结果呢?真机上的轮式里程计标定不准、雷达安装角度有偏差、电机响应延迟、地面打滑……每一个问题都能让你卡上一周。后来一位做移动机器人多年的前辈点醒了我:SLAM的入门阶段,仿真环境才是最高效的学习工具。你可以在几分钟内重置整个世界,随意调整传感器噪声、地图复杂度、机器人运动模型,而不用去拧螺丝、换电池、担心撞坏设备。

这个项目标题“ROS的SLAM学习:地图扫描与导航仿真”,核心就是围绕ROS(Robot Operating System)这个机器人软件框架,利用Gazebo仿真器和RViz可视化工具,搭建一个完整的SLAM(Simultaneous Localization and Mapping,同步定位与建图)流程。具体来说,我们要让一个虚拟机器人在虚拟环境中,通过激光雷达扫描周围环境,实时构建二维栅格地图,然后基于这张地图实现自主导航——你给它一个目标点,它自己规划路径、避开障碍、走到终点。

这套内容适合谁?如果你刚学完ROS的基础概念(节点、话题、服务、TF变换),想找一个完整的项目把知识串起来,那这个仿真项目就是为你准备的。如果你已经会跑一些简单的示例,但对SLAM的完整数据流(从传感器数据到地图到路径规划)还不太清楚,这篇文章也能帮你把链路打通。甚至如果你是从其他机器人平台转过来的,想快速了解ROS生态下的SLAM工具链,也可以按这个流程走一遍。

我下面要讲的,不是官方文档的复述,而是我自己反复搭建、踩坑、优化之后总结出来的一套可复现方案。包括环境选型、参数配置、启动流程、常见报错处理,以及一些让仿真跑得更顺畅的实操技巧。

2. 环境选型与整体方案设计:为什么是这套组合

2.1 ROS版本与仿真工具的选择逻辑

ROS的版本迭代很快,不同版本对SLAM相关包的支持程度差异很大。目前主流的选择集中在ROS Noetic(对应Ubuntu 20.04)和ROS2 Humble(对应Ubuntu 22.04)两个版本上。我个人的建议是:如果你刚开始学SLAM仿真,优先选ROS Noetic + Gazebo Classic这套组合。原因很直接——网上能找到的SLAM教程、开源包、社区问答,绝大多数都是基于ROS1生态的。gmapping、cartographer、amcl、move_base这些经典包在ROS1下的文档最全,遇到问题搜索到的解决方案也最多。

ROS2虽然架构更先进,但SLAM相关的仿真教程相对分散,而且Gazebo在新版本中的集成方式有变化(Gazebo Classic和Ignition Gazebo并存),对新手来说容易在环境配置上浪费大量时间。当然,如果你所在团队已经全面转向ROS2,那就直接上Humble + Gazebo Sim,只是需要做好心理准备:有些包的配置方式和ROS1不一样,需要重新适应。

仿真器方面,Gazebo是ROS生态中集成度最高的选择。它提供了物理引擎(ODE、Bullet等)、传感器仿真(激光雷达、深度相机、IMU)、以及丰富的模型库。相比之下,CoppeliaSim(原V-REP)虽然功能强大,但与ROS的桥接配置更繁琐;Webots近几年对ROS2的支持不错,但ROS1下的资料偏少。所以Gazebo Classic + ROS Noetic是我认为入门SLAM仿真最稳妥的起点。

2.2 SLAM算法包的选择:gmapping还是cartographer

在ROS1下做二维激光SLAM,最常用的两个包是gmapping和cartographer。gmapping基于粒子滤波(Rao-Blackwellized Particle Filter),是早期ROS社区最流行的SLAM方案,优点是配置简单、计算量小、在小场景下表现稳定;缺点是建图精度受粒子数影响大,回环检测能力弱,大场景下容易漂移。

cartographer是Google开源的SLAM方案,基于图优化(Graph-based SLAM),支持回环检测,建图精度更高,适合大场景。但它的配置相对复杂,需要调参的地方更多,而且对计算资源的要求也更高。

对于这个仿真项目,我的建议是先用gmapping跑通整个流程,理解SLAM的基本数据流和坐标系关系,然后再切换到cartographer对比效果。这样你能直观感受到两种算法在同一环境下的差异,对理解SLAM的核心问题(比如回环、漂移、粒子退化)非常有帮助。

2.3 导航栈的组成:move_base与代价地图

导航部分的核心是move_base节点,它是ROS导航栈的调度中心。move_base内部集成了全局规划器(global planner)和局部规划器(local planner),分别负责长距离路径规划和实时避障。全局规划器常用的有navfn(基于Dijkstra或A*)和global_planner;局部规划器常用的是base_local_planner(DWA算法)和teb_local_planner(时间弹性带算法)。

代价地图(costmap)是导航栈的另一个核心概念。它把环境信息转换成一张“代价地图”,每个栅格有一个代价值,表示机器人通过该位置的“代价”。代价地图分为全局代价地图(global costmap)和局部代价地图(local costmap),分别服务于全局规划和局部避障。理解代价地图的层结构(static layer、obstacle layer、inflation layer)是调好导航参数的关键。

整体方案的数据流是这样的:Gazebo中的虚拟激光雷达发布/scan话题,gmapping订阅/scan和TF信息,输出/map话题和map到odom的TF变换;move_base订阅/map、/scan、TF和里程计信息,输出/cmd_vel速度指令控制机器人运动。RViz作为可视化工具,把地图、机器人模型、传感器数据、路径规划结果都显示出来。

3. 仿真环境搭建与机器人模型配置

3.1 创建工作空间与安装依赖包

第一步是建立规范的ROS工作空间。我习惯在~/catkin_ws下组织代码,所有自己写的包放在src目录里。安装依赖的时候,有几个包是必须的:

sudo apt install ros-noetic-gazebo-ros-pkg ros-noetic-gmapping ros-noetic-map-server ros-noetic-amcl ros-noetic-move-base ros-noetic-teb-local-planner ros-noetic-joint-state-publisher ros-noetic-robot-state-publisher ros-noetic-xacro

这里解释一下每个包的作用。gazebo_ros_pkgs是Gazebo与ROS的桥接包,提供了在Gazebo中加载ROS控制器和传感器的插件;gmapping是SLAM算法包;map-server负责地图的保存和加载;amcl是蒙特卡洛定位算法,用于已知地图下的定位;move-base是导航调度中心;teb-local-planner是一个比默认DWA更好用的局部规划器;joint-state-publisher和robot-state-publisher负责发布机器人关节状态和TF变换;xacro是URDF的宏语言,用来简化机器人模型的描述。

注意:安装之前先确认你的ROS版本和Ubuntu版本匹配。ROS Noetic只支持Ubuntu 20.04,如果你用的是22.04,需要装ROS2 Humble,包名和管理工具都不一样。

3.2 用URDF/XACRO描述机器人模型

机器人模型是整个仿真的基础。我们需要用URDF(Unified Robot Description Format)描述机器人的几何形状、惯性矩阵、碰撞属性、关节关系,以及传感器的安装位置。直接用URDF写会比较冗长,所以通常用XACRO宏来简化。

一个典型的差速驱动移动机器人模型包含以下部件:底盘(base_link)、左右驱动轮(left_wheel、right_wheel)、万向轮(caster_wheel)、激光雷达(laser_link)。底盘用长方体或圆柱体表示,驱动轮用圆柱体,激光雷达用一个小圆柱体表示安装位置。

关键参数包括:底盘尺寸(长宽高)、轮子半径和宽度、轮距(两个驱动轮之间的距离)、激光雷达的安装高度和扫描范围。这些参数直接影响仿真中的运动学和传感器数据,必须和后续的控制器配置保持一致。

<!-- 示例:激光雷达的XACRO片段 --> <xacro:macro name="laser" params="prefix"> <link name="${prefix}laser_link"> <visual> <geometry> <cylinder radius="0.05" length="0.06"/> </geometry> <origin xyz="0 0 0" rpy="0 0 0"/> </visual> <collision> <geometry> <cylinder radius="0.05" length="0.06"/> </geometry> </collision> <inertial> <mass value="0.1"/> <inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0001"/> </inertial> </link> <joint name="${prefix}laser_joint" type="fixed"> <parent link="base_link"/> <child link="${prefix}laser_link"/> <origin xyz="0.1 0 0.15" rpy="0 0 0"/> </joint> </xacro:macro>

惯性矩阵这一块很多人会忽略,但它在Gazebo仿真中非常重要。如果惯性矩阵设置不合理,机器人可能会抖动、翻倒,或者激光雷达的数据出现异常噪声。对于简单的长方体底盘,惯性矩阵可以用公式计算:( I_{xx} = \frac{1}{12}m(h^2+d^2) ),( I_{yy} = \frac{1}{12}m(w^2+d^2) ),( I_{zz} = \frac{1}{12}m(w^2+h^2) ),其中m是质量,w、h、d分别是长宽高。

3.3 Gazebo世界文件与传感器插件配置

Gazebo的世界文件(.world)定义了仿真环境中的地面、光照、障碍物、以及机器人模型。我们可以自己搭建一个简单的室内环境,比如几个房间、走廊、一些家具形状的障碍物。Gazebo自带的模型库里有桌子、椅子、箱子等基本模型,直接拖进来就行。

激光雷达的仿真需要在URDF中加载Gazebo插件。常用的插件是libgazebo_ros_laser.so,配置参数包括:

<gazebo reference="laser_link"> <sensor type="ray" name="laser_sensor"> <pose>0 0 0 0 0 0</pose> <visualize>true</visualize> <update_rate>10</update_rate> <ray> <scan> <horizontal> <samples>360</samples> <resolution>1</resolution> <min_angle>-3.14159</min_angle> <max_angle>3.14159</max_angle> </horizontal> </scan> <range> <min>0.12</min> <max>10.0</max> </range> <noise> <type>gaussian</type> <mean>0.0</mean> <stddev>0.01</stddev> </noise> </ray> <plugin name="laser_controller" filename="libgazebo_ros_laser.so"> <topicName>/scan</topicName> <frameName>laser_link</frameName> </plugin> </sensor> </gazebo>

这里的参数需要仔细调。update_rate是雷达的扫描频率,常见的是10Hz;samples是一圈扫描的点数,360个点对应1度分辨率;min_angle和max_angle定义了扫描范围,-π到π表示360度全向扫描;range的min和max是测距范围,我设的是0.12米到10米,模拟一个常见的二维激光雷达。noise部分加了高斯噪声,标准差0.01米,让仿真更接近真实传感器的表现。

实操心得:如果你发现建图时地图边缘有毛刺或者重影,先检查激光雷达的噪声参数。噪声太大建图会糊,噪声太小又太理想化,0.01米左右是一个比较平衡的值。

4. SLAM建图流程与关键参数调优

4.1 启动顺序与TF树的关系

ROS中启动仿真和SLAM节点是有顺序讲究的。正确的启动顺序是:先启动Gazebo仿真环境(加载机器人和世界),再启动机器人状态发布器(robot_state_publisher),然后启动SLAM节点(gmapping),最后启动RViz。如果顺序反了,可能会出现TF变换缺失、话题没有数据等问题。

TF树是ROS中管理坐标系变换的核心机制。在这个仿真中,TF树的结构是:map→odom→base_link→laser_link。其中map到odom的变换由gmapping发布(表示SLAM算法估计的机器人位姿修正),odom到base_link的变换由里程计发布(表示轮式编码器推算的位姿),base_link到laser_link的变换由robot_state_publisher根据URDF发布(表示雷达的安装位置)。

理解这个TF树非常关键。gmapping的核心工作就是通过激光扫描匹配,不断修正map到odom的变换,从而消除里程计的累积误差。如果你在RViz中看到激光扫描和地图对不上,或者机器人模型跳来跳去,多半是TF树出了问题。

4.2 gmapping参数配置与调优

gmapping的配置文件通常放在launch目录下,作为一个.yaml文件加载。核心参数包括:

参数名含义推荐值调优说明
particles粒子数30-80太少建图不准,太多计算量大
delta地图分辨率0.05单位米,0.05表示每个栅格5cm
xmin/ymin/xmax/ymax地图初始范围-50/-50/50/50根据环境大小调整
llsamplerange似然采样范围0.01激光匹配的搜索范围
lasamplerange角度采样范围0.005角度搜索步长
linearUpdate线性更新阈值0.1机器人移动多少米更新一次
angularUpdate角度更新阈值0.1机器人转动多少弧度更新一次
temporalUpdate时间更新阈值1.0静止时多久更新一次

particles是最关键的参数之一。粒子数越多,SLAM的精度越高,但计算量也越大。在仿真环境中,因为计算资源相对充裕,我一般设50个粒子。如果发现建图过程中机器人位姿跳变明显,可以适当增加到80。delta决定了地图的精细程度,0.05米是一个比较通用的值,如果环境很大(比如整个楼层),可以放宽到0.1米以节省内存。

linearUpdate和angularUpdate控制着地图更新的频率。设得太小,计算频繁,CPU占用高;设得太大,地图更新滞后,快速运动时容易丢失特征。0.1米和0.1弧度的组合在实际测试中比较平衡。

4.3 建图实操:从启动到保存地图

完整的建图流程是这样的。首先启动Gazebo仿真:

roslaunch my_robot_gazebo my_robot_world.launch

这个launch文件里包含了加载世界文件、生成机器人模型、启动Gazebo ROS插件等步骤。等Gazebo界面出来,机器人出现在环境中后,新开一个终端启动gmapping:

roslaunch my_robot_slam gmapping.launch

再新开一个终端启动RViz:

roslaunch my_robot_slam slam_rviz.launch

在RViz中,你需要添加几个显示项:LaserScan(订阅/scan)、Map(订阅/map)、RobotModel、TF。然后新开一个终端,用键盘遥控机器人移动:

rosrun teleop_twist_keyboard teleop_twist_keyboard.py

控制机器人慢慢走遍整个环境。注意不要走太快,也不要急转弯,让gmapping有足够的时间处理激光数据。你会看到RViz中的地图逐渐扩展,从一团模糊的点云变成清晰的栅格地图。

建图完成后,用map_server保存地图:

rosrun map_server map_saver -f ~/my_map

这会生成两个文件:my_map.pgm(地图图像)和my_map.yaml(地图元数据)。yaml文件里记录了地图的分辨率、原点坐标、阈值等信息,后续导航时会用到。

注意事项:保存地图前,确保机器人已经走遍了所有需要建图的区域,并且回到了起点附近。如果地图有明显的重影或错位,说明SLAM过程中出现了较大的漂移,建议重新建图,或者调整gmapping参数后重试。

5. 导航仿真:从代价地图到路径规划

5.1 导航栈的启动与配置

导航仿真需要加载之前保存的地图,然后启动amcl定位和move_base导航。启动文件通常包含以下内容:加载地图到map_server、启动amcl节点、启动move_base节点、启动RViz。

amcl的参数配置也很关键。min_particles和max_particles控制粒子数的范围,一般设100到5000;laser_model_type选择激光模型,常用likelihood_field;odom_alpha系列参数描述里程计噪声,需要根据实际仿真中的里程计精度调整。在Gazebo仿真中,里程计通常比较理想,所以这些噪声参数可以设小一点。

move_base的配置分为三个文件:global_costmap_params.yaml、local_costmap_params.yaml、base_local_planner_params.yaml。全局代价地图的global_frame设为map,robot_base_frame设为base_link,update_frequency一般设1-5Hz;局部代价地图的global_frame设为odom,update_frequency设5-10Hz,rolling_window设为true表示局部地图跟随机器人移动。

5.2 代价地图层结构与膨胀半径的计算

代价地图的层结构是导航调参的核心。以全局代价地图为例,通常包含三层:

  • static layer:直接来自map_server的静态地图,表示已知的障碍物。
  • obstacle layer:来自激光雷达的实时障碍物信息,用于检测动态障碍物。
  • inflation layer:在障碍物周围生成一个“膨胀”区域,表示机器人中心不能进入的范围。

膨胀半径(inflation_radius)的计算是很多人困惑的地方。它应该等于机器人的内切圆半径加上一个安全余量。比如一个长0.5米、宽0.4米的机器人,内切圆半径是0.2米(宽度的一半),加上0.1米的安全余量,膨胀半径就是0.3米。如果设得太小,机器人会贴着障碍物走,容易碰撞;设得太大,狭窄通道可能无法通过。

cost_scaling_factor控制代价值随距离衰减的速度。值越大,代价值衰减越快,机器人越倾向于远离障碍物;值越小,机器人越愿意靠近障碍物走。一般设3-10之间,我常用5。

5.3 全局与局部路径规划器的选择与调参

全局规划器我推荐用global_planner(比navfn更灵活),它可以配置使用A或Dijkstra算法。在global_planner的参数中,use_dijkstra设为false表示用A,use_grid_path设为true表示路径沿着栅格走,allow_unknown设为true允许在未知区域规划。

局部规划器我强烈推荐teb_local_planner,它比默认的DWA在狭窄环境和动态避障中表现更好。TEB的核心参数包括:max_vel_x(最大前进速度)、max_vel_theta(最大角速度)、acc_lim_x(加速度限制)、min_turning_radius(最小转弯半径)、weight_kinematics_forward_drive(鼓励前进的权重)。在仿真中,我一般设max_vel_x为0.5m/s,max_vel_theta为1.0rad/s,这样机器人运动比较平稳,不会因为速度太快导致定位丢失。

实操心得:TEB的参数比较多,新手容易调晕。我的建议是先保证机器人能走起来,用默认参数跑一遍,然后根据实际表现逐步调整。如果机器人原地打转,检查weight_kinematics_forward_drive是不是太小;如果机器人走得太慢,检查max_vel_x和acc_lim_x;如果机器人撞障碍物,检查膨胀半径和weight_obstacle。

5.4 在RViz中设置目标点并观察导航过程

导航启动后,在RViz中使用“2D Nav Goal”工具点击地图上的目标位置,move_base就会开始规划路径。你会看到RViz中显示出一条全局路径(通常是绿色或蓝色),以及局部规划器生成的局部轨迹(通常是红色)。机器人会沿着路径移动,同时局部规划器实时调整轨迹以避开障碍物。

观察导航过程时,重点关注几个指标:机器人是否平滑地跟随路径、是否在障碍物附近减速或绕行、到达目标点后是否准确停止。如果机器人出现震荡(左右摇摆)、卡住不动、或者绕远路,就需要调整对应的参数。

6. 常见问题与排查技巧实录

6.1 建图与定位类问题

问题一:RViz中激光扫描数据不显示,或者显示为一条直线。

这种情况通常是激光雷达的插件配置有问题。检查URDF中激光雷达的frameName是否和TF树中的坐标系名称一致;检查Gazebo插件是否正确加载(可以在终端中看有没有报错信息);检查/scan话题是否有数据(rostopic echo /scan)。

问题二:建图时地图重影严重,墙壁变成双层。

这是SLAM漂移的典型表现。原因可能是机器人运动太快、激光匹配参数不合适、或者里程计噪声太大。解决方法:降低机器人运动速度;增大particles粒子数;调整llsamplerange和lasamplerange参数;检查里程计标定是否准确。

问题三:导航时机器人定位丢失,在地图上跳来跳去。

amcl定位丢失通常是因为初始位姿不确定或者激光数据和地图匹配度低。解决方法:在RViz中用“2D Pose Estimate”工具手动给出初始位姿;检查amcl的laser_model_type和laser_likelihood_max_dist参数;确认地图和实际环境一致。

6.2 导航与路径规划类问题

问题四:机器人规划不出路径,提示“Failed to get robot pose”。

这通常是TF树的问题。检查map到odom到base_link的TF变换是否完整;检查amcl是否正常发布map到odom的变换;检查move_base的global_frame和robot_base_frame参数是否和TF树一致。

问题五:机器人贴着障碍物走,甚至撞上去。

检查膨胀半径是否设得太小;检查代价地图的obstacle_layer是否正常订阅了激光数据;检查局部规划器的weight_obstacle参数是否足够大。

问题六:机器人到达目标点附近后反复调整,无法准确停止。

这是局部规划器的收敛问题。可以增大xy_goal_tolerance和yaw_goal_tolerance(允许的到达误差);调整TEB的weight_kinematics_forward_drive和weight_kinematics_nh参数;检查min_turning_radius是否和机器人实际转弯能力匹配。

6.3 仿真性能与稳定性问题

问题七:Gazebo仿真运行卡顿,实时率低于1.0。

Gazebo的实时率(real-time factor)低于1表示仿真运行比真实时间慢。解决方法:降低激光雷达的update_rate和samples;减少环境中的障碍物数量;关闭Gazebo的阴影和高质量渲染;在gazebo启动参数中设置--verbose查看性能瓶颈。

问题八:机器人模型在Gazebo中抖动或翻倒。

检查惯性矩阵是否设置合理;检查驱动轮的摩擦系数(mu1和mu2)是否足够;检查万向轮的碰撞属性是否和底盘重叠导致穿模。

问题现象可能原因排查方法解决方案
激光数据不显示插件未加载/话题名错误rostopic list检查话题修正URDF插件配置
地图重影SLAM漂移降低速度/增加粒子数调参或重新建图
定位跳变amcl匹配失败检查初始位姿手动给出初始位姿
规划失败TF树不完整rosrun tf view_frames补全TF变换
撞障碍物膨胀半径太小检查costmap参数增大inflation_radius
仿真卡顿计算量过大查看CPU占用降低雷达频率/简化环境

7. 从仿真到真机的经验衔接

仿真跑通之后,很多人会迫不及待地想上真机。但我要泼一盆冷水:仿真和真机之间的差距,比你想象的大得多。仿真中的里程计是理想的,真机上的轮式里程计会因为轮子打滑、地面不平、编码器分辨率不足而产生累积误差;仿真中的激光雷达噪声是高斯分布,真机上的雷达会受到光照、反射率、灰尘的影响;仿真中的电机响应是瞬时的,真机上的电机有加减速延迟和死区。

我的建议是,在仿真中把SLAM和导航的完整流程跑熟,理解每个参数的含义和调整方向,然后再上真机。上真机后,第一件事是标定里程计——让机器人直线走1米,看odom数据是否也是1米;让机器人转360度,看odom的角度是否也是360度。如果偏差大,需要调整轮距和轮半径参数。第二件事是确认激光雷达的安装位置和URDF中的描述一致,包括高度、角度、前后偏移。第三件事是重新调整导航参数,特别是膨胀半径和速度限制,因为真机的安全要求更高。

仿真最大的价值不是替代真机,而是让你在低成本、零风险的环境下建立对SLAM和导航的直觉。当你能在仿真中快速定位问题、调整参数、验证方案时,上真机后的调试效率会成倍提升。我在实际项目中的体会是,仿真阶段花时间把TF树、代价地图、规划器参数这些基础打牢,真机上至少能省掉一半的调试时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:17:33

eWebEditor v8.0部署实战:旧系统改造中的富文本编辑器配置与避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:17:04

CTF RSA密码题从入门到实战:常见题型与解题脚本全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:17:04

ESD设计窗口:TVS选型与PCB布局如何决定防护成败

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:16:38

EMC标准分类全解析:通用标准与产品族标准选用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:14:51

CMake源码编译安装指南:cmake-3.24.0离线部署与版本升级实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:14:36

VS编译LAStools工具全解析:从源码到可执行文件

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华