import os

from bcad.binterpreter.progress_helper import advance_progress, adjust_progress_total, check_cancel, push_label, pop_label, set_progress_label

from OCC.Core.GeomAPI import GeomAPI_Interpolate

from OCC.Core.AIS import AIS_Line
from OCC.Core.Geom import Geom_Line
from OCC.Core.Prs3d import Prs3d_LineAspect
from OCC.Core.HLRBRep import HLRBRep_HLRToShape, HLRBRep_Algo
from OCC.Core.HLRAlgo import HLRAlgo_Projector
from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_GTrsf, gp_Vec, gp_Pln, gp_Circ, gp_Mat
from OCC.Core.ChFi2d import ChFi2d_AnaFilletAlgo
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge, BRepBuilderAPI_Transform, BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeWire, BRepBuilderAPI_FindPlane
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakePrism, BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeCone, BRepPrimAPI_MakeBox, BRepPrimAPI_MakeRevol, BRepPrimAPI_MakeSphere
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut, BRepAlgoAPI_Common
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_ThruSections, BRepOffsetAPI_MakePipeShell, BRepOffsetAPI_MakeOffsetShape
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
from OCC.Core.GeomAbs import GeomAbs_Sphere, GeomAbs_Arc, GeomAbs_Intersection, GeomAbs_Plane
from OCC.Core.BRepOffset import BRepOffset_Skin, BRepOffset_MakeOffset
from OCC.Core.TopoDS import topods, TopoDS_Compound, TopoDS_Iterator, TopoDS_Shape
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_WIRE, TopAbs_SHELL, TopAbs_SOLID, TopAbs_COMPOUND
from OCC.Core.BRep import BRep_Builder, BRep_Tool
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Quantity import (Quantity_Color, Quantity_ColorRGBA, Quantity_NOC_PERU)
from OCC.Core.V3d import V3d_XposYposZpos, V3d_XnegYposZpos, V3d_XnegYnegZpos, V3d_XposYposZneg, V3d_XnegYposZneg, V3d_XposYnegZneg, V3d_XnegYnegZneg
from OCC.Display.OCCViewer import rgb_color
from OCC.Extend.TopologyUtils import TopologyExplorer
from OCC.Extend.ShapeFactory import make_wire
from OCC.Extend.DataExchange import read_stl_file

from OCC.Core.IFSelect import IFSelect_RetDone
from OCC.Core.TDocStd import TDocStd_Document
from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
from OCC.Core.STEPCAFControl import STEPCAFControl_Reader
from OCC.Core.TDF import TDF_LabelSequence, TDF_Label
from OCC.Core.TopLoc import TopLoc_Location

from OCC.Core.TopTools import TopTools_ListOfShape
from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
from OCC.Core.Message import Message_ProgressRange

from bcad.binterpreter.bgui import init_display
from bcad.binterpreter.colorname_map import colorname_map
from bcad.binterpreter.events import ep
from bcad.binterpreter.singleton import Singleton
from bcad.binterpreter.scl_shape import SCLShape, DISP_MODE_WIREFRAME
from bcad.binterpreter.scl_util import is_var_set, unstringify, parse_hex_color

from logging import debug, warning

import math

class ContextError(Exception):
    def __init__(self, preamble, fname, line):
        self.message = "%s in %s at line %i"%(preamble, fname, line)

    def __str__(self):
        return self.message

names = {
    "scale": 0,
    "translate": 0,
    "rotate": 0,
    "color": 0,
    "cube": 0,
    "polygon": 0,
    "cylinder": 0,
    "linear_extrude": 0,
    "union": 0,
    "intersection": 0,
    "difference": 0,
    "minkowski": 0,
    "projection": 0,
    "profile2": 0,
    "step": 0,
    "stl": 0,
    "part": 0,
    "extrusion": 0,
    "loft": 0,
    "unknown": 0,
}

def get_inc_name(prefix):
    if prefix not in names:
        n = "unk_"+prefix+str(names["unknown"])
        names["unknown"]+=1
    else:
        n = prefix+str(names[prefix])
        names[prefix]+=1
    return n

class SCLDisplay:
    def __init__(self):
        self.display = None
        self.start_display = None
        self._axis_length = 0
        self._axis_ais = []
        self.scl_init_display()

    def mk_axis_line(self, direction, length, color, linestyle, width):
        origin = gp_Pnt(0.0, 0.0, 0.0)
        geom_line = Geom_Line(origin, direction)
        ais_line = AIS_Line(geom_line)

        drawer = ais_line.Attributes()
        aspect = Prs3d_LineAspect(color, linestyle, width)
        drawer.SetLineAspect(aspect)
        drawer.SetMaximalParameterValue(length / 2.0)
        ais_line.SetAttributes(drawer)
        return ais_line

    def axis(self):
        if self.display is None:
            return
        w, h = self.display.View.Size()
        diag = math.sqrt(w*w + h*h)
        self._axis_length = max(1e-8, 10 * diag / 2.0)
        self._axis_ais = []
        for dir, color in [
            (gp_Dir(1, 0, 0), rgb_color(1,0,0)),
            (gp_Dir(0, 1, 0), rgb_color(0,1,0)),
            (gp_Dir(0, 0, 1), rgb_color(0,0,1)),
        ]:
            ais = self.mk_axis_line(dir, self._axis_length, color, 0, 1.0)
            self.display.Context.Display(ais, False)
            self._axis_ais.append(ais)

    def _update_axis_length(self):
        if self.display is None or not hasattr(self.display, 'View') or self.display.View is None:
            return
        w, h = self.display.View.Size()
        new_len = max(1e-8, 10 * math.sqrt(w*w + h*h) / 2.0)
        if abs(new_len - self._axis_length) < 0.01 * self._axis_length:
            return
        self._axis_length = new_len
        for ais in self._axis_ais:
            self.display.Context.Remove(ais, False)
        self.axis()

    def perspective(self, event=None):
        self.display.SetPerspectiveProjection()
        self.display.FitAll()

    def orthographic(self, event=None):
        self.display.SetOrthographicProjection()
        self.display.FitAll()

    def top(self, event=None):
        self.display.View_Top()
        self.display.Context.UpdateCurrentViewer()

    def bottom(self, event=None):
        self.display.View_Bottom()
        self.display.Context.UpdateCurrentViewer()

    def left(self, event=None):
        self.display.View_Left()
        self.display.Context.UpdateCurrentViewer()

    def right(self, event=None):
        self.display.View_Right()
        self.display.Context.UpdateCurrentViewer()

    def front(self, event=None):
        self.display.View_Front()
        self.display.Context.UpdateCurrentViewer()

    def rear(self, event=None):
        self.display.View_Rear()
        self.display.Context.UpdateCurrentViewer()

    def rxp(self, event=None):
        self.display.View.Rotate(math.radians(15),0,0, True)
        self.display.Context.UpdateCurrentViewer()

    def rxn(self, event=None):
        self.display.View.Rotate(math.radians(-15),0,0, True)
        self.display.Context.UpdateCurrentViewer()

    def ryp(self, event=None):
        self.display.View.Rotate(0,math.radians(15),0, True)
        self.display.Context.UpdateCurrentViewer()

    def ryn(self, event=None):
        self.display.View.Rotate(0,math.radians(-15),0, True)
        self.display.Context.UpdateCurrentViewer()

    def rzp(self, event=None):
        self.display.View.Rotate(0,0,math.radians(15), True)
        self.display.Context.UpdateCurrentViewer()

    def rzn(self, event=None):
        self.display.View.Rotate(0,0,math.radians(-15), True)
        self.display.Context.UpdateCurrentViewer()

    def isometric1(self, event=None):
        self.display.View_Iso()
        self.display.FitAll()

    def isometric2(self, event=None):
        self.display.View.SetProj(V3d_XposYposZpos)
        self.display.FitAll()

    def isometric3(self, event=None):
        self.display.View.SetProj(V3d_XnegYposZpos)
        self.display.FitAll()

    def isometric4(self, event=None):
        self.display.View.SetProj(V3d_XnegYnegZpos)
        self.display.FitAll()

    def isometric5(self, event=None):
        self.display.View.SetProj(V3d_XposYposZneg)
        self.display.FitAll()

    def isometric6(self, event=None):
        self.display.View.SetProj(V3d_XnegYposZneg)
        self.display.FitAll()

    def isometric7(self, event=None):
        self.display.View.SetProj(V3d_XposYnegZneg)
        self.display.FitAll()

    def isometric8(self, event=None):
        self.display.View.SetProj(V3d_XnegYnegZneg)
        self.display.FitAll()

    def reset(self, event=None):
        #self.display.Context.CloseAllContexts()
        self.display.Context.RemoveAll(True)
        self.axis()
        self.display.Repaint()

    def periodic(self):
        ep.process()

    def scl_init_display(self):
        self.display, self.start_display = init_display(periodic_callback=self.periodic, period=1)
        self.display.SetRenderingParams(Method=0, RaytracingDepth=3, IsShadowEnabled=False, IsReflectionEnabled=False, IsAntialiasingEnabled=False, IsTransparentShadowEnabled=False, StereoMode=0, AnaglyphFilter=1, ToReverseStereo=False)


        # self.add_menu('View')
        # self.add_function_to_menu('View', self.top)
        # self.add_function_to_menu('View', self.bottom)
        # self.add_function_to_menu('View', self.left)
        # self.add_function_to_menu('View', self.right)
        # self.add_function_to_menu('View', self.front)
        # self.add_function_to_menu('View', self.rear)
        # self.add_function_to_menu('View', self.isometric1)
        # self.add_function_to_menu('View', self.isometric2)
        # self.add_function_to_menu('View', self.isometric3)
        # self.add_function_to_menu('View', self.isometric4)
        # self.add_function_to_menu('View', self.isometric5)
        # self.add_function_to_menu('View', self.isometric6)
        # self.add_function_to_menu('View', self.isometric7)
        # self.add_function_to_menu('View', self.isometric8)
        # self.add_function_to_menu('View', self.perspective)
        # self.add_function_to_menu('View', self.orthographic)
        # self.add_function_to_menu('View', self.reset)
        # self.add_function_to_menu('View', self.rxp) # positive x rotation
        # self.add_function_to_menu('View', self.rxn) # negative x rotation
        # self.add_function_to_menu('View', self.ryp)
        # self.add_function_to_menu('View', self.ryn)
        # self.add_function_to_menu('View', self.rzp)
        # self.add_function_to_menu('View', self.rzn)

        bg1, bg2 = Singleton.settings.get_rgb_pair('display')
        self.display.set_bg_gradient_color(bg1, bg2)

        self.axis()

        self.display.Repaint()

class V2:
    def __init__(self, x, y):
        self.v = [x,y]

    def x(self):
        return self.v[0]

    def y(self):
        return self.v[1]

    def __repr__(self):
        return "<V2 (%f,%f)>"%(self.v[0], self.v[1])

class V3:
    def __init__(self, x, y, z):
        self.v = [x,y,z]

    def x(self):
        return self.v[0]

    def y(self):
        return self.v[1]

    def z(self):
        return self.v[2]

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            if (self.x() == other.x()) and (self.y() == other.y()) and (self.z() == other.z()):
                return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return "<V3 (%f,%f,%f)>"%(self.v[0], self.v[1], self.v[2])

class Line3:
    def __init__(self, sp, dp):
        self.sp = sp
        self.dp = dp
        self.edge = None

    def to_edge(self):
        if (self.edge is None):
            try:
                s = gp_Pnt(self.sp.x(), self.sp.y(), self.sp.z())
                e = gp_Pnt(self.dp.x(), self.dp.y(), self.dp.z())
                self.edge = BRepBuilderAPI_MakeEdge(s, e).Edge()
            except Exception:
                warning("line: invalid edge skipped")
                return None
        return self.edge

    def __repr__(self):
        return "<Line (%s; %s)>"%(repr(self.sp), repr(self.dp))

class Fillet2:
    def __init__(self, radius=1.0):
        self.radius = radius
        self.edge = None
        self.l1 = None
        self.l2 = None

    def to_edge(self, l1, l2):
        if (self.edge is None):
            f = ChFi2d_AnaFilletAlgo()
            f.Init(l1.to_edge(), l2.to_edge(), gp_Pln())
            f.Perform(self.radius)
            self.edge = f.Result(l1.to_edge(), l2.to_edge())
            self.l1 = l1
            self.l2 = l2
        return self.edge

    def __repr__(self):
        return "<Fillet (%s;%s)>"%(repr(self.l1), repr(self.l2))

class Arc3:
    def __init__(self, start, center, angle):
        self.start = start
        self.center = center
        self.angle = angle
        self.edge = None

    def to_edge(self):
        if self.edge is None:
            try:
                dx = self.start.x() - self.center.x()
                dy = self.start.y() - self.center.y()
                if not isinstance(dx, (int, float)) or not isinstance(dy, (int, float)):
                    warning("arc: invalid coordinates")
                    return None
                r = math.sqrt(dx*dx + dy*dy)
                if r < 1e-10:
                    warning("arc: radius too small, skipped")
                    return None
                u_start = math.atan2(dy, dx)
                u_end = u_start + math.radians(self.angle)
                ax = gp_Ax2(gp_Pnt(self.center.x(), self.center.y(), self.center.z()),
                            gp_Dir(0, 0, 1))
                circ = gp_Circ(ax, r)
                self.edge = BRepBuilderAPI_MakeEdge(circ, u_start, u_end).Edge()
            except Exception:
                warning("arc: failed to build edge")
                return None
        return self.edge

    def end_point(self):
        dx = self.start.x() - self.center.x()
        dy = self.start.y() - self.center.y()
        a = math.radians(self.angle)
        cos_a = math.cos(a)
        sin_a = math.sin(a)
        ex = dx * cos_a - dy * sin_a + self.center.x()
        ey = dx * sin_a + dy * cos_a + self.center.y()
        return V3(ex, ey, self.start.z())

    def __repr__(self):
        return "<Arc (start=%s; center=%s; angle=%g)>"%(repr(self.start), repr(self.center), self.angle)

class Spline3:
    def __init__(self, points):
        self.points = points
        self.edge = None

    def to_edge(self):
        if self.edge is None:
            try:
                valid_pts = [p for p in self.points
                             if isinstance(p, V3) and all(isinstance(v, (int, float)) for v in p.v)]
                if len(valid_pts) < 2:
                    warning("spline: need at least 2 valid points")
                    return None
                from OCC.Core.TColgp import TColgp_HArray1OfPnt
                from OCC.Core.GeomAPI import GeomAPI_PointsToBSpline
                n = len(valid_pts)
                arr = TColgp_HArray1OfPnt(1, n)
                for i, p in enumerate(valid_pts):
                    arr.SetValue(i + 1, gp_Pnt(p.x(), p.y(), p.z()))
                bspline = GeomAPI_PointsToBSpline(arr).Curve()
                self.edge = BRepBuilderAPI_MakeEdge(bspline).Edge()
            except Exception:
                warning("spline: failed to build edge")
                return None
        return self.edge

    def end_point(self):
        return self.points[-1]

    def __repr__(self):
        return "<Spline (%i points)>"%(len(self.points),)

class SCLContext(object):
    def __init__(self, parent):
        self.parent = parent
        self.children = []
        self.name = get_inc_name("context")
        self.shape_color = None

    def set_name(self, name):
        self.name = name

    def add_child_context(self, ctx):
        self.children.append(ctx)

    def apply_trsf(self, trsf):
        """ Дефолтный метод: переопределяется в дочерних классах при наличии формы """
        pass

    def apply_color(self, color):
        if color is None:
            self.shape_color = None
            return
        self.shape_color = Quantity_ColorRGBA(
            color[0], color[1], color[2],
            float(color[3]) if len(color) >= 4 else 1.0)

    def apply_display_mode(self, mode):
        """ Дефолтный метод: переопределяется в дочерних классах при наличии формы """
        pass

    def propagate_trsf(self, trsf):
        debug("Apply transform to %s: %s"%(self.name, type(self),))
        self.apply_trsf(trsf)
        for c in self.children:
            c.propagate_trsf(trsf)

    def apply_gtrsf(self, gtrsf):
        pass

    def propagate_gtrsf(self, gtrsf):
        debug("Apply gtrsf to %s: %s"%(self.name, type(self),))
        self.apply_gtrsf(gtrsf)
        for c in self.children:
            c.propagate_gtrsf(gtrsf)

    def propagate_color(self, color):
        debug("Apply color to %s: %s"%(self.name, type(self),))
        self.apply_color(color)
        for c in self.children:
            c.propagate_color(color)

    def color(self, v):
        if v is None or (isinstance(v, list) and len(v) >= 1 and v[0] == None):
            for c in self.children:
                c.propagate_color(None)
            return

        color_val = [0.5, 0.5, 0.5, 1.0]

        # Сценарий А: Передан пакет вида [имя_цвета/hex, alpha]
        if isinstance(v, list) and len(v) == 2 and isinstance(v[0], str):
            v_clean = v[0]
            if v_clean.startswith('#'):
                hc = parse_hex_color(v_clean)
                if hc:
                    alpha = hc[3] * float(v[1])
                    color_val = [hc[0], hc[1], hc[2], alpha]
            elif v_clean in colorname_map:
                c = Quantity_Color(colorname_map[v_clean])
                color_val = [c.Red(), c.Green(), c.Blue(), float(v[1])]
        # Сценарий Б: Передан список [R, G, B] или [R, G, B, Alpha]
        elif isinstance(v, list) and len(v) >= 3:
            color_val = list(v)
            if len(color_val) < 4:
                color_val.append(1.0)
        # Сценарий В: Передана просто строка имени цвета/hex без явного указания альфы
        elif isinstance(v, str):
            v_clean = v
            if v_clean.startswith('#'):
                hc = parse_hex_color(v_clean)
                if hc:
                    color_val = [hc[0], hc[1], hc[2], hc[3]]
            elif v_clean in colorname_map:
                c = Quantity_Color(colorname_map[v_clean])
                color_val = [c.Red(), c.Green(), c.Blue(), 1.0]

        for c in self.children:
            c.propagate_color(color_val)

    def propagate_display_mode(self, mode):
        debug("Apply mode to %s: %s"%(self.name, type(self),))
        self.apply_display_mode(mode)
        for c in self.children:
            c.propagate_display_mode(mode)

    def display(self, writer=None):
        debug("Display SCLContext")
        objects = []
        for c in self.children:
            objects.append(self.child.display(writer))
        objects = {'name': self.name, 'children': objects}
        debug("end SCLContext")
        return objects

    def start_display(self, writer=None):
        if (writer is None):
            if (self.parent is None):
                Singleton.sd.display.FitAll()
                Singleton.sd.start_display()

    def get_children(self):
        children = []
        for c in self.children:
            children.append(c)
            if getattr(c, 'shape', None) is None:
                children.extend(c.get_children())
        return children

class SCLPart3(SCLContext):
    def __init__(self, parent):
        super().__init__(parent)
        self.shape = None
        self.profile = None

    def set_shape(self, shape):
        self.shape = shape

    def apply_color(self, color):
        if color is None:
            self.shape_color = None
            if self.shape is not None:
                self.shape.color(None)
            return
        self.shape_color = Quantity_ColorRGBA(
            color[0], color[1], color[2],
            float(color[3]) if len(color) >= 4 else 1.0)
        if self.shape is not None:
            self.shape.color(color)

    def apply_trsf(self, trsf):
        if self.shape is not None:
            self.shape.transform(trsf)

    def apply_gtrsf(self, gtrsf):
        if self.shape is not None:
            self.shape.transform_gtrsf(gtrsf)

    def apply_display_mode(self, mode):
        if self.shape is not None:
            self.shape.display_mode = mode

    def add_profile_shape(self):
        for c in self.children:
            if isinstance(c, SCLProfile2):
                self.profile = c
                try:
                    w = c.get_wire()
                    if w is not None:
                        face = BRepBuilderAPI_MakeFace(w).Face()
                        self.shape = SCLShape(face)
                except Exception:
                    pass
                break

    def rotate(self, x=0.0, y=0.0, z=0.0, a=None, v=None):
        trsf = gp_Trsf()
        if a is not None:
            ax = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(v[0], v[1], v[2]))
            trsf.SetRotation(ax, math.radians(a))
        else:
            if (x!=0.0):
                axx = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(1., 0., 0.))
                a_trsf1 = gp_Trsf()
                a_trsf1.SetRotation(axx, math.radians(x))
                trsf = a_trsf1*trsf

            if (y!=0.0):
                axy = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(0., 1., 0.))
                a_trsf2 = gp_Trsf()
                a_trsf2.SetRotation(axy, math.radians(y))
                trsf = a_trsf2*trsf

            if (z!=0.0):
                axz = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(0., 0., 1.))
                a_trsf3 = gp_Trsf()
                a_trsf3.SetRotation(axz, math.radians(z))
                trsf = a_trsf3*trsf

        for c in self.children:
            c.propagate_trsf(trsf)

    def display_wireframe(self):
        self.display_mode = DISP_MODE_WIREFRAME
        for c in self.children:
            c.propagate_display_mode(self.display_mode)

    def translate(self, x=0.0, y=0.0, z=0.0):
        v = gp_Vec(x, y, z)
        a_trsf = gp_Trsf()
        a_trsf.SetTranslation(v)
        for c in self.children:
            c.propagate_trsf(a_trsf)

    def mirror(self, x=0.0, y=0.0, z=0.0):
        debug("mirror: %f, %f, %f"%(x,y,z))
        if (x==0 and y==0 and z==0):
            return
        trsf = gp_Trsf()

        d = gp_Dir(x, y, z)
        p = gp_Pnt(0,0,0)
        trsf.SetMirror(gp_Ax2(p, d))
        for c in self.children:
            c.propagate_trsf(trsf)

    def scale(self, x=1.0, y=1.0, z=1.0):
        debug("scale: %f, %f, %f"%(x,y,z))
        if x == y == z:
            trsf = gp_Trsf()
            trsf.SetScale(gp_Pnt(0,0,0), x)
            for c in self.children:
                c.propagate_trsf(trsf)
        else:
            gtrsf = gp_GTrsf()
            gtrsf.SetVectorialPart(gp_Mat(
                x, 0, 0,
                0, y, 0,
                0, 0, z
            ))
            for c in self.children:
                c.propagate_gtrsf(gtrsf)

    def cube(self, xyz, center=False):

        # Бесшумная санитария размеров
        try:
            x = float(xyz.x())
            y = float(xyz.y())
            z = float(xyz.z())
        except Exception:
            return # Если координаты не числовые, тихо пропускаем

        # По стандарту OpenSCAD: если любой размер куба <= 0, он просто не создается
        if x <= 0.0 or y <= 0.0 or z <= 0.0:
            return

        cube_shape = BRepPrimAPI_MakeBox(xyz.x(), xyz.y(), xyz.z()).Shape()

        scls = SCLShape(cube_shape)
        if (center):
            debug("center cube")
            trsf = gp_Trsf()
            trsf.SetTranslation(gp_Vec(-xyz.x()/2.0, -xyz.y()/2.0, -xyz.z()/2.0))
            scls.transform(trsf)
        sclp = SCLPart3(self)
        sclp.set_shape(scls)
        name = get_inc_name("cube")
        sclp.set_name(name)
        debug("Creating cube %s"%(name,))
        self.add_child_context(sclp)

    def polygon(self, points, paths, fname, line):
        p = SCLProfile2(self)
        if paths == []:
            paths = [i for i in range(len(points))]
        for e, pth in enumerate(paths):
            start = int(paths[e-1])
            end = int(paths[e])
            if ((start>=len(points)) or (start<0)):
                raise ContextError("Point #%i start index (%i) is out of range"%(e, start), fname, line)
            if ((end>=len(points)) or (end<0)):
                raise ContextError("Point #%i end index (%i) is out of range"%(e, end), fname, line)

            ps = points[start]
            pe = points[end]
            if (len(ps)<2):
                raise ContextError("Element with index #%i is not a point: need at array of 2 elements, got %s"%(start, str(ps)), fname, line)
            if (len(pe)<2):
                raise ContextError("Element with index #%i is not a point: need at array of 2 elements, got %s"%(end, str(pe)), fname, line)
            v2s = V3(ps[0], ps[1], 0)
            v2e = V3(pe[0], pe[1], 0)
            p.line(v2s, v2e)
        self.add_child_context(p)
        p.set_name(get_inc_name("polygon"))
        self.add_profile_shape()


    def get_face(self):
        if (self.profile is not None):
            return self.profile.get_face()

        if self.shape is not None:

            shape_occ = self.shape.get_shape()
            # Если форма сама является гранью
            if shape_occ.ShapeType() == TopAbs_FACE:
                return topods.Face(shape_occ)

            # --- ИСПРАВЛЕННЫЙ БЛОК: Интеллектуальный поиск граней внутри сложных контейнеров ---
            # Если форма — это Compound (сборка), проверяем, содержит ли она грани внутри
            faces = list(TopologyExplorer(shape_occ).faces())
            if len(faces) > 0:
                return faces[0]
            # ----------------------------------------------------------------------------------

        for c in self.children:
            if hasattr(c, 'get_face'):
                f = c.get_face()
                if f is not None:
                    return f
        warning("Part contains no profile")
        return None

    def get_wire(self):
        if (self.profile is not None):
            return self.profile.get_wire()

        if self.shape is not None:

            shape_occ = self.shape.get_shape()

            # --- ИСПРАВЛЕННЫЙ БЛОК: Интеллектуальный поиск контуров ---
            # Собираем вообще все замкнутые контуры (Wires) из нашей геометрии
            wires = list(TopologyExplorer(shape_occ).wires())
            if len(wires) > 0:
                if len(wires) == 1:
                    return wires[0]
                else:
                    # Если контуров несколько (например, внешняя граница и отверстие),
                    # упаковываем их в один общий контейнер (Compound)
                    builder = BRep_Builder()
                    comp = TopoDS_Compound()
                    builder.MakeCompound(comp)
                    for w in wires:
                        builder.Add(comp, w)
                    return comp
            # -----------------------------------------------------------

        for c in self.children:
            if hasattr(c, 'get_wire'):
                w = c.get_wire()
                if w is not None:
                    return w
        warning("Part contains no profile")
        return None



    def cylinder(self, r, r1, r2, d, d1, d2, h, center=False, fn=0):
        nr1 = None
        nr2 = None
        nh = None
        if (not is_var_set(d)) and is_var_set(r):
            nr1 = r
            nr2 = r
        elif is_var_set(d) and (not is_var_set(r)):
            nr1 = d/2.0
            nr2 = d/2.0
        elif is_var_set(d) and is_var_set(r):
            nr1 = d/2.0
            nr2 = d/2.0
        elif (not is_var_set(d)) and (not is_var_set(r)) and (is_var_set(r1) and is_var_set(r2)):
            nr1 = r1
            nr2 = r2
        elif (not is_var_set(d)) and (not is_var_set(r)) and (is_var_set(d1) and is_var_set(d2)):
            nr1 = d1/2.0
            nr2 = d2/2.0
        else:
            nr1 = 0.5
            nr2 = 0.5

        nh = 1.0
        if is_var_set(h):
            nh = h

        # Бесшумная санация параметров цилиндра/конуса
        try:
            val_nh = float(nh) if nh is not None else 0.0
            val_nr1 = float(nr1) if nr1 is not None else 0.0
            val_nr2 = float(nr2) if nr2 is not None else 0.0
        except Exception:
            return

        # 1. Если высота нулевая или отрицательная — пропускаем
        if val_nh <= 0.0:
            return

        # 2. Если хотя бы один радиус отрицательный — пропускаем
        if val_nr1 < 0.0 or val_nr2 < 0.0:
            return

        # 3. Если оба радиуса нулевые — пропускаем
        if val_nr1 == 0.0 and val_nr2 == 0.0:
            return

        # При $fn > 0 строим правильный многоугольник (призму/пирамиду)
        if fn > 0:
            n = max(3, int(fn))
            da = 2.0 * math.pi / n

            r1v = val_nr1
            r2v = val_nr2

            bottom_pts = [gp_Pnt(r1v * math.cos(i * da), r1v * math.sin(i * da), 0.0) for i in range(n)]
            top_pts    = [gp_Pnt(r2v * math.cos(i * da), r2v * math.sin(i * da), val_nh) for i in range(n)]

            from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid
            from OCC.Core.BRepLib import breplib

            faces = []

            # Нижняя крышка
            if r1v > 0:
                w = BRepBuilderAPI_MakePolygon()
                for p in reversed(bottom_pts):
                    w.Add(p)
                w.Close()
                f = BRepBuilderAPI_MakeFace(w.Wire())
                if f.IsDone():
                    faces.append(f.Face())

            # Верхняя крышка
            if r2v > 0:
                w = BRepBuilderAPI_MakePolygon()
                for p in top_pts:
                    w.Add(p)
                w.Close()
                f = BRepBuilderAPI_MakeFace(w.Wire())
                if f.IsDone():
                    faces.append(f.Face())

            # Боковые грани
            apex_bottom = gp_Pnt(0, 0, 0)
            apex_top    = gp_Pnt(0, 0, val_nh)
            for i in range(n):
                j = (i + 1) % n
                w = BRepBuilderAPI_MakePolygon()
                if r1v > 0:
                    w.Add(bottom_pts[i])
                    w.Add(bottom_pts[j])
                else:
                    # r1v == 0 — пирамида, вершина внизу
                    w.Add(apex_bottom)
                if r2v > 0:
                    w.Add(top_pts[j])
                    w.Add(top_pts[i])
                else:
                    # r2v == 0 — пирамида, вершина вверху
                    w.Add(apex_top)
                w.Close()
                f = BRepBuilderAPI_MakeFace(w.Wire())
                if f.IsDone():
                    faces.append(f.Face())

            # Сшивка → Solid
            sewing = BRepBuilderAPI_Sewing()
            for f in faces:
                sewing.Add(f)
            sewing.Perform()
            sewed = sewing.SewedShape()
            shells = list(TopologyExplorer(sewed).shells())
            if not shells:
                return
            solid_builder = BRepBuilderAPI_MakeSolid(shells[0])
            if not solid_builder.IsDone():
                return
            solid = solid_builder.Solid()
            breplib.OrientClosedSolid(solid)

            scls = SCLShape(solid)
            if center:
                trsf = gp_Trsf()
                trsf.SetTranslation(gp_Vec(0, 0, -val_nh / 2.0))
                scls.transform(trsf)
            sclp = SCLPart3(self)
            sclp.set_shape(scls)
            name = get_inc_name("cylinder")
            sclp.set_name(name)
            debug("Creating $fn prism %s (n=%d)" % (name, n))
            self.add_child_context(sclp)
            return

        ax = gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1))
        s = None
        if nr1==nr2:
            s = BRepPrimAPI_MakeCylinder(ax, nr1, nh).Shape()
        else:
            s = BRepPrimAPI_MakeCone(ax, nr1, nr2, nh).Shape()
        scls = SCLShape(s)
        if (center):
            debug("center cylinder")
            trsf = gp_Trsf()
            trsf.SetTranslation(gp_Vec(0, 0, -val_nh / 2.0))
            scls.transform(trsf)
        sclp = SCLPart3(self)
        sclp.set_shape(scls)
        name = get_inc_name("cylinder")
        sclp.set_name(name)
        debug("Creating cylinder %s"%(name,))
        self.add_child_context(sclp)

    def sphere(self, r, d):
        # По умолчанию радиус равен 1.0
        radius = 1.0

        # Если задан диаметр, радиус равен d/2. Иначе, если задан r, берем r.
        if is_var_set(d):
            radius = d / 2.0
        elif is_var_set(r):
            radius = r

        # Бесшумная санитария параметров сферы
        try:
            val_r = float(radius)
        except Exception:
            return

        # Если радиус нулевой или отрицательный — тихо пропускаем построение
        if val_r <= 0.0:
            return

        # Строим сферу средствами OpenCASCADE
        sphere_shape = BRepPrimAPI_MakeSphere(val_r).Shape()

        scls = SCLShape(sphere_shape)
        sclp = SCLPart3(self)
        sclp.set_shape(scls)
        name = get_inc_name("sphere")
        sclp.set_name(name)
        debug("Creating sphere %s"%(name,))
        self.add_child_context(sclp)

    def circle(self, r, d):
        # По умолчанию радиус равен 1.0
        radius = 1.0
        if is_var_set(d):
            radius = d / 2.0
        elif is_var_set(r):
            radius = r

        # Бесшумная санитария
        try:
            val_r = float(radius)
        except Exception:
            return

        if val_r <= 0.0:
            return

        # Строим идеальный плоский круг на плоскости Z=0
        ax = gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
        circ = gp_Circ(ax, val_r)
        edge = BRepBuilderAPI_MakeEdge(circ).Edge()
        wire = BRepBuilderAPI_MakeWire(edge).Wire()
        face = BRepBuilderAPI_MakeFace(wire).Face()

        scls = SCLShape(face)
        sclp = SCLPart3(self)
        sclp.set_shape(scls)
        name = get_inc_name("part")
        sclp.set_name(name)
        debug("Creating circle %s"%(name,))
        self.add_child_context(sclp)

    def polyhedron(self, points, faces, fname, line):
        from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace, BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid
        from OCC.Core.BRepLib import breplib

        occ_faces = []
        # Допуск для слияния точек (1 микрон)
        TOLERANCE = 1e-9

        for face_idx, f_indices in enumerate(faces):
            # 1. Переводим Clockwise (OpenSCAD) в Counter-Clockwise (OpenCASCADE)
            occ_indices = f_indices[::-1]

            # 2. Фильтрация дубликатов по физическому расстоянию
            clean_points = []
            for idx_val in occ_indices:
                idx = int(idx_val)
                if idx >= len(points) or idx < 0:
                    raise ContextError("Point index %i is out of range in face %i"%(idx, face_idx), fname, line)

                # Создаем точку OCC
                pt = gp_Pnt(
                    float(points[idx][0]) if points[idx][0] is not None else 0.0,
                    float(points[idx][1]) if points[idx][1] is not None else 0.0,
                    float(points[idx][2]) if points[idx][2] is not None else 0.0,
                )

                # Проверяем расстояние до предыдущей точки
                if not clean_points or clean_points[-1].Distance(pt) > TOLERANCE:
                    clean_points.append(pt)

            # Если контур закрыт вручную (последняя точка совпадает с первой)
            if len(clean_points) > 1 and clean_points[0].Distance(clean_points[-1]) <= TOLERANCE:
                clean_points.pop()

            # Для построения грани нужно как минимум 3 уникальные точки
            if len(clean_points) < 3:
#                warning("Face %i has fewer than 3 unique vertices after cleaning. Skipping." % face_idx)
                continue

            # 3. Строим плоский замкнутый полигон
            poly = BRepBuilderAPI_MakePolygon()
            for pt in clean_points:
                poly.Add(pt)

            # Безопасно пытаемся закрыть полигон
            try:
                poly.Close()
            except Exception:
                #warning("Exception closing polygon for face %i: %s. Skipping." % (face_idx, str(e)))
                continue

            if not poly.IsDone():
                #warning("Failed to build polygon for face %i (NotDone). Skipping." % face_idx)
                continue

            # Превращаем полигон в плоскую грань (Face)
            wire = poly.Wire()
            face_builder = BRepBuilderAPI_MakeFace(wire)
            if face_builder.IsDone():
                occ_faces.append(face_builder.Face())
            #else:
            #   warning("Failed to build face for face %i (NotDone). Skipping." % face_idx)

        if not occ_faces:
            warning("No valid faces found for polyhedron")
            return

        # Сшиваем все грани в одну общую оболочку
        sewing = BRepBuilderAPI_Sewing()
        for face in occ_faces:
            sewing.Add(face)
        sewing.Perform()
        sewed_shape = sewing.SewedShape()

        solid = None
        # Извлекаем оболочку из контейнера
        shells = list(TopologyExplorer(sewed_shape).shells())
        if len(shells) > 0:
            shell = shells[0]
            # Превращаем оболочку в монолитное твердое тело
            solid_builder = BRepBuilderAPI_MakeSolid(shell)
            if solid_builder.IsDone():
                solid = solid_builder.Solid()

                # Дополнительно применяем автокоррекцию (на случай мелких ошибок в самом scad-файле)
                breplib.OrientClosedSolid(solid)
#                debug("Polyhedron successfully built as a SOLID and oriented")
            else:
                solid = sewed_shape
#                debug("BRepBuilderAPI_MakeSolid failed. Using sewed shape.")
        else:
            solid = sewed_shape
#            debug("No shells found in sewed shape. Using raw sewed shape.")

        scls = SCLShape(solid)
        sclp = SCLPart3(self)
        sclp.set_shape(scls)
        name = get_inc_name("part")
        sclp.set_name(name)
        self.add_child_context(sclp)


    def read_step(self, filename):
        """ Returns list of tuples (topods_shape, label, color)
        Use OCAF.
        """
        if not os.path.isfile(filename):
            raise FileNotFoundError("%s not found." % filename)
        debug("Loading %s"%(filename,))
        # the list:
        output_shapes = {}

        # create an handle to a document
        doc = TDocStd_Document("pythonocc-doc")

        # Get root assembly
        shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main())
        color_tool = XCAFDoc_DocumentTool.ColorTool(doc.Main())
        #layer_tool = XCAFDoc_DocumentTool.LayerTool(doc.Main())
        #mat_tool = XCAFDoc_DocumentTool.MaterialTool(doc.Main())

        step_reader = STEPCAFControl_Reader()
        step_reader.SetColorMode(True)
        step_reader.SetLayerMode(True)
        step_reader.SetNameMode(True)
        step_reader.SetMatMode(True)
        step_reader.SetGDTMode(True)

        debug("Read step file")
        status = step_reader.ReadFile(filename)
        if status == IFSelect_RetDone:
            step_reader.Transfer(doc)

        locs = []

        def _get_sub_shapes(lab, loc):
            #global cnt, lvl
            #cnt += 1
            #print("\n[%d] level %d, handling LABEL %s\n" % (cnt, lvl, _get_label_name(lab)))
            #print()
            #print(lab.DumpToString())
            #print()
            #print("Is Assembly    :", shape_tool.IsAssembly(lab))
            #print("Is Free        :", shape_tool.IsFree(lab))
            #print("Is Shape       :", shape_tool.IsShape(lab))
            #print("Is Compound    :", shape_tool.IsCompound(lab))
            #print("Is Component   :", shape_tool.IsComponent(lab))
            #print("Is SimpleShape :", shape_tool.IsSimpleShape(lab))
            #print("Is Reference   :", shape_tool.IsReference(lab))

            #users = TDF_LabelSequence()
            #users_cnt = shape_tool.GetUsers(lab, users)
            #print("Nr Users       :", users_cnt)

            l_subss = TDF_LabelSequence()
            shape_tool.GetSubShapes(lab, l_subss)
            #print("Nb subshapes   :", l_subss.Length())
            l_comps = TDF_LabelSequence()
            shape_tool.GetComponents(lab, l_comps)
            #print("Nb components  :", l_comps.Length())
            #print()
            name = lab.GetLabelName()
            debug("Name : %s"%(name,))

            if shape_tool.IsAssembly(lab):
                l_c = TDF_LabelSequence()
                shape_tool.GetComponents(lab, l_c)
                for i in range(l_c.Length()):
                    label = l_c.Value(i+1)
                    if shape_tool.IsReference(label):
                        #print("\n########  reference label :", label)
                        label_reference = TDF_Label()
                        shape_tool.GetReferredShape(label, label_reference)
                        loc = shape_tool.GetLocation(label)
                        #print("    loc          :", loc)
                        #trans = loc.Transformation()
                        #print("    tran form    :", trans.Form())
                        #rot = trans.GetRotation()
                        #print("    rotation     :", rot)
                        #print("    X            :", rot.X())
                        #print("    Y            :", rot.Y())
                        #print("    Z            :", rot.Z())
                        #print("    W            :", rot.W())
                        #tran = trans.TranslationPart()
                        #print("    translation  :", tran)
                        #print("    X            :", tran.X())
                        #print("    Y            :", tran.Y())
                        #print("    Z            :", tran.Z())

                        locs.append(loc)
                        #print(">>>>")
                        #lvl += 1
                        _get_sub_shapes(label_reference, loc)
                        #lvl -= 1
                        #print("<<<<")
                        locs.pop()

            elif shape_tool.IsSimpleShape(lab):
                #print("\n########  simpleshape label :", lab)
                shape = shape_tool.GetShape(lab)

                if shape.IsNull():
                    return
                #print("    all ass locs   :", locs)

                loc = TopLoc_Location()
                for ln in locs:
                    loc = loc.Multiplied(ln)

                #trans = loc.Transformation()
                #print("    FINAL loc    :")
                #print("    tran form    :", trans.Form())
                #rot = trans.GetRotation()
                #print("    rotation     :", rot)
                #print("    X            :", rot.X())
                #print("    Y            :", rot.Y())
                #print("    Z            :", rot.Z())
                #print("    W            :", rot.W())
                #tran = trans.TranslationPart()
                #print("    translation  :", tran)
                #print("    X            :", tran.X())
                #print("    Y            :", tran.Y())
                #print("    Z            :", tran.Z())
                c = Quantity_ColorRGBA(0.5, 0.5, 0.5, 1.0)  # default color (gray, opaque)
                colorSet = False
                if (color_tool.GetInstanceColor(shape, 0, c) or
                        color_tool.GetInstanceColor(shape, 1, c) or
                        color_tool.GetInstanceColor(shape, 2, c)):
                    color_tool.SetInstanceColor(shape, 0, c)
                    color_tool.SetInstanceColor(shape, 1, c)
                    color_tool.SetInstanceColor(shape, 2, c)
                    colorSet = True

                if not colorSet:
                    if (color_tool.GetColor(shape, 0, c) or
                            color_tool.GetColor(shape, 1, c) or
                            color_tool.GetColor(shape, 2, c)):

                        color_tool.SetInstanceColor(shape, 0, c)
                        color_tool.SetInstanceColor(shape, 1, c)
                        color_tool.SetInstanceColor(shape, 2, c)

                shape_disp = BRepBuilderAPI_Transform(shape, loc.Transformation()).Shape()
                if shape_disp not in output_shapes:
                    output_shapes[shape_disp] = [name, lab.GetLabelName(), c]
                for i in range(l_subss.Length()):
                    lab_subs = l_subss.Value(i+1)
                    #print("\n########  simpleshape subshape label :", lab)
                    shape_sub = shape_tool.GetShape(lab_subs)
                    if shape_sub.IsNull():
                        continue

                    c = Quantity_ColorRGBA(0.5, 0.5, 0.5, 1.0)  # default color (gray, opaque)
                    colorSet = False
                    if (color_tool.GetInstanceColor(shape_sub, 0, c) or
                            color_tool.GetInstanceColor(shape_sub, 1, c) or
                            color_tool.GetInstanceColor(shape_sub, 2, c)):
                        color_tool.SetInstanceColor(shape_sub, 0, c)
                        color_tool.SetInstanceColor(shape_sub, 1, c)
                        color_tool.SetInstanceColor(shape_sub, 2, c)
                        colorSet = True

                    if not colorSet:
                        if (color_tool.GetColor(shape_sub, 0, c) or
                                color_tool.GetColor(shape_sub, 1, c) or
                                color_tool.GetColor(shape_sub, 2, c)):
                            color_tool.SetInstanceColor(shape, 0, c)
                            color_tool.SetInstanceColor(shape, 1, c)
                            color_tool.SetInstanceColor(shape, 2, c)
                    shape_to_disp = BRepBuilderAPI_Transform(shape_sub, loc.Transformation()).Shape()
                    # position the subshape to display
                    if shape_to_disp not in output_shapes:
                        output_shapes[shape_to_disp] = [name, lab_subs.GetLabelName(), c]


        def _get_shapes():
            debug("Get shapes")
            labels = TDF_LabelSequence()
            shape_tool.GetFreeShapes(labels)
            #global cnt
            #cnt += 1

            debug("Number of shapes at root : %s"%(str(labels.Length(),)))
            for i in range(labels.Length()):
                root_item = labels.Value(i+1)
                debug("Get sub shapes")
                _get_sub_shapes(root_item, None)
            debug("Get shapes done")
        _get_shapes()
        return output_shapes


    def import_file(self, filename):
        ext = os.path.splitext(filename)[1].lower()
        if ext in ('.step', '.stp'):
            shapes_labels_colors = self.read_step(filename)
            prev_name = None
            part = None
            for shpt_lbl_color in shapes_labels_colors:
                name, label, c = shapes_labels_colors[shpt_lbl_color]
                debug("name: %s, label: %s, shpt_lbl_color: %s"%(str(name), str(label), str(shpt_lbl_color),))
                current_part = None
                if prev_name != name:
                    prev_name = name
                    scls = SCLShape(shpt_lbl_color)
                    rgb = c.GetRGB()
                    scls.color([rgb.Red(), rgb.Green(), rgb.Blue(), c.Alpha()])
                    part = SCLPart3(self)
                    part.set_shape(scls)
                    part.set_name(name)
                    current_part = part
                else:
                    scls = SCLShape(shpt_lbl_color)
                    rgb = c.GetRGB()
                    scls.color([rgb.Red(), rgb.Green(), rgb.Blue(), c.Alpha()])
                    sclp = SCLPart3(part)
                    sclp.set_shape(scls)
                    sclp.set_name(name)
                    current_part = sclp
                debug("Creating step %s"%(name,))
                self.add_child_context(current_part)
        elif ext == '.stl':
            stl_shp = read_stl_file(filename)
            scls = SCLShape(stl_shp)
            sclp = SCLPart3(self)
            sclp.set_shape(scls)
            name = get_inc_name("stl")
            sclp.set_name(name)
            debug("Creating stl %s"%(name,))
            self.add_child_context(sclp)
        else:
            warning("Import format not supported: %s" % ext)

    def display(self, writer=None):
        debug("Display SCLPart3 %s"%(self.name,))
        objects = []
        if self.shape is None:
            for c in self.children:
                objects.append(c.display(writer))
        objects = {'name': self.name, 'children': objects}

        if self.shape is not None:
            self.shape.display(writer)

        debug("end SCLPart3")
        return objects

    def start_display(self, writer=None):
        if (writer is None):
            if (self.parent is None):
                Singleton.sd.display.FitAll()
                Singleton.sd.start_display()

class SCLProfile2(SCLContext):
    def __init__(self, parent):
        super().__init__(parent)
        self.edges = []
        self.wire = None
        self.trsf = gp_Trsf()
        self.face = None
        self.current_pos = None
        self.start_pos = None

    def move(self, p):
        self.current_pos = p
        self.start_pos = p

    def line(self, *args):
        if len(args) == 1:
            p = args[0]
            if self.current_pos is None:
                self.current_pos = p
                self.start_pos = p
                return
            self.push_element(Line3(self.current_pos, p))
            self.current_pos = p
        elif len(args) == 2:
            sp, dp = args
            self.push_element(Line3(sp, dp))
            self.current_pos = dp
        if self.start_pos is None:
            self.start_pos = self.current_pos

    def arc(self, center, angle):
        if self.current_pos is None:
            warning("arc: no current position, call move() or line() first")
            return
        self.push_element(Arc3(self.current_pos, center, angle))
        self.current_pos = self.edges[-1].end_point()

    def spline(self, points):
        if not points:
            return
        if self.current_pos is not None:
            pts = [self.current_pos] + list(points)
        else:
            pts = list(points)
        self.push_element(Spline3(pts))
        self.current_pos = pts[-1]
        if self.start_pos is None:
            self.start_pos = pts[0]

    def fillet(self, radius):
        self.push_element(Fillet2(radius))

    def close(self, fillet_radius=None):
        if self.current_pos is None or self.start_pos is None:
            warning("close: no contour to close")
            return
        dx = self.current_pos.x() - self.start_pos.x()
        dy = self.current_pos.y() - self.start_pos.y()
        dz = self.current_pos.z() - self.start_pos.z()
        if dx*dx + dy*dy + dz*dz < 1e-14:
            return
        if fillet_radius is not None:
            self.push_element(Line3(self.current_pos, self.start_pos))
            self.push_element(Fillet2(fillet_radius))
        else:
            self.push_element(Line3(self.current_pos, self.start_pos))
        self.current_pos = self.start_pos

    def push_element(self, e):
        self.edges.append(e)

    # def rotate(self, x=0.0, y=0.0, z=0.0):
    #     if (x!=0.0):
    #         axx = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(1., 0., 0.))
    #         a_trsf1 = gp_Trsf()
    #         a_trsf1.SetRotation(axx, math.radians(x))
    #         self.trsf = self.trsf*a_trsf1

    #     if (y!=0.0):
    #         axy = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(0., 1., 0.))
    #         a_trsf2 = gp_Trsf()
    #         a_trsf2.SetRotation(axy, math.radians(y))
    #         self.trsf = self.trsf*a_trsf2

    #     if (z!=0.0):
    #         axz = gp_Ax1(gp_Pnt(0., 0., 0.), gp_Dir(0., 0., 1.))
    #         a_trsf3 = gp_Trsf()
    #         a_trsf3.SetRotation(axz, math.radians(z))
    #         self.trsf = self.trsf*a_trsf3

    # def mirror(self, x=0.0, y=0.0, z=0.0):
    #     if (x==0 and y== 0 and z==0):
    #         warning("Warning: no axis to mirror")
    #         return
    #     trsf = gp_Trsf()

    #     d = gp_Dir(x, y, z)
    #     p = gp_Pnt()
    #     trsf.SetMirror(gp_Ax1(p, d))
    #     self.trsf = self.trsf*trsf

    def transform(self, trsf):
        self.wire = topods.Wire(BRepBuilderAPI_Transform(self.get_wire(), trsf, True).Shape())

    def apply_trsf(self, trsf):
        self.transform(trsf)

    def get_wire(self):
        if (self.wire is None):
            occ_edges = []
            n = len(self.edges)
            for i, e in enumerate(self.edges):
                o = "Element %i/%i: "%(i, n)
                if isinstance(e, Line3):
                    edge = e.to_edge()
                    if edge is not None:
                        occ_edges.append(edge)
                elif isinstance(e, Fillet2):
                    prev = self.edges[i-1]
                    nxt = self.edges[i+1] if i+1 < n else self.edges[0]
                    try:
                        edge = e.to_edge(prev, nxt)
                        if edge is not None:
                            occ_edges.append(edge)
                    except Exception:
                        warning("fillet %i/%i skipped: edges have no common point" % (i, n))
                elif isinstance(e, Arc3):
                    edge = e.to_edge()
                    if edge is not None:
                        occ_edges.append(edge)
                elif isinstance(e, Spline3):
                    edge = e.to_edge()
                    if edge is not None:
                        occ_edges.append(edge)
                o += repr(e)
                debug(o)
            if occ_edges == []:
                return None
            self.wire = make_wire(occ_edges)
            trsf_wire = BRepBuilderAPI_Transform(self.wire, self.trsf)
            trsf_shape = trsf_wire.Shape()
            self.wire = topods.Wire(trsf_shape)
            self.face = None
        return self.wire

    def get_face(self):
        if (self.face is None):
            try:
                w = self.get_wire()
                if w is None:
                    return None
                self.face = BRepBuilderAPI_MakeFace(w)
            except Exception:
                warning("profile: failed to build wire face")
                return None
        return self.face

    def display(self, writer=None):
        debug("Display SCLProfile2")
        w = self.get_wire()
        objects = {'name': self.name, 'children': None}
        if (w is not None):
            Singleton.sd.display.DisplayShape(w, writer)
        debug("end SCLProfile2")
        return objects


def make_helix_wire(radius, height, twist_deg, num_points=100):
    """ Генерирует винтовую линию (спираль) в виде TopoDS_Wire """
    from OCC.Core.TColgp import TColgp_HArray1OfPnt  # <--- Используем H-класс (Handle)

    pts = TColgp_HArray1OfPnt(1, num_points)
    twist_rad = math.radians(twist_deg)

    for i in range(1, num_points + 1):
        t = (i - 1) / (num_points - 1)
        angle = t * twist_rad
        x = radius * math.cos(angle)
        y = radius * math.sin(angle)
        z = t * height
        pts.SetValue(i, gp_Pnt(x, y, z))

    # Интерполируем точки в плавную сплайновую кривую
    interpolator = GeomAPI_Interpolate(pts, False, 0.0001)
    interpolator.Perform()
    curve = interpolator.Curve()

    # Строим ребро и контур
    edge = BRepBuilderAPI_MakeEdge(curve).Edge()
    wire = BRepBuilderAPI_MakeWire(edge).Wire()
    return wire


class SCLExtrude(SCLContext):
    def __init__(self, parent):
        super().__init__(parent)
        self.shape = None

    def apply_trsf(self, trsf):
        if self.shape is not None:
            self.shape.transform(trsf)



    def linear_extrude(self, height=1.0, twist=0.0):
        # Копируем список детей, чтобы безопасно очистить оригинальный список
        original_children = list(self.children)
        self.children = []

        for c in original_children:
            if twist == 0.0:
                # Прямое выдавливание (требует Face)
                f = c.get_face()
                if f is None:
                    continue
                prism_vec = gp_Vec(0, 0, height)

                face_shape = f.Face() if hasattr(f, 'Face') else f
                shape = BRepPrimAPI_MakePrism(face_shape, prism_vec).Shape()
            else:
                # Винтовое выдавливание (требует Wire)
                f = c.get_face()
                if f is None:
                    continue
                face_shape = f.Face() if hasattr(f, 'Face') else f

                # Извлекаем все проволоки из грани (внешняя + отверстия)
                wires = []
                exp_w = TopExp_Explorer(face_shape, TopAbs_WIRE)
                while exp_w.More():
                    wires.append(topods.Wire(exp_w.Current()))
                    exp_w.Next()
                if not wires:
                    continue

                # Строим прямой позвоночник вдоль оси Z
                spine_edge = BRepBuilderAPI_MakeEdge(gp_Pnt(0,0,0), gp_Pnt(0,0,height)).Edge()
                spine_wire = BRepBuilderAPI_MakeWire(spine_edge).Wire()

                # Вычисляем радиус спирали из габаритов сечения
                bbox = Bnd_Box()
                brepbndlib.Add(face_shape, bbox)
                xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
                diag = math.sqrt((xmax - xmin)**2 + (ymax - ymin)**2 + (zmax - zmin)**2)
                helix_radius = max(diag * 0.5, 1.0)

                # Строим направляющую спираль
                guide_wire = make_helix_wire(helix_radius, height, twist)

                # Заметаем внешнюю проволоку, затем вычитаем отверстия
                result_shape = None
                for i, wire in enumerate(wires):
                    pipe = BRepOffsetAPI_MakePipeShell(spine_wire)
                    pipe.SetMode(guide_wire, False)
                    pipe.Add(wire)
                    pipe.Build()
                    if not pipe.IsDone():
                        continue
                    pipe.MakeSolid()
                    s = pipe.Shape()
                    if s is None or s.IsNull():
                        continue
                    if i == 0:
                        result_shape = s
                    else:
                        cutter = _bool_op(BRepAlgoAPI_Cut, [result_shape, s])
                        if cutter.IsDone():
                            result_shape = cutter.Shape()
                if result_shape is None:
                    continue
                shape = result_shape

            scls = SCLShape(shape)
            sclp = SCLPart3(self)
            sclp.set_shape(scls)
            name = get_inc_name("extrusion")
            sclp.set_name(name)
            debug("Creating extrusion %s"%(name,))
            self.add_child_context(sclp)


    def display(self, writer=None):
        debug("Display SCLExtrude")
        debug("Children: %s"%(repr(self.children),))
        objects = []
        for c in self.children:
            objects.append(c.display(writer))
        objects = {'name': self.name, 'children': objects}
        
        if self.shape is not None:
            self.shape.display(writer)
        debug("end SCLExtrude")
        return objects


class SCLHull(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def hull(self):
        import numpy as np
        import math
        try:
            from scipy.spatial import ConvexHull
        except ImportError:
            warning("scipy is required for hull(). Please run: pip install scipy")
            return

        from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
        from OCC.Core.TopLoc import TopLoc_Location
        from OCC.Core.BRep import BRep_Tool
        from OCC.Extend.TopologyUtils import TopologyExplorer

        # --- ГЕНЕРИРУЕМ WARNING О ПОТЕРЕ КАЧЕСТВА ---
        warning("%s: Warning: hull() performed with quality loss (geometry converted to faceted polyhedron)" % (self.name,))

        children = self.get_children()

        # Собираем реальные Python-обертки (SCLShape)
        scl_shapes = []
        for c in children:
            if c.shape is not None:
                scl_shapes.append(c.shape)

        if not scl_shapes:
            self.children = []
            return

        # --- ЧИТАЕМ ПЕРЕМЕННЫЕ КАЧЕСТВА ИЗ КОНТЕКСТА ---
        fa = 12.0
        fe = 0.5
        try:
            fa = float(Singleton.scl.find_variable_value('$fa', 0))
        except Exception:
            pass
        try:
            fe = float(Singleton.scl.find_variable_value('$fe', 0))
        except Exception:
            pass

        # Переводим $fa (минимальный угол в градусах) в радианы для OpenCASCADE
        ang_deflection = math.radians(fa) if fa > 0 else 0.5
        lin_deflection = fe if fe > 0 else 0.5

        points_list = []
        for scls in scl_shapes:
            s = scls.get_shape()

            # Триангулируем формы с динамически вычисленной точностью!
            BRepMesh_IncrementalMesh(s, lin_deflection, False, ang_deflection, True)

            has_faces = False
            for face in TopologyExplorer(s).faces():
                has_faces = True
                loc = TopLoc_Location()
                tri_handle = BRep_Tool.Triangulation(face, loc)
                if tri_handle:
                    for i in range(1, tri_handle.NbNodes() + 1):
                        pt = tri_handle.Node(i)
                        pt.Transform(loc.Transformation())
                        points_list.append([pt.X(), pt.Y(), pt.Z()])

            if not has_faces:
                for v in TopologyExplorer(s).vertices():
                    pt = BRep_Tool.Pnt(v)
                    points_list.append([pt.X(), pt.Y(), pt.Z()])

        if len(points_list) < 3:
            warning("Not enough points to compute hull.")
            self.children = []
            return

        pts_array = np.array(points_list)

        z_vals = pts_array[:, 2]
        is_2d = np.allclose(z_vals, z_vals[0], atol=1e-5)

        try:
            if is_2d and abs(z_vals[0]) < 1e-5:
                hull2d = ConvexHull(pts_array[:, 0:2])
                path = hull2d.vertices.tolist()
                points_2d = pts_array[:, 0:2].tolist()
                self.polygon(points_2d, path, "hull", 0)
            else:
                hull3d = ConvexHull(pts_array, qhull_options='QJ')
                faces = hull3d.simplices.tolist()
                points_3d = pts_array.tolist()
                self.polyhedron(points_3d, faces, "hull", 0)
        except Exception as e:
            warning("ConvexHull calculation failed: " + str(e))
            self.children = []
            return

        if len(self.children) > 0 and getattr(self.children[-1], 'shape', None) is not None:
            self.shape = self.children[-1].shape
            if len(scl_shapes) > 0:
                self.shape.set_shape_color(scl_shapes[0].get_shape_color())

        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}


def collect_wires(this):
    wires = []
    for c in this.children:
        if isinstance(c, SCLProfile2):
            w = c.get_wire()
            if w is not None:
                wires.append(w)
        else:
            collected = collect_wires(c)
            if collected:
                wires += collected
            elif hasattr(c, 'get_wire'):
                w = c.get_wire()
                if w is not None:
                    wires.append(w)
    return wires

class SCLLoft(SCLContext):
    def __init__(self, parent):
        super().__init__(parent)
        self.shape = None

    def apply_trsf(self, trsf):
        if self.shape is not None:
            self.shape.transform(trsf)

    def loft(self, solid=True, ruled=True, precision=0.000001):
        wires = collect_wires(self)

        self.children = []

        if len(wires) < 2:
            warning("loft: need at least 2 profiles, got %d" % len(wires))
            return

        generator = BRepOffsetAPI_ThruSections(solid, ruled, precision)

        for w in wires:
            debug("wire: %s"%(str(w),))
            generator.AddWire(w)
        generator.Build()

        if not generator.IsDone():
            warning("loft: BRepOffsetAPI_ThruSections failed")
            return

        try:
            shape = generator.Shape()
        except RuntimeError as e:
            warning("loft: %s" % str(e))
            return
        scls = SCLShape(shape)
        sclp = SCLPart3(self)
        sclp.set_shape(scls)
        name = get_inc_name("loft")
        sclp.set_name(name)
        debug("Creating loft %s"%(name,))
        self.add_child_context(sclp)

    def display(self, writer=None):
        debug("Display SCLLoft")
        debug("Children: %s"%(repr(self.children),))
        objects = []
        for c in self.children:
            objects.append(c.display(writer))
        objects = {'name': self.name, 'children': objects}

        if self.shape is not None:
            self.shape.display(writer)
        debug("end SCLLoft")
        return objects

def _list_of_shape_to_py(lst):
    result = []
    while lst.Size() > 0:
        result.append(lst.First())
        lst.RemoveFirst()
    return result


def _map_colors(builder, sources):
    """Единый маппинг цветов от source shapes в результат булевой операции.
    builder: BRepAlgoAPI_* или BRepOffset_MakeShape с IsDeleted(),
    Generated(), Modified(). sources: [(TopoDS_Shape,
    {hash(face): ColorRGBA}), ...]. Возвращает {hash(face): ColorRGBA}
    для faces в builder.Shape()."""
    result_faces = []
    exp = TopExp_Explorer(builder.Shape(), TopAbs_FACE)
    while exp.More():
        result_faces.append(exp.Current())
        exp.Next()

    result = {}
    assigned = [False] * len(result_faces)
    result_by_hash = {hash(rf): ri for ri, rf in enumerate(result_faces)}

    for src_shape, src_colors in sources:
        if src_colors is None:
            continue
        exp = TopExp_Explorer(src_shape, TopAbs_FACE)
        while exp.More():
            src_face = exp.Current()
            color = src_colors.get(hash(src_face))
            if color is None:
                exp.Next()
                continue
            if hasattr(builder, 'IsDeleted') and builder.IsDeleted(src_face):
                exp.Next()
                continue

            for gen in _list_of_shape_to_py(builder.Generated(src_face)):
                if gen.ShapeType() != TopAbs_FACE:
                    continue
                ri = result_by_hash.get(hash(gen))
                if ri is not None and not assigned[ri]:
                    result[hash(gen)] = color
                    assigned[ri] = True

            for mod in _list_of_shape_to_py(builder.Modified(src_face)):
                if mod.ShapeType() != TopAbs_FACE:
                    continue
                ri = result_by_hash.get(hash(mod))
                if ri is not None and not assigned[ri]:
                    result[hash(mod)] = color
                    assigned[ri] = True

            for ri, rf in enumerate(result_faces):
                if not assigned[ri] and rf.IsSame(src_face):
                    result[hash(rf)] = color
                    assigned[ri] = True
                    break

            exp.Next()

    default = Quantity_ColorRGBA(Quantity_Color(Quantity_NOC_PERU), 1.0)
    for ri, rf in enumerate(result_faces):
        if not assigned[ri]:
            result[hash(rf)] = default
    return result


def _map_unified_colors(unifier, src_shape, src_colors):
    """Маппинг цветов через ShapeUpgrade_UnifySameDomain с использованием History.
    src_colors: {hash(face): ColorRGBA}. Возвращает {hash(face): ColorRGBA}
    для faces в unifier.Shape()."""
    result_sh = unifier.Shape()
    history = unifier.History()

    result_faces = []
    exp = TopExp_Explorer(result_sh, TopAbs_FACE)
    while exp.More():
        result_faces.append(exp.Current())
        exp.Next()

    result = {}
    assigned = [False] * len(result_faces)
    result_by_hash = {hash(rf): ri for ri, rf in enumerate(result_faces)}

    exp = TopExp_Explorer(src_shape, TopAbs_FACE)
    while exp.More():
        src_face = exp.Current()
        color = src_colors.get(hash(src_face))
        if color is None:
            exp.Next()
            continue

        if history.IsRemoved(src_face):
            for gen in _list_of_shape_to_py(history.Generated(src_face)):
                if gen.ShapeType() != TopAbs_FACE:
                    continue
                ri = result_by_hash.get(hash(gen))
                if ri is not None and not assigned[ri]:
                    result[hash(gen)] = color
                    assigned[ri] = True

        for mod in _list_of_shape_to_py(history.Modified(src_face)):
            if mod.ShapeType() != TopAbs_FACE:
                continue
            ri = result_by_hash.get(hash(mod))
            if ri is not None and not assigned[ri]:
                result[hash(mod)] = color
                assigned[ri] = True

        for ri, rf in enumerate(result_faces):
            if not assigned[ri] and rf.IsSame(src_face):
                result[hash(rf)] = color
                assigned[ri] = True
                break

        exp.Next()

    default = Quantity_ColorRGBA(Quantity_Color(Quantity_NOC_PERU), 1.0)
    for ri, rf in enumerate(result_faces):
        if not assigned[ri]:
            result[hash(rf)] = default
    return result


_PERU_RGBA = None
def _peru_color():
    global _PERU_RGBA
    if _PERU_RGBA is None:
        _PERU_RGBA = Quantity_ColorRGBA(Quantity_Color(Quantity_NOC_PERU), 1.0)
    return _PERU_RGBA


def _adjacency_fill_colors(shape, face_colors, blend=False):
    """Заполняет PERU-грани цветом соседних не-PERU граней (для offset).

    Multi-pass, двухфазный collect→apply.
      blend=False — mode-based: самый частый цвет среди соседей (tie по RGBA).
      blend=True  — blend: среднее RGB+A всех не-PERU соседей.
    Полностью детерминированно, порядок обхода OCCT не влияет.
    """
    if not face_colors:
        return face_colors

    peru = _peru_color()

    # early exit: all PERU, nothing to propagate
    if all(c == peru for c in face_colors.values()):
        return face_colors

    # build edge → faces map + face adjacency list
    face_list = []
    edge_faces = {}

    exp = TopExp_Explorer(shape, TopAbs_FACE)
    while exp.More():
        face = exp.Current()
        fi = len(face_list)
        face_list.append(face)

        eexp = TopExp_Explorer(face, TopAbs_EDGE)
        while eexp.More():
            eh = hash(eexp.Current())
            if eh not in edge_faces:
                edge_faces[eh] = set()
            edge_faces[eh].add(fi)
            eexp.Next()

        exp.Next()

    face_neighbors = [set() for _ in face_list]
    for faces_set in edge_faces.values():
        for fi in faces_set:
            face_neighbors[fi].update(faces_set)
    for fi in range(len(face_list)):
        face_neighbors[fi].discard(fi)

    result = dict(face_colors)

    # multi-pass, two-phase: collect → apply
    while True:
        assignments = {}
        for fi, face in enumerate(face_list):
            fh = hash(face)
            if result.get(fh) != peru:
                continue

            if blend:
                sum_r = sum_g = sum_b = sum_a = 0.0
                count = 0
                for ni in face_neighbors[fi]:
                    nc = result.get(hash(face_list[ni]))
                    if nc is not None and nc != peru:
                        rgb = nc.GetRGB()
                        sum_r += rgb.Red()
                        sum_g += rgb.Green()
                        sum_b += rgb.Blue()
                        sum_a += nc.Alpha()
                        count += 1
                if count:
                    assignments[fh] = Quantity_ColorRGBA(
                        sum_r / count, sum_g / count, sum_b / count, sum_a / count
                    )
            else:
                counts = {}
                for ni in face_neighbors[fi]:
                    nc = result.get(hash(face_list[ni]))
                    if nc is not None and nc != peru:
                        k = (nc.GetRGB().Red(), nc.GetRGB().Green(),
                             nc.GetRGB().Blue(), nc.Alpha())
                        if k in counts:
                            counts[k] = (counts[k][0] + 1, counts[k][1])
                        else:
                            counts[k] = (1, nc)
                if counts:
                    best_key = max(counts, key=lambda k: (counts[k][0], k))
                    assignments[fh] = counts[best_key][1]

        if not assignments:
            break
        for fh, color in assignments.items():
            result[fh] = color

    # fallback: isolated PERU faces (no colored neighbors at all)
    n_peru_after = sum(1 for fi in range(len(face_list))
                       if result.get(hash(face_list[fi])) == peru)
    if n_peru_after:
        fallback = next((c for c in face_colors.values() if c != peru), None)
        if fallback is not None:
            for fi, face in enumerate(face_list):
                fh = hash(face)
                if result.get(fh) == peru:
                    result[fh] = fallback

    return result


class _ColoredFaceWrapper:
    def __init__(self, rgba):
        self._rgba = rgba
    def get_face_colors(self):
        return None
    def has_face_colors(self):
        return False
    def get_default_color(self):
        return self._rgba if self._rgba is not None else Quantity_ColorRGBA(Quantity_Color(Quantity_NOC_PERU), 1.0)

def _find_context_color(ctx):
    while ctx is not None:
        if hasattr(ctx, 'shape_color') and ctx.shape_color is not None:
            return ctx.shape_color
        ctx = ctx.parent
    return None

def _extract_face_colors(wrapper, shape):
    if wrapper is not None and wrapper.has_face_colors():
        return wrapper.get_face_colors()
    color = wrapper.get_default_color() if wrapper is not None else Quantity_ColorRGBA(Quantity_Color(Quantity_NOC_PERU), 1.0)
    face_colors = {}
    exp = TopExp_Explorer(shape, TopAbs_FACE)
    while exp.More():
        face = exp.Current()
        face_colors[hash(face)] = color
        exp.Next()
    return face_colors

def _unpack_compound(shape, face_colors):
    if shape.ShapeType() != TopAbs_COMPOUND:
        if face_colors is None:
            return [(shape, {})]
        return [(shape, face_colors)]

    result = []
    it = TopoDS_Iterator(shape)
    while it.More():
        result.extend(_unpack_compound(it.Value(), face_colors))
        it.Next()
    return result


def _bool_op(op_class, args, tools=None, fuzzy=None):
    """Булевая операция через новый multi-tool API OCC.
    args:  список TopoDS_Shape для SetArguments (первый элемент) и SetTools (остальные).
    tools: если задан явно — args целиком → SetArguments, tools → SetTools.
    Возвращает построенный оператор op_class."""
    op = op_class()
    al = TopTools_ListOfShape()
    al.Append(args[0])
    op.SetArguments(al)
    tl = TopTools_ListOfShape()
    for s in (tools if tools is not None else args[1:]):
        tl.Append(s)
    op.SetTools(tl)
    if fuzzy is not None:
        op.SetFuzzyValue(fuzzy)
    op.SetUseOBB(True)
    op.SetRunParallel(True)
    op.Build()
    return op


def _fuse_parts(objects):
    """Сливает список объектов (children) в один shape через Fuse + ShapeUpgrade.

    Для каждого объекта:
      - .shape (SCLShape) → get_shape() + get_face_colors() → _unpack_compound
      - .get_face() (profile) → face + _find_context_color
      - .get_wire() (profile) → wire, без цветов
      - SCLShape напрямую → get_shape() + get_face_colors() → _unpack_compound

    Если частей <= 1 — возврат как есть.
    Returns: (TopoDS_Shape or None, {hash: ColorRGBA})
    """
    all_parts = []
    for obj in objects:
        wr = getattr(obj, 'shape', None)
        if wr is not None and hasattr(wr, 'get_shape'):
            pass
        elif isinstance(obj, SCLShape):
            wr = obj
        else:
            wr = None

        if wr is not None and hasattr(wr, 'get_shape'):
            s = wr.get_shape()
            colors = _extract_face_colors(wr, s)
            all_parts.extend(_unpack_compound(s, colors))
        elif hasattr(obj, 'get_face') and not isinstance(obj, SCLPart3):
            f = obj.get_face()
            if f is not None:
                face_shape = f.Face() if hasattr(f, 'Face') else f
                ctx = _find_context_color(obj)
                colors = {hash(face_shape): ctx} if ctx is not None else {}
                all_parts.append((face_shape, colors))
        elif hasattr(obj, 'get_wire'):
            w = obj.get_wire()
            if w is not None:
                all_parts.append((w, {}))

    if len(all_parts) == 0:
        return None, {}
    if len(all_parts) == 1:
        return all_parts[0]

    fuser = _bool_op(BRepAlgoAPI_Fuse, [p for p, _ in all_parts])
    if not fuser.IsDone() or fuser.Shape().IsNull():
        return None, {}

    fuse_colors = _map_colors(fuser, all_parts) if any(c for _, c in all_parts) else {}

    unifier = ShapeUpgrade_UnifySameDomain(fuser.Shape(), True, True, True)
    unifier.Build()
    unified_colors = _map_unified_colors(unifier, fuser.Shape(), fuse_colors)

    return unifier.Shape(), unified_colors


def _cut_parts(base_parts, cutter_shape, cutter_colors):
    """Вычитает cutter_shape из каждого элемента base_parts с сохранением цветов.
    base_parts: [(TopoDS_Shape, {hash: ColorRGBA}), ...]
    cutter_colors: {hash: ColorRGBA} или None.
    Возвращает [(TopoDS_Shape, {hash: ColorRGBA}), ...] или []."""
    cutter_parts = _unpack_compound(cutter_shape, cutter_colors)
    cutter_boxes = [get_bnd_box(s) for s, _ in cutter_parts]

    result = []
    for part_a, sub_colors_a in base_parts:
        box_a = get_bnd_box(part_a)

        overlapping = [
            (s, c) for (s, c), cb in zip(cutter_parts, cutter_boxes)
            if not box_a.IsOut(cb)
        ]
        if not overlapping:
            result.append((part_a, sub_colors_a))
            continue

        current = part_a
        current_colors = sub_colors_a
        empty = False
        for part_b, sub_colors_b in overlapping:
            cut_api = _bool_op(BRepAlgoAPI_Cut, [current, part_b])
            if cut_api.IsDone():
                res = cut_api.Shape()
                if is_shape_empty(res):
                    empty = True
                    break
                current_colors = _map_colors(cut_api, [(current, current_colors), (part_b, sub_colors_b)])
                current = res
            else:
                empty = True
                break

        if not empty:
            result.extend(_unpack_compound(current, current_colors))

    return result


class SCLGroup(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def group(self):
        children = self.get_children()

        wrappers = []
        for c in children:
            if getattr(c, 'shape', None) is not None:
                s = c.shape.get_shape()
                wrappers.append((s, c.shape))
            elif hasattr(c, 'get_face') and not isinstance(c, SCLPart3):
                f = c.get_face()
                if f is not None:
                    face_shape = f.Face() if hasattr(f, 'Face') else f
                    ctx_color = _find_context_color(c)
                    wrapper = _ColoredFaceWrapper(ctx_color) if ctx_color is not None else None
                    wrappers.append((face_shape, wrapper))

        if len(wrappers) == 1:
            s, wr = wrappers[0]
            colors = _extract_face_colors(wr, s)
            self.shape = SCLShape(s)
            self.shape.set_face_colors(colors)
        elif len(wrappers) > 1:
            builder = BRep_Builder()
            compound = TopoDS_Compound()
            builder.MakeCompound(compound)
            face_colors = {}
            for s, wr in wrappers:
                if s.ShapeType() == TopAbs_COMPOUND:
                    sub_list = _unpack_compound(s, wr.get_face_colors() if hasattr(wr, 'get_face_colors') else None)
                    for sub_s, sub_colors in sub_list:
                        builder.Add(compound, sub_s)
                        face_colors.update(sub_colors)
                else:
                    builder.Add(compound, s)
                    colors = _extract_face_colors(wr, s)
                    face_colors.update(colors)
            self.shape = SCLShape(compound)
            self.shape.set_face_colors(face_colors)

        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}


class SCLUnion(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def union(self):
        children = self.get_children()

        objects = []
        for c in children:
            if getattr(c, 'shape', None) is not None:
                objects.append(c)
            elif hasattr(c, 'get_face') and not isinstance(c, SCLPart3):
                if c.get_face() is not None:
                    objects.append(c)

        if objects:
            adjust_progress_total(1)
            push_label()
            set_progress_label("fuse")

            fused_shape, unified_colors = _fuse_parts(objects)
            if fused_shape is not None:
                self.shape = SCLShape(fused_shape)
                self.shape.set_face_colors(unified_colors)
            else:
                self.shape = None

            advance_progress()
            pop_label()

        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}


class SCLDifference(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def difference(self):
        children = self.get_children()

        wrappers = []
        for c in children:
            if getattr(c, 'shape', None) is not None:
                s = c.shape.get_shape()
                wrappers.append((s, c.shape))
            elif hasattr(c, 'get_face') and not isinstance(c, SCLPart3):
                fac = c.get_face()
                if fac is not None:
                    face_shape = fac.Face() if hasattr(fac, 'Face') else fac
                    ctx_color = _find_context_color(c)
                    wrapper = _ColoredFaceWrapper(ctx_color) if ctx_color is not None else None
                    wrappers.append((face_shape, wrapper))

        if len(wrappers) > 0:
            for _, wr in wrappers[1:]:
                if wr is not None and hasattr(wr, 'context_role'):
                    wr.context_role = "cutter"

            n_ops = max(len(wrappers) - 1, 1)
            adjust_progress_total(n_ops)
            push_label()
            result_parts = _unpack_compound(
                wrappers[0][0],
                _extract_face_colors(wrappers[0][1], wrappers[0][0])
            )

            for idx, (s, wr) in enumerate(wrappers[1:], start=1):
                set_progress_label(f"cut {idx}/{n_ops}")
                check_cancel()
                result_parts = _cut_parts(result_parts, s, _extract_face_colors(wr, s))
                if not result_parts:
                    self.shape = None
                    self.children = []
                    return
                advance_progress()

            pop_label()
            if len(result_parts) == 1:
                self.shape = SCLShape(result_parts[0][0])
                self.shape.set_face_colors(result_parts[0][1])
            elif len(result_parts) > 1:
                b = BRep_Builder()
                compound = TopoDS_Compound()
                b.MakeCompound(compound)
                all_colors = {}
                for part, part_colors in result_parts:
                    b.Add(compound, part)
                    all_colors.update(part_colors)
                self.shape = SCLShape(compound)
                self.shape.set_face_colors(all_colors)

        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}








class SCLIntersection(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def intersection(self):
        children = self.get_children()

        wrappers = []
        for c in children:
            if getattr(c, 'shape', None) is not None:
                s = c.shape.get_shape()
                wrappers.append((s, c.shape))
            elif hasattr(c, 'get_face') and not isinstance(c, SCLPart3):
                fac = c.get_face()
                if fac is not None:
                    face_shape = fac.Face() if hasattr(fac, 'Face') else fac
                    ctx_color = _find_context_color(c)
                    wrapper = _ColoredFaceWrapper(ctx_color) if ctx_color is not None else None
                    wrappers.append((face_shape, wrapper))

        if len(wrappers) > 0:
            if len(wrappers) == 1:
                s, wr = wrappers[0]
                self.shape = SCLShape(s)
                self.shape.set_face_colors(_extract_face_colors(wr, s))
            else:
                n_ops = max(len(wrappers) - 1, 1)
                adjust_progress_total(n_ops)
                push_label()

                result_shape = wrappers[0][0]
                result_face_colors = _extract_face_colors(wrappers[0][1], result_shape)

                for idx, (s, wr) in enumerate(wrappers[1:], start=1):
                    set_progress_label(f"intersect {idx}/{n_ops}")
                    check_cancel()
                    tgt_colors = _extract_face_colors(wr, s)
                    result_shape, result_face_colors = universal_optimized_common_colored(
                        result_shape, s, result_face_colors, tgt_colors
                    )
                    if result_shape is None:
                        self.shape = None
                        self.children = []
                        return
                    advance_progress()

                pop_label()
                self.shape = SCLShape(result_shape)
                self.shape.set_face_colors(result_face_colors)

        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}



class VertexValenceGroup:
    def __init__(self, vertex):
        self.vertex = vertex
        self.edges = []

    def add_edge_if_unique(self, new_edge):
        for edge in self.edges:
            if edge.IsSame(new_edge):
                return
        self.edges.append(new_edge)


def get_inward_tangent(edge, vertex):
    if BRep_Tool.Degenerated(edge):
        return None

    adaptor = BRepAdaptor_Curve(edge)
    u_start = adaptor.FirstParameter()
    u_end = adaptor.LastParameter()

    p_start, p_end = gp_Pnt(), gp_Pnt()
    v_start, v_end = gp_Vec(), gp_Vec()

    adaptor.D1(u_start, p_start, v_start)
    adaptor.D1(u_end, p_end, v_end)

    v_pnt = BRep_Tool.Pnt(vertex)
    if v_pnt.Distance(p_start) < v_pnt.Distance(p_end):
        return v_start
    else:
        v_end.Reverse()
        return v_end


def resolve_high_valence_vertices(
    shape: TopoDS_Shape,
    micro_depth: float = 0.005,
    max_angle_deg: float = 100.0
):
    unique_vertices = []

    # 1. collect topology
    edge_explorer = TopExp_Explorer(shape, TopAbs_EDGE)
    while edge_explorer.More():
        edge = topods.Edge(edge_explorer.Current())
        vertex_explorer = TopExp_Explorer(edge, TopAbs_VERTEX)
        while vertex_explorer.More():
            vertex = topods.Vertex(vertex_explorer.Current())
            found = False
            for group in unique_vertices:
                if group.vertex.IsSame(vertex):
                    group.add_edge_if_unique(edge)
                    found = True
                    break
            if not found:
                new_group = VertexValenceGroup(vertex)
                new_group.add_edge_if_unique(edge)
                unique_vertices.append(new_group)
            vertex_explorer.Next()
        edge_explorer.Next()

    high_valence_groups = [g for g in unique_vertices if len(g.edges) >= 4]

    if not high_valence_groups:
        return shape, None

    cut_tools = []
    max_angle_rad = math.radians(max_angle_deg)

    # 2. compute micro-disks for each high-valence vertex
    for group in high_valence_groups:
        v_pnt = BRep_Tool.Pnt(group.vertex)

        avg_vec = gp_Vec(0, 0, 0)
        tangents = []
        for edge in group.edges:
            tangent = get_inward_tangent(edge, group.vertex)
            if tangent is None:
                continue
            tangent.Normalize()
            tangents.append(tangent)
            avg_vec.Add(tangent)

        if not tangents or avg_vec.Magnitude() < 1e-6:
            continue

        avg_vec.Normalize()

        skip_vertex = False
        max_tan = 0.0
        for t in tangents:
            angle_rad = avg_vec.Angle(t)
            if angle_rad >= max_angle_rad:
                skip_vertex = True
                break
            max_tan = max(max_tan, math.tan(angle_rad))

        if skip_vertex:
            continue

        radius = max(micro_depth * max_tan * 1.2, micro_depth * 1.5)

        plane_pnt = gp_Pnt(v_pnt.X() + avg_vec.X() * micro_depth,
                           v_pnt.Y() + avg_vec.Y() * micro_depth,
                           v_pnt.Z() + avg_vec.Z() * micro_depth)

        cut_normal = gp_Dir(-avg_vec.X(), -avg_vec.Y(), -avg_vec.Z())
        axes = gp_Ax2(plane_pnt, cut_normal)
        height = micro_depth * 2.5

        disk = BRepPrimAPI_MakeCylinder(axes, radius, height).Shape()
        cut_tools.append(disk)

    if not cut_tools:
        return shape, None

    # 3. subtract disks
    cutter_op = _bool_op(BRepAlgoAPI_Cut, [shape], cut_tools, fuzzy=1e-5)

    if not cutter_op.IsDone():
        warning("resolve_high_valence_vertices: Cut operation failed")
        return shape, None

    return cutter_op.Shape(), cutter_op



def _solid_volume(solid):
    props = GProp_GProps()
    brepgprop.VolumeProperties(solid, props, True, False, False)
    return max(0.0, props.Mass())


def _ensure_solid(shape):
    if shape is None or shape.IsNull():
        return shape
    if shape.ShapeType() == TopAbs_SOLID:
        return shape

    from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeSolid, BRepBuilderAPI_Sewing
    from OCC.Core.BRepLib import breplib
    from OCC.Core.TopoDS import topods
    from OCC.Extend.TopologyUtils import TopologyExplorer

    if shape.ShapeType() == TopAbs_SHELL:
        sewing = BRepBuilderAPI_Sewing()
        sewing.Add(shape)
        sewing.Perform()
        sewed = sewing.SewedShape()
        shells = list(TopologyExplorer(sewed).shells())
        if shells:
            sb = BRepBuilderAPI_MakeSolid(shells[0])
            if sb.IsDone():
                solid = sb.Solid()
                breplib.OrientClosedSolid(solid)
                return solid
        return shape

    if shape.ShapeType() == TopAbs_COMPOUND:
        exp = TopExp_Explorer(shape, TopAbs_SOLID)
        sols = []
        while exp.More():
            sols.append(topods.Solid(exp.Current()))
            exp.Next()
        if len(sols) > 0:
            sols.sort(key=lambda s: _solid_volume(s), reverse=True)
            if _solid_volume(sols[0]) > 1e-9:
                return sols[0]
            if len(sols) > 1:
                result = sols[1]
                for s in sols[2:]:
                    fuse = _bool_op(BRepAlgoAPI_Fuse, [result, s])
                    if fuse.IsDone():
                        result = fuse.Shape()
                if _solid_volume(result) > 1e-9:
                    return result

        exp = TopExp_Explorer(shape, TopAbs_SHELL)
        shells = []
        while exp.More():
            shells.append(topods.Shell(exp.Current()))
            exp.Next()
        if shells:
            solids = []
            for sh in shells:
                sb = BRepBuilderAPI_MakeSolid(sh)
                if sb.IsDone():
                    sol = sb.Solid()
                    breplib.OrientClosedSolid(sol)
                    if _solid_volume(sol) > 1e-9:
                        solids.append(sol)
            if len(solids) == 1:
                return solids[0]
            if len(solids) > 1:
                result = solids[0]
                for s in solids[1:]:
                    fuse = _bool_op(BRepAlgoAPI_Fuse, [result, s])
                    if fuse.IsDone():
                        result = fuse.Shape()
                if result.ShapeType() == TopAbs_SOLID or _solid_volume(result) > 1e-9:
                    return result

    return shape


def _offset_3d(base_shape, offset_val, join_type=GeomAbs_Arc, orig_colors=None,
               color_blend=False):
    if offset_val == 0:
        return base_shape, orig_colors

    if orig_colors is None:
        orig_colors = {}

    cut_shape, cut_op = resolve_high_valence_vertices(
        base_shape, max(0.001, abs(offset_val / 20))
    )
    cut_shape = _ensure_solid(cut_shape)
    try:
        maker = BRepOffsetAPI_MakeOffsetShape()
        maker.PerformByJoin(
            cut_shape, offset_val, 1e-6,
            BRepOffset_Skin,
            True, True, GeomAbs_Arc, True,
            Message_ProgressRange()
        )
    except Exception as e:
        warning(f"offset: BRepOffsetAPI_MakeOffsetShape threw: {e}")
        return base_shape, orig_colors

    if not maker.IsDone():
        warning("offset: BRepOffsetAPI_MakeOffsetShape failed")
        return base_shape, orig_colors

    res = maker.Shape()
    if res is None or res.IsNull():
        warning("offset: result is null")
        return base_shape, orig_colors

    res = _ensure_solid(res)

    if orig_colors:
        if cut_op is not None:
            mid_colors = _map_colors(cut_op, [(base_shape, orig_colors)])
            face_colors = _map_colors(maker, [(cut_shape, mid_colors)])
        else:
            face_colors = _map_colors(maker, [(base_shape, orig_colors)])
        face_colors = _adjacency_fill_colors(res, face_colors, blend=color_blend)
    else:
        face_colors = None

    return res, face_colors


def _offset_2d(wire_or_face, offset_val, join_type=GeomAbs_Arc, orig_colors=None):
    from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeOffset

    if isinstance(wire_or_face, TopoDS_Compound) or wire_or_face.ShapeType() == TopAbs_FACE:
        exp = TopExp_Explorer(wire_or_face, TopAbs_WIRE)
        wires = []
        while exp.More():
            wires.append(topods.Wire(exp.Current()))
            exp.Next()
        if not wires:
            return None, None
        wire = wires[0]
    else:
        wire = topods.Wire(wire_or_face)

    maker = BRepOffsetAPI_MakeOffset(wire, join_type, False)
    maker.Perform(offset_val)
    if not maker.IsDone():
        return None, None
    res = maker.Shape()
    if res is None or res.IsNull():
        return None, None

    result_wire = topods.Wire(res)
    # edge case: if the offset collapsed the wire, result may be empty
    exp2 = TopExp_Explorer(result_wire, TopAbs_EDGE)
    has_edges = False
    while exp2.More():
        has_edges = True
        break
    if not has_edges:
        warning("offset(): 2D offset produced empty result — offset too large?")
        return None, None

    result_face = BRepBuilderAPI_MakeFace(result_wire).Face()

    face_colors = None
    if orig_colors:
        face_colors = {hash(result_face): next(iter(orig_colors.values()))}

    return result_face, face_colors


def _get_offset_path(shape):
    exp = TopExp_Explorer(shape, TopAbs_SOLID)
    if exp.More():
        return '3d'

    faces = []
    exp = TopExp_Explorer(shape, TopAbs_FACE)
    while exp.More():
        faces.append(topods.Face(exp.Current()))
        exp.Next()
    n = len(faces)

    if n == 0:
        exp = TopExp_Explorer(shape, TopAbs_WIRE)
        wires = []
        while exp.More():
            wires.append(topods.Wire(exp.Current()))
            exp.Next()
        if not wires:
            return None
        for w in wires:
            finder = BRepBuilderAPI_FindPlane(w, 1e-6)
            if not finder.Found():
                return None
        return '2d'

    if n == 1:
        surf = BRepAdaptor_Surface(faces[0])
        return '2d' if surf.GetType() == GeomAbs_Plane else None

    return '3d'


class SCLMinkowski(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def _detect_sphere_radius(self, shape):
        exp = TopExp_Explorer(shape, TopAbs_FACE)
        face_count = 0
        radius = None
        while exp.More():
            face_count += 1
            face = topods.Face(exp.Current())
            adaptor = BRepAdaptor_Surface(face)
            if adaptor.GetType() == GeomAbs_Sphere:
                radius = adaptor.Sphere().Radius()
            exp.Next()
        if face_count == 1 and radius is not None:
            return radius
        return None

    def minkowski(self, color_blend=False):
        """Minkowski sum — sphere-only rounding via BRepOffset_MakeOffset.

        The algorithm:
        1. Collect children: the first non-sphere child is the base shape,
           all sphere children accumulate into the offset radius.
        2. _offset_3d() applies micro-sphere fix, BRepOffset_MakeOffset
           with GeomAbs_Arc, volume sanity check, and face color mapping.
        """
        children = self.get_children()
        base_obj = None
        base_shape = None
        offset = 0.0
        warned = False

        for c in children:
            s = getattr(c, 'shape', None)
            if s is None:
                continue
            shape = s.get_shape()
            r = self._detect_sphere_radius(shape)
            if r is not None:
                offset += r
            elif base_obj is None:
                base_shape = shape
                base_obj = c
            else:
                if not warned:
                    warning("minkowski(): non-sphere child ignored — only sphere rounding supported")
                    warned = True

        adjust_progress_total(1)
        push_label()
        set_progress_label("minkowski")
        check_cancel()

        if base_obj is not None:
            base_shape, colors = _fuse_parts([base_obj])

        if base_shape is None and offset > 0:
            self.shape = SCLShape(BRepPrimAPI_MakeSphere(offset).Shape())
        elif base_shape is not None and offset > 0:
            res, face_colors = _offset_3d(base_shape, offset, GeomAbs_Arc, orig_colors=colors,
                                          color_blend=color_blend)
            if res is not None:
                self.shape = SCLShape(res)
                if face_colors:
                    self.shape.set_face_colors(face_colors)

        if self.shape is None and base_shape is not None:
            self.shape = SCLShape(base_shape)

        advance_progress()
        pop_label()
        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}


class SCLOffset(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def offset(self, r=None, delta=None, chamfer=False, color_blend=False):
        children = self.get_children()
        if not children:
            warning("offset(): no child shape")
            return

        have_r = r is not None
        have_delta = delta is not None
        if have_r and have_delta:
            warning("offset(): both r and delta specified — using r")
            have_delta = False
        if not have_r and not have_delta:
            warning("offset(): no r or delta specified, using default r=1")
            r = 1.0
            have_r = True
        if chamfer:
            warning("offset(): chamfer not yet supported — using sharp (delta-mode) join")

        join_type = GeomAbs_Arc if have_r else GeomAbs_Intersection
        offset_val = r if have_r else delta

        base_obj = None
        for c in children:
            if getattr(c, 'shape', None) is not None:
                base_obj = c
                break
        if base_obj is None:
            for c in children:
                if hasattr(c, 'get_face'):
                    face_obj = c.get_face()
                    if face_obj is not None:
                        base_obj = c
                        break
        if base_obj is None:
            for c in children:
                if hasattr(c, 'get_wire'):
                    wire = c.get_wire()
                    if wire is not None:
                        base_obj = c
                        break
        if base_obj is None:
            warning("offset(): child has no shape")
            return

        adjust_progress_total(1)
        push_label()
        set_progress_label("offset")
        check_cancel()

        base_shape, colors = _fuse_parts([base_obj])

        path = _get_offset_path(base_shape)
        if path is None:
            warning("offset(): shape is not suitable for offset")
            self.shape = SCLShape(base_shape)
            if colors:
                self.shape.set_face_colors(colors)
        elif path == '2d':
            res, face_colors = _offset_2d(base_shape, offset_val, join_type, orig_colors=colors)
            if res is not None:
                self.shape = SCLShape(res)
                if face_colors:
                    self.shape.set_face_colors(face_colors)
        else:
            res, face_colors = _offset_3d(base_shape, offset_val, join_type, orig_colors=colors,
                                          color_blend=color_blend)
            if res is not None:
                self.shape = SCLShape(res)
                if face_colors:
                    self.shape.set_face_colors(face_colors)

        advance_progress()
        pop_label()
        self.children = []

    def display(self, writer=None):
        if self.shape is not None:
            self.shape.display(writer)
        return {'name': self.name, 'children': None}


class SCLProjection(SCLPart3):
    def __init__(self, parent):
        super().__init__(parent)

    def projector(self, trsf):
        p = HLRAlgo_Projector(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, -1)))
        return p

    def projection(self):
        children = self.get_children()
        shapes = []
        debug("children: %s"%(children,))
        for c in children:
            if c.shape is not None:
                shapes.append(c.shape.get_shape())
        self.hlr_project(shapes)

    def hlr_project(self, shapes):
        hlralgo = HLRBRep_Algo()
        if len(shapes)>0:
            for s in shapes:
                hlralgo.Add(s)
            self.children = []
            p = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), gp_Dir(0, 0, 1)))
            hlralgo.Projector(p)
            hlralgo.Update()
            hlralgo.Hide()
            hlrtoshape = HLRBRep_HLRToShape(hlralgo)
            vcompound = hlrtoshape.VCompound()
            voutline = hlrtoshape.OutLineVCompound()
            rg1linevcompound = hlrtoshape.Rg1LineVCompound()
            rgnlinevcompound = hlrtoshape.RgNLineVCompound()
            isolinevcompound = hlrtoshape.IsoLineVCompound()
            hcompound = hlrtoshape.HCompound()
            houtline = hlrtoshape.OutLineHCompound()
            rg1linehcompound = hlrtoshape.Rg1LineHCompound()
            rgnlinehcompound = hlrtoshape.RgNLineHCompound()
            isolinehcompound = hlrtoshape.IsoLineHCompound()
            debug("vcompound: %s"%(str(vcompound),))
            debug("voutline: %s"%(str(voutline),))
            debug("rg1linevcompound: %s"%(str(rg1linevcompound),))
            debug("rgnlinevcompound: %s"%(str(rgnlinevcompound),))
            debug("isolinevcompound: %s"%(str(isolinevcompound),))
            debug("hcompound: %s"%(str(hcompound),))
            debug("houtline: %s"%(str(houtline),))
            debug("rg1linehcompound: %s"%(str(rg1linehcompound),))
            debug("rgnlinehcompound: %s"%(str(rgnlinehcompound),))
            debug("isolinehcompound: %s"%(str(isolinehcompound),))
            name = get_inc_name("projection")
            if (vcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(vcompound)
                scls.set_linestyle("main_projection")
                sclp.set_shape(scls)
                sclp.set_name(name+'vcompound')
                self.add_child_context(sclp)
            if (voutline is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(voutline)
                scls.set_linestyle("main_projection")
                sclp.set_shape(scls)
                sclp.set_name(name+'voutline')
                self.add_child_context(sclp)
            if (rg1linevcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(rg1linevcompound)
                scls.set_linestyle("main_projection")
                sclp.set_shape(scls)
                sclp.set_name(name+'rg1linevcompound')
                self.add_child_context(sclp)
            if (rgnlinevcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(rgnlinevcompound)
                scls.set_linestyle("main_projection")
                sclp.set_shape(scls)
                sclp.set_name(name+'rgnlinecompound')
                self.add_child_context(sclp)
            if (isolinevcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(isolinevcompound)
                scls.set_linestyle("main_projection")
                sclp.set_shape(scls)
                sclp.set_name(name+'isolinevcompound')
                self.add_child_context(sclp)
            if (hcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(hcompound)
                scls.set_linestyle("hidden")
                scls.set_hidden(True)
                sclp.set_shape(scls)
                sclp.set_name(name+'hcompound')
                self.add_child_context(sclp)
            if (houtline is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(houtline)
                scls.set_linestyle("hidden")
                scls.set_hidden(True)
                sclp.set_shape(scls)
                sclp.set_name(name+'houtline')
                self.add_child_context(sclp)
            if (rg1linehcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(rg1linehcompound)
                scls.set_linestyle("hidden")
                scls.set_hidden(True)
                sclp.set_shape(scls)
                sclp.set_name(name+'rg1linehcompound')
                self.add_child_context(sclp)
            if (rgnlinehcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(rgnlinehcompound)
                scls.set_linestyle("hidden")
                scls.set_hidden(True)
                sclp.set_shape(scls)
                sclp.set_name(name+'rgnlinehcompound')
                self.add_child_context(sclp)
            if (isolinehcompound is not None):
                sclp = SCLPart3(self)
                scls = SCLShape(isolinehcompound)
                scls.set_linestyle("hidden")
                scls.set_hidden(True)
                sclp.set_shape(scls)
                sclp.set_name(name+'isolinehcompound')
                self.add_child_context(sclp)


    def display(self, writer=None):
        debug("Display SCLProjection")
        objects = []
        for c in self.children:
            objects.append(c.display(writer))
        if self.shape is not None:
            self.shape.display(writer)
        objects = {'name': self.name, 'children': objects}
        debug("end SCLProjection")
        return objects

class SCLRevol(SCLContext):
    def __init__(self, parent):
        super().__init__(parent)
        self.shape = None

    def apply_trsf(self, trsf):
        if self.shape is not None:
            self.shape.transform(trsf)

    def rotate_extrude(self, angle=360.0):
        faces = []
        for c in self.children:
            debug("Adding face for revolution")
            f = c.get_face()
            if f is not None:
                faces.append(f)

        self.children = []

        # Создаем матрицу поворота на 90 градусов вокруг оси X,
        # чтобы "поставить вертикально" плоский профиль из XY в плоскость XZ
        rotate_trsf = gp_Trsf()
        rotate_trsf.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), math.radians(90))

        # Ось вращения в OpenSCAD — это ось Z
        axis = gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
        angle_rad = math.radians(angle)

        for f in faces:
            # 1. Поворачиваем плоскую грань вертикально
            vertical_face = BRepBuilderAPI_Transform(f.Face() if hasattr(f, 'Face') else f, rotate_trsf).Shape()

            # 2. Вращаем вертикальную грань вокруг оси Z
            revol = BRepPrimAPI_MakeRevol(vertical_face, axis, angle_rad)
            shape = revol.Shape()

            scls = SCLShape(shape)
            sclp = SCLPart3(self)
            sclp.set_shape(scls)
            name = get_inc_name("extrusion")
            sclp.set_name(name)
            debug("Creating revolution %s"%(name,))
            self.add_child_context(sclp)

    def display(self, writer=None):
        debug("Display SCLRevol")
        debug("Children: %s"%(repr(self.children),))
        objects = []
        for c in self.children:
            objects.append(c.display(writer))
        objects = {'name': self.name, 'children': objects}

        if self.shape is not None:
            self.shape.display(writer)
        debug("end SCLRevol")
        return objects







# utility functions



def get_bnd_box(shape):
    box = Bnd_Box()
    brepbndlib.Add(shape, box)
    return box

def is_shape_empty(shape):
    if shape.IsNull():
        return True
    if shape.ShapeType() == TopAbs_COMPOUND:
        it = TopoDS_Iterator(shape)
        if not it.More():
            return True
    return False


def universal_optimized_common_colored(shape_a, shape_b, colors_a=None, colors_b=None):
    """Попарный Common sub-shapes с bbox-фильтром и сохранением цветов граней.
    colors_a/b: dict[int, Quantity_ColorRGBA] или None.
    Возвращает (result_compound, face_colors_dict) или (None, None)."""
    list_a = _unpack_compound(shape_a, colors_a)
    list_b = _unpack_compound(shape_b, colors_b)

    boxes_a = [get_bnd_box(s) for s, _ in list_a]
    boxes_b = [get_bnd_box(s) for s, _ in list_b]

    builder = BRep_Builder()
    result_compound = TopoDS_Compound()
    builder.MakeCompound(result_compound)
    has_results = False
    all_face_colors = {}

    for i, (part_a, sub_colors_a) in enumerate(list_a):
        for j, (part_b, sub_colors_b) in enumerate(list_b):
            if boxes_a[i].IsOut(boxes_b[j]):
                continue

            common_api = _bool_op(BRepAlgoAPI_Common, [part_a, part_b])

            if common_api.IsDone():
                res_shape = common_api.Shape()
                if not is_shape_empty(res_shape):
                    pair_colors = _map_colors(
                        common_api, [(part_a, sub_colors_a), (part_b, sub_colors_b)]
                    )
                    builder.Add(result_compound, res_shape)
                    all_face_colors.update(pair_colors)
                    has_results = True

    if has_results:
        return result_compound, all_face_colors
    return None, None









# --- Debug utilities -----------------------------------------------------------

def _shape_type_str(shape):
    t = shape.ShapeType()
    names = {
        TopAbs_COMPOUND: "COMPOUND",
        TopAbs_SOLID:    "SOLID",
        TopAbs_SHELL:    "SHELL",
        TopAbs_FACE:     "FACE",
        TopAbs_WIRE:     "WIRE",
        TopAbs_EDGE:     "EDGE",
        TopAbs_VERTEX:   "VERTEX",
    }
    return names.get(t, f"UNKNOWN({t})")


def debug_shape_structure(shape, indent=0, label=""):
    prefix = "  " * indent
    n_faces = 0
    exp = TopExp_Explorer(shape, TopAbs_FACE)
    while exp.More():
        n_faces += 1
        exp.Next()
    extra = f" / {label}" if label else ""
    print(f"{prefix}{_shape_type_str(shape)} (faces={n_faces}){extra}")
    if shape.ShapeType() == TopAbs_COMPOUND:
        it = TopoDS_Iterator(shape)
        child_idx = 0
        while it.More():
            debug_shape_structure(it.Value(), indent + 1)
            it.Next()


def print_algo_diagnostics(algo):
    from OCC.Core.Message import Message_Warning, Message_Alarm
    """
    Печатает все предупреждения (Warnings) и ошибки (Errors/Fails)
    из алгоритма BRepAlgoAPI.
    """
    if not algo.IsDone():
        print("[СБОЙ] Алгоритм завершился с критической ошибкой топологии!")

    report = algo.GetReport()

    # 1. Проверяем наличие Ошибок через сам алгоритм
    if algo.HasErrors():
        print("\n--- ОШИБКИ (ERRORS) ---")
        errors = report.GetAlerts(Message_Fail)

        for alert in errors:
            alert_type = alert.DynamicType().Name()
            alert_key = alert.GetMessageKey()
            print(f"[Error Type]: {alert_type}")
            print(f"[Message Key]: {alert_key}")
            print("-" * 30)

    # 2. Проверяем наличие Предупреждений
    if algo.HasWarnings():
        print("\n--- ПРЕДУПРЕЖДЕНИЯ (WARNINGS) ---")
        warnings = report.GetAlerts(Message_Warning)

        for alert in warnings:
            alert_type = alert.DynamicType().Name()
            alert_key = alert.GetMessageKey()
            print(f"[Warning Type]: {alert_type}")
            print(f"[Message Key]: {alert_key}")
            print("-" * 30)

    if not algo.HasErrors() and not algo.HasWarnings():
        print("Диагностика чиста: ни ошибок, ни предупреждений не обнаружено.")
