Files
OrcaSlicer/doc/developer-reference/Print_Time_Estimation_Analysis.md
xiaoyeliu b43cfaaaf9 2.2.0 flutter & WCP & Network Test (#54)
* Add docs about time_estimate

* Fix: Problems with graceful program exit caused by Flutter refactoring

* Add: sw_OpenBrowser() & sw_OpenOrcaWebview

* Fix: NetworkTestDialog Crash & Add: Lan Device test \ cloud test
2025-12-09 10:39:27 +08:00

488 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 打印时间预估分析文档
## 概述
本文档详细分析了OrcaSlicer中所有影响打印时间预估的工艺配置项以及温度相关的预估逻辑。
## 时间预估核心机制
时间预估的核心实现在 `src/libslic3r/GCode/GCodeProcessor.cpp` 中的 `TimeMachine` 类。它通过以下方式计算时间:
1. **梯形速度曲线Trapezoid**每个移动块TimeBlock被分解为加速、巡航、减速三个阶段
2. **加速度限制**:根据配置的加速度值计算加速/减速所需的时间和距离
3. **速度限制**:根据配置的速度值计算巡航阶段的时间
4. **同步等待**某些命令如M400、G4、M191等会调用 `simulate_st_synchronize()` 添加固定等待时间
## 影响时间预估的配置项
### 1. 速度相关配置Speed Settings
这些配置直接影响移动速度,从而影响时间预估:
| 配置名称 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `initial_layer_speed` | 首层打印速度 | `GCodeProcessor::process_line_G1()` | 首层所有移动使用此速度,速度越低时间越长 |
| `initial_layer_infill_speed` | 首层填充速度 | `GCodeProcessor::process_line_G1()` | 首层填充移动使用此速度 |
| `outer_wall_speed` | 外壁速度 | `GCodeProcessor::process_line_G1()` | 外壁移动使用此速度,影响外壁打印时间 |
| `inner_wall_speed` | 内壁速度 | `GCodeProcessor::process_line_G1()` | 内壁移动使用此速度,影响内壁打印时间 |
| `top_surface_speed` | 顶面速度 | `GCodeProcessor::process_line_G1()` | 顶面移动使用此速度 |
| `internal_solid_infill_speed` | 内部实心填充速度 | `GCodeProcessor::process_line_G1()` | 内部实心填充移动使用此速度 |
| `sparse_infill_speed` | 稀疏填充速度 | `GCodeProcessor::process_line_G1()` | 稀疏填充移动使用此速度 |
| `gap_infill_speed` | 间隙填充速度 | `GCodeProcessor::process_line_G1()` | 间隙填充移动使用此速度 |
| `travel_speed` | 空走速度 | `GCodeProcessor::process_line_G1()` | 空走移动使用此速度,影响空走时间 |
| `small_perimeter_speed` | 小周长速度 | `GCodeProcessor::process_line_G1()` | 小周长移动使用此速度 |
**代码级描述**
-`GCodeProcessor::process_line_G1()`根据移动类型ExtrusionRole选择对应的速度配置
- 速度值直接用于计算 `TimeBlock``cruise_feedrate`
- 时间计算公式:`time = acceleration_time + cruise_time + deceleration_time`
- 其中 `cruise_time = cruise_distance / cruise_feedrate`
### 2. 加速度相关配置Acceleration Settings
这些配置影响加速/减速过程,从而影响时间预估:
| 配置名称 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `default_acceleration` | 默认加速度 | `GCodeProcessor::apply_config()` | 作为默认加速度值,影响所有移动的加速/减速时间 |
| `initial_layer_acceleration` | 首层加速度 | `GCodeProcessor::process_line_G1()` | 首层移动使用此加速度,加速度越低加速/减速时间越长 |
| `outer_wall_acceleration` | 外壁加速度 | `GCodeProcessor::process_line_G1()` | 外壁移动使用此加速度 |
| `inner_wall_acceleration` | 内壁加速度 | `GCodeProcessor::process_line_G1()` | 内壁移动使用此加速度 |
| `top_surface_acceleration` | 顶面加速度 | `GCodeProcessor::process_line_G1()` | 顶面移动使用此加速度 |
| `internal_solid_infill_acceleration` | 内部实心填充加速度 | `GCodeProcessor::process_line_G1()` | 内部实心填充移动使用此加速度 |
| `sparse_infill_acceleration` | 稀疏填充加速度 | `GCodeProcessor::process_line_G1()` | 稀疏填充移动使用此加速度 |
| `travel_acceleration` | 空走加速度 | `GCodeProcessor::set_travel_acceleration()` | 空走移动使用此加速度 |
| `bridge_acceleration` | 桥接加速度 | `GCodeProcessor::process_line_G1()` | 桥接移动使用此加速度 |
**代码级描述**
-`GCodeProcessor::apply_config()` 中,加速度值被设置到 `TimeMachine::acceleration`
-`TimeBlock::calculate_trapezoid()` 中,使用加速度计算加速/减速距离:
```cpp
float accelerate_distance = estimated_acceleration_distance(entry_feedrate, cruise_feedrate, acceleration);
float decelerate_distance = estimated_acceleration_distance(cruise_feedrate, exit_feedrate, -acceleration);
```
- 加速度越低,加速/减速距离越长,巡航距离越短,但总时间可能更长(取决于移动距离)
### 3. 机器限制配置Machine Limits
这些配置限制了速度和加速度的上限:
| 配置名称 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `machine_max_acceleration_extruding` | 挤出时最大加速度 | `GCodeProcessor::apply_config()` | 限制所有挤出移动的加速度上限 |
| `machine_max_acceleration_retracting` | 回抽时最大加速度 | `GCodeProcessor::apply_config()` | 限制回抽移动的加速度上限 |
| `machine_max_acceleration_travel` | 空走时最大加速度 | `GCodeProcessor::apply_config()` | 限制空走移动的加速度上限 |
| `machine_max_speed_x` | X轴最大速度 | `GCodeProcessor::process_M203()` | 限制X轴移动速度上限 |
| `machine_max_speed_y` | Y轴最大速度 | `GCodeProcessor::process_M203()` | 限制Y轴移动速度上限 |
| `machine_max_speed_z` | Z轴最大速度 | `GCodeProcessor::process_M203()` | 限制Z轴移动速度上限 |
| `machine_max_speed_e` | E轴最大速度 | `GCodeProcessor::process_M203()` | 限制挤出速度上限 |
**代码级描述**
- 在 `GCodeProcessor::apply_config()` 中,机器限制被设置到 `TimeMachine::max_acceleration`
- 在 `TimeBlock::calculate_trapezoid()` 中,加速度会被限制在最大值内
- 在 `GCodeProcessor::process_M203()` 中,速度限制被应用到各轴
### 4. 温度相关配置Temperature Settings
#### 4.1 预热配置
| 配置名称 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `preheat_time` | 预热时间 | `GCodeProcessor::apply_config()` | **不直接影响时间预估**仅用于计算何时插入M104预热命令 |
| `delta_temperature` | 预热温度差 | `GCodeProcessor::apply_config()` | **不直接影响时间预估**,仅用于计算预热温度 |
| `preheat_steps` | 预热步数 | `GCodeProcessor::apply_config()` | **不直接影响时间预估**,用于预热回溯的步数 |
**代码级描述**
- 在 `GCodeProcessor::apply_config()` 中,这些值被存储为成员变量
- 在 `GCodeProcessor::process_line_T()` 中,使用 `preheat_time` 和 `preheat_steps` 计算何时插入M104命令
- **重要**:预热时间本身**不**被添加到时间预估中因为M104命令是异步的不等待
#### 4.2 温度等待命令
| G代码命令 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `M109` | 设置温度并等待 | `GCodeProcessor::process_M109()` | **当前实现有问题**:只更新温度值,**没有添加等待时间** |
| `M190` | 等待热床温度 | `GCodeProcessor::process_M190()` | **当前实现有问题**:只更新温度值,**没有添加等待时间** |
| `M191` | 等待腔室温度 | `GCodeProcessor::process_M191()` | 如果温度>40°C添加**硬编码的720秒**等待时间 |
**代码级描述**
**M109问题**`src/libslic3r/GCode/GCodeProcessor.cpp:3640-3655`
```cpp
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('R', new_temp)) {
// ... 更新温度值 ...
}
else if (line.has_value('S', new_temp))
m_extruder_temps[m_extruder_id] = new_temp;
// ❌ 问题:没有调用 simulate_st_synchronize() 添加等待时间
}
```
**M190问题**`src/libslic3r/GCode/GCodeProcessor.cpp:3696-3701`
```cpp
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp))
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
// ❌ 问题:没有调用 simulate_st_synchronize() 添加等待时间
}
```
**M191实现**`src/libslic3r/GCode/GCodeProcessor.cpp:3703-3710`
```cpp
void GCodeProcessor::process_M191(const GCodeReader::GCodeLine& line)
{
float chamber_temp = 0;
const float wait_chamber_temp_time = 720.0; // 硬编码720秒
if (line.has_value('S', chamber_temp) && chamber_temp > 40)
simulate_st_synchronize(wait_chamber_temp_time); // ✅ 正确添加了等待时间
}
```
**问题分析**
- **所有M109命令都没有计算等待时间**`process_M109()` 只更新温度值,没有调用 `simulate_st_synchronize()`
- **所有M190命令都没有计算等待时间**`process_M190()` 只更新温度值,没有调用 `simulate_st_synchronize()`
- 这导致时间预估**显著偏短**,因为忽略了所有温度等待时间
- 实际打印中M109/M190会阻塞直到温度达到但预估中没有计算这部分时间
- 影响最严重的情况:
- **首层打印前**如果开始G-code中有M109等待时间可能达到30-60秒
- **工具切换时**如果切换工具需要加热等待时间可能达到20-40秒
- **温度变化大时**从低温如150°C加热到高温如280°C等待时间可能达到50-80秒
### 5. 工具切换相关配置Tool Change Settings
| 配置名称 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `machine_tool_change_time` | 工具切换时间 | `GCodeProcessor::apply_config()` | 在 `process_T()` 中,每次工具切换添加此时间 |
| `machine_load_filament_time` | 加载耗材时间 | `GCodeProcessor::apply_config()` | 在 `process_T()` 中,工具切换时添加此时间 |
| `machine_unload_filament_time` | 卸载耗材时间 | `GCodeProcessor::apply_config()` | 在 `process_M702()` 中,卸载时添加此时间 |
**代码级描述**
- 在 `GCodeProcessor::process_T()` 中(`src/libslic3r/GCode/GCodeProcessor.cpp:3964-4012`
```cpp
float extra_time = 0.0f;
if (m_time_processor.extruder_unloaded) {
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(m_extruder_id));
extra_time += m_time_processor.machine_tool_change_time;
simulate_st_synchronize(extra_time);
}
```
### 6. 延迟/等待命令Delay/Wait Commands
| G代码命令 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `G4` | 延迟命令 | `GCodeProcessor::process_G4()` | 添加指定的延迟时间S参数为秒P参数为毫秒 |
| `M1` | 暂停等待 | `GCodeProcessor::process_M1()` | 添加同步等待时间(无限等待,但预估中可能不计算) |
| `M400` | 等待移动完成 | `GCodeProcessor::process_M400()` | 添加同步等待时间 |
**代码级描述**
- `G4` 命令(`src/libslic3r/GCode/GCodeProcessor.cpp:3457-3462`
```cpp
float value_s = 0.0;
float value_p = 0.0;
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
value_s += value_p * 0.001; // P参数转换为秒
simulate_st_synchronize(value_s);
}
```
### 7. 其他影响时间预估的因素
| 配置/因素 | 配置解释 | 代码位置 | 如何影响时间预估 |
|---------|---------|---------|----------------|
| `slow_down_layer_time` | 层冷却时间 | `CoolingBuffer` | 如果层时间太短,会降低速度以增加层时间 |
| `fan_cooling_layer_time` | 风扇冷却层时间 | `CoolingBuffer` | 影响冷却逻辑,可能降低速度 |
| `G29` (自动调平) | 自动调平 | `GCodeProcessor::process_G29()` | 硬编码添加**260秒**等待时间 |
| `retraction_speed` | 回抽速度 | `GCodeProcessor::process_line_G1()` | 影响回抽移动的时间 |
| `deretraction_speed` | 回退速度 | `GCodeProcessor::process_line_G1()` | 影响回退移动的时间 |
## 温度相关时间预估问题总结
### 问题1M109没有添加等待时间
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:3640-3655`
**问题**M109命令应该等待温度达到目标值但当前实现只更新了温度值没有添加等待时间。
**影响**:时间预估会**偏短**因为忽略了温度等待时间。对于需要从低温加热到高温的情况如从室温到220°C实际等待时间可能达到30-60秒但预估中没有计算。
**建议修复**
```cpp
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
float new_temp;
float current_temp = m_extruder_temps[m_extruder_id];
if (line.has_value('R', new_temp)) {
// ... 处理R参数 ...
}
else if (line.has_value('S', new_temp)) {
m_extruder_temps[m_extruder_id] = new_temp;
}
// 计算等待时间:根据温度差估算
if (new_temp > current_temp) {
float temp_diff = new_temp - current_temp;
// 假设加热速度约为 2-3°C/秒(可根据实际情况调整)
float wait_time = temp_diff / 2.5f; // 保守估计
// 最小等待时间5秒最大等待时间120秒
wait_time = std::clamp(wait_time, 5.0f, 120.0f);
simulate_st_synchronize(wait_time);
}
}
```
### 问题2M190没有添加等待时间
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:3696-3701`
**问题**M190命令应该等待热床温度达到目标值但当前实现只更新了温度值没有添加等待时间。
**影响**:时间预估会**偏短**,特别是首层打印前的热床预热时间。
**建议修复**
```cpp
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp)) {
float current_bed_temp = m_highest_bed_temp;
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
// 计算等待时间:热床加热速度较慢,约为 0.5-1°C/秒
if (new_temp > current_bed_temp) {
float temp_diff = new_temp - current_bed_temp;
float wait_time = temp_diff / 0.75f; // 保守估计
// 最小等待时间10秒最大等待时间300秒
wait_time = std::clamp(wait_time, 10.0f, 300.0f);
simulate_st_synchronize(wait_time);
}
}
}
```
### 问题3M191使用硬编码时间
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:3703-3710`
**问题**M191使用硬编码的720秒12分钟等待时间没有根据实际温度差计算。
**影响**:对于不同的温度目标,等待时间可能不准确。
**建议修复**
```cpp
void GCodeProcessor::process_M191(const GCodeReader::GCodeLine& line)
{
float chamber_temp = 0;
if (line.has_value('S', chamber_temp) && chamber_temp > 40) {
// 腔室加热速度很慢,约为 0.1-0.2°C/秒
// 假设从室温(~25°C加热到目标温度
float temp_diff = chamber_temp - 25.0f;
float wait_time = temp_diff / 0.15f; // 保守估计
// 最小等待时间60秒最大等待时间1800秒30分钟
wait_time = std::clamp(wait_time, 60.0f, 1800.0f);
simulate_st_synchronize(wait_time);
}
}
```
### 问题4预热时间不参与时间预估
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:4594-4668`
**问题**`preheat_time` 配置仅用于计算何时插入M104命令但不参与时间预估。
**影响**:如果预热时间设置较长,实际打印中工具切换时的等待时间可能被缩短,但预估中没有考虑这个优化。
**分析**
- 预热机制的目的是**减少**工具切换时的等待时间
- 如果预热成功M109的等待时间应该**减少**(因为工具已经预热)
- 当前实现中,预热时间不参与预估是**合理的**,因为:
1. M104是异步命令不阻塞打印
2. 预热是否成功取决于实际打印进度
3. 如果预热失败仍然需要等待M109
**建议**保持当前实现但可以考虑在M109中添加逻辑如果检测到之前有M104预热命令可以减少等待时间。
## 补充问题解答
### 1. 打印准备时间prepare_time是怎么来的
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:375-376, 2774, 3228`
**机制**
1. **标记准备阶段**:在 `process_line_G1()` 中当处理G1/G0命令时会创建 `TimeBlock` 并设置:
```cpp
block.flags.prepare_stage = m_processing_start_custom_gcode;
```
2. **设置准备阶段标志**`m_processing_start_custom_gcode` 在 `process_gcode_line()` 中设置:
```cpp
// 当遇到自定义G代码erCustom且是第一条G1命令时
m_processing_start_custom_gcode = (m_extrusion_role == erCustom && m_g1_line_id == 0);
```
这意味着**开始G代码start_gcode中的移动**会被标记为准备阶段。
3. **累加准备时间**:在 `TimeMachine::calculate_time()` 中:
```cpp
if (block.flags.prepare_stage)
prepare_time += block_time;
```
**包含的内容**
- 开始G代码`machine_start_gcode`)中的所有移动时间
- 开始G代码中的温度设置、回零等操作时间
- 首层打印前的所有准备操作时间
**注意**准备阶段中的空走Travel移动**不计入**空走时间统计,但**计入**准备时间:
```cpp
//BBS: don't calculate travel of start gcode into travel time
if (!block.flags.prepare_stage || block.move_type != EMoveType::Travel)
moves_time[static_cast<size_t>(block.move_type)] += block_time;
```
**用途**
- 在UI中显示"首层打印时间"时,会从总时间中减去准备时间
- 用于区分"准备时间"和"实际打印时间"
### 2. simulate_st_synchronize 是干什么的?
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:257-263`
**功能**模拟固件如Marlin的 `st_synchronize()` 函数调用,表示**等待所有移动完成并添加额外时间**。
**实现**
```cpp
void GCodeProcessor::TimeMachine::simulate_st_synchronize(float additional_time)
{
if (!enabled)
return;
calculate_time(0, additional_time);
}
```
**作用**
1. **处理时间块队列**:调用 `calculate_time()` 处理 `blocks` 队列中累积的移动块
2. **添加额外时间**:将 `additional_time` 添加到第一个待处理块的时间中
3. **更新统计**:累加到总时间、层时间、角色时间等统计中
**使用场景**
- **G4延迟命令**`process_G4()` 调用 `simulate_st_synchronize(value_s)` 添加延迟时间
- **M400等待完成**`process_M400()` 调用 `simulate_st_synchronize()` 等待移动完成
- **M1暂停**`process_M1()` 调用 `simulate_st_synchronize()` 添加暂停时间
- **M191等待腔室温度**`process_M191()` 调用 `simulate_st_synchronize(720.0)` 添加等待时间
- **工具切换**`process_T()` 调用 `simulate_st_synchronize(extra_time)` 添加切换和加载时间
- **G29自动调平**`process_G29()` 调用 `simulate_st_synchronize(260.0)` 添加调平时间
**为什么需要**
- 固件的 `st_synchronize()` 会阻塞直到所有移动完成
- 某些命令如M109、M190需要等待操作完成
- 需要在时间预估中反映这些等待时间
**问题**当前实现中M109和M190**没有调用** `simulate_st_synchronize()`,导致等待时间没有被计算。
### 3. 算时间是所有G-code都生成了之后后处理的还是生成一条G-code就算一行
**答案****混合模式** - 大部分是**实时处理**,最终计算是**后处理**。
**详细流程**
#### 3.1 实时处理阶段G-code生成/解析时)
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:1550-2074` (`process_gcode_line`)
**过程**
1. **解析G-code行**每解析一行G-code调用 `process_gcode_line()`
2. **处理G1/G0命令**遇到G1/G0命令时调用 `store_move_vertex()` → `process_line_G1()`
3. **创建时间块**:在 `process_line_G1()` 中创建 `TimeBlock` 并添加到 `blocks` 队列:
```cpp
TimeBlock block;
block.move_type = type;
block.role = m_extrusion_role;
block.distance = distance;
// ... 设置其他属性 ...
machine.blocks.push_back(block);
```
4. **立即计算(部分)**:某些命令会立即调用 `simulate_st_synchronize()`
- G4延迟立即添加延迟时间
- M400等待立即处理队列
- 工具切换:立即添加切换时间
**特点**
- 时间块TimeBlock是**实时创建**的
- 但**不立即计算**最终时间(因为需要前后块的信息来优化速度曲线)
#### 3.2 后处理阶段所有G-code处理完成后
**位置**`src/libslic3r/GCode/GCodeProcessor.cpp:1309-1361` (`finalize`)
**过程**
1. **最终计算**:在 `finalize()` 中调用 `calculate_time()`
```cpp
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
machine.calculate_time(); // 处理所有剩余的blocks
}
```
2. **速度曲线优化**`calculate_time()` 执行:
- **前向传递forward_pass**:从前往后优化入口速度
- **反向传递reverse_pass**:从后往前优化出口速度
- **重新计算梯形**:根据优化后的速度重新计算加速/巡航/减速阶段
3. **累加统计**:计算每个块的时间并累加到:
- `time`:总时间
- `prepare_time`:准备时间
- `layers_time`:每层时间
- `roles_time`:每种角色时间
- `moves_time`:每种移动类型时间
**为什么需要后处理**
- **速度曲线优化**:需要知道后续块的速度才能优化当前块的出口速度
- **加速度限制**:需要确保相邻块之间的速度转换符合加速度限制
- **全局优化**:需要反向传递来确保从后往前的速度优化
#### 3.3 混合模式的原因
**实时处理**
- 创建时间块TimeBlock
- 处理立即生效的命令G4、M400等
- 累积到队列中
**后处理**
- 优化速度曲线(需要全局信息)
- 计算最终时间(需要所有块的信息)
- 更新统计信息
**性能考虑**
- 如果每条G-code都完整计算性能会很差需要重新计算所有块
- 采用批量处理 + 最终优化的方式,平衡了实时性和准确性
## 总结
影响时间预估的主要因素:
1. **速度配置**:直接影响移动时间
2. **加速度配置**:影响加速/减速时间
3. **机器限制**:限制速度和加速度上限
4. **温度等待****当前实现有问题**M109/M190没有添加等待时间
5. **工具切换**:添加固定的切换和加载时间
6. **延迟命令**G4、M400等添加固定延迟
7. **准备时间**开始G代码中的操作时间单独统计
8. **时间计算模式**:混合模式 - 实时创建时间块,后处理优化和计算
**最严重的问题**M109和M190没有添加等待时间这会导致时间预估**显著偏短**,特别是对于需要从低温加热到高温的打印任务。
**时间计算流程**
1. **实时**解析G-code → 创建TimeBlock → 添加到队列
2. **实时(部分)**:某些命令立即调用 `simulate_st_synchronize()` 处理队列
3. **后处理**所有G-code处理完成后调用 `finalize()` → `calculate_time()` 优化速度曲线并计算最终时间