Загрузка данных


# gui/core_planner.py
import math
import re

class CNCPlannerX:
    def __init__(self, accel=150.0, min_speed=2.0, junction_deviation=0.02):
        self.accel = accel                     # мм/сек^2
        self.min_speed = min_speed             # мм/сек
        self.junction_deviation = junction_deviation  # мм

    def parse_and_plan(self, gcode_lines):
        """Главный публичный API метод."""
        # 1. Линеаризация (Чистый парсинг)
        segments = self._parse_to_segments(gcode_lines)
        if not segments:
            return []

        # 2. Математика Look-Ahead (Разгон / Торможение / Изломы)
        segments = self._apply_look_ahead(segments)
        
        # 3. Сборка бинарного потока пакетов B:... с 7 разделителями
        packet_stream = []
        for seg in segments:
            packet = (
                f"B:{seg['type']};{seg['line']};"
                f"{seg['x']:.3f};{seg['y']:.3f};{seg['z']:.3f};"
                f"{seg['f_min']:.3f};{seg['v_start']:.3f};{seg['v_end']:.3f}"
            )
            packet_stream.append(packet)
            
        return packet_stream

    def _parse_to_segments(self, gcode_lines):
        """Парсер модальных осей. Защищен от синтаксических ошибок."""
        segments = []
        curr_x, curr_y, curr_z = 0.0, 0.0, 0.0
        curr_f = 600.0  # мм/мин
        
        for idx, line in enumerate(gcode_lines):
            # 1. Переводим в верхний регистр и убираем пробелы по краям
            line_upper = line.upper().strip()
            if not line_upper:
                continue
                
            # 2. ИСПРАВЛЕНО: Безопасно отсекаем комментарии (split возвращает список, берем элемент)
            clean_line = line_upper.split(';')[0].strip()
            if not clean_line:
                continue
                
            # Поиск токенов через Regex
            g_match = re.search(r'G([0-1])', clean_line)
            m_match = re.search(r'M([0-9]+)', clean_line)
            x_match = re.search(r'X([-+]?[0-9]*\.?[0-9]+)', clean_line)
            y_match = re.search(r'Y([-+]?[0-9]*\.?[0-9]+)', clean_line)
            z_match = re.search(r'Z([-+]?[0-9]*\.?[0-9]+)', clean_line)
            f_match = re.search(r'F([0-9]*\.?[0-9]+)', clean_line)
            
            seg_type = 1  # По умолчанию движение G0/G1
            if m_match:
                seg_type = 3  # М-команда
                
            if f_match:
                curr_f = float(f_match.group(1))
            f_min = curr_f / 60.0  # Ограничение кадра (мм/сек)
            
            # Модальное автозаполнение осей (Защита от улета в 0)
            target_x = float(x_match.group(1)) if x_match else curr_x
            target_y = float(y_match.group(1)) if y_match else curr_y
            target_z = float(z_match.group(1)) if z_match else curr_z
            
            # Расчет дельт перемещений
            dx = target_x - curr_x
            dy = target_y - curr_y
            dz = target_z - curr_z
            length = math.sqrt(dx*dx + dy*dy + dz*dz)
            
            # Сохраняем абсолютно все кадры
            segment = {
                "type": seg_type, 
                "line": idx, 
                "x": target_x, "y": target_y, "z": target_z,
                "dx": dx, "dy": dy, "dz": dz, "length": length,
                "f_min": f_min, 
                "v_start": self.min_speed, 
                "v_end": self.min_speed
            }
            segments.append(segment)
            
            # Сдвигаем точку виртуального инструмента
            curr_x, curr_y, curr_z = target_x, target_y, target_z
            
        return segments

    def _apply_look_ahead(self, segments):
        """Классический трехпроходной Look-Ahead алгоритм ЧПУ."""
        if len(segments) <= 1:
            return segments
            
        # Инициализируем стыки максимальным маршевым потолком кадра
        for seg in segments:
            if seg["type"] == 1 and seg["length"] > 1e-4:
                seg["v_start"] = seg["f_min"]
                seg["v_end"] = seg["f_min"]
            else:
                seg["v_start"] = self.min_speed
                seg["v_end"] = self.min_speed
            
        # Пасс 1: Расчет лимитов скоростей на изломах векторов (Junction Deviation)
        for i in range(len(segments) - 1):
            seg = segments[i]
            next_seg = segments[i + 1]
            
            if seg["type"] != 1 or next_seg["type"] != 1 or seg["length"] < 1e-4 or next_seg["length"] < 1e-4:
                v_junction = self.min_speed
            else:
                dot_product = (seg["dx"] * next_seg["dx"] + 
                               seg["dy"] * next_seg["dy"] + 
                               seg["dz"] * next_seg["dz"])
                
                cos_theta = dot_product / (seg["length"] * next_seg["length"])
                cos_theta = max(-1.0, min(1.0, cos_theta))
                
                if cos_theta > 0.99999:
                    v_junction = min(seg["f_min"], next_seg["f_min"])
                else:
                    sin_half_theta = math.sqrt((1.0 - cos_theta) / 2.0)
                    if sin_half_theta >= 0.9999:
                        v_junction = self.min_speed
                    else:
                        radius = (self.junction_deviation * sin_half_theta) / (1.0 - sin_half_theta)
                        v_junction = math.sqrt(self.accel * radius)
                        v_junction = min(v_junction, seg["f_min"], next_seg["f_min"])
                        v_junction = max(v_junction, self.min_speed)
                        
            seg["v_end"] = min(seg["v_end"], v_junction)
            next_seg["v_start"] = min(next_seg["v_start"], v_junction)
            
        # Фиксируем края всей программы
        segments[0]["v_start"] = self.min_speed
        segments[-1]["v_end"] = self.min_speed
        
        # Пасс 2: Торможение (Обратный проход)
        for i in reversed(range(len(segments) - 1)):
            seg = segments[i]
            next_seg = segments[i + 1]
            
            if seg["type"] == 1 and seg["length"] > 1e-4:
                v_allowable = math.sqrt(seg["v_end"]**2 + 2.0 * self.accel * seg["length"])
                if seg["v_start"] > v_allowable:
                    seg["v_start"] = v_allowable
            
            if next_seg["length"] > 1e-4:
                seg["v_end"] = min(seg["v_end"], next_seg["v_start"])
            
        # Пасс 3: Разгон (Прямой проход)
        for i in range(len(segments) - 1):
            seg = segments[i]
            next_seg = segments[i + 1]
            
            if seg["type"] == 1 and seg["length"] > 1e-4:
                v_allowable = math.sqrt(seg["v_start"]**2 + 2.0 * self.accel * seg["length"])
                if seg["v_end"] > v_allowable:
                    seg["v_end"] = v_allowable
                    
            if seg["length"] > 1e-4:
                next_seg["v_start"] = min(next_seg["v_start"], seg["v_end"])
            
        # Финальная инспекция нижнего порога скорости
        for seg in segments:
            if seg["type"] == 1 and seg["length"] > 1e-4:
                seg["v_start"] = max(seg["v_start"], self.min_speed)
                seg["v_end"] = max(seg["v_end"], self.min_speed)
                
        return segments