Source code for declaracad.occ.impl.occ_svg

"""
Copyright (c) 2016-2018, CodeLV.

Distributed under the terms of the GPL v3 License.

The full license is in the file LICENSE, distributed with this software.

Created on Sep 20, 2018

@author: jrm
"""

import os
import re
import warnings
from math import cos, pi, radians, sin, sqrt, tan

from atom.api import Atom, Dict, ForwardTyped, Instance, Str, set_default
from lxml import etree
from OCCT.BRep import BRep_Builder
from OCCT.BRepBuilderAPI import (
    BRepBuilderAPI_MakeEdge,
    BRepBuilderAPI_MakeFace,
    BRepBuilderAPI_MakePolygon,
    BRepBuilderAPI_MakeWire,
    BRepBuilderAPI_Transform,
)

# from OCCT.BRepLib import breplib_BuildCurves3d
from OCCT.GC import GC_MakeArcOfEllipse
from OCCT.Geom import Geom_BezierCurve
from OCCT.gp import (
    gp_Ax1,
    gp_Ax2,
    gp_Circ,
    gp_Dir,
    gp_Elips,
    gp_Pnt,
    gp_Pnt2d,
    gp_Trsf,
    gp_Trsf2d,
    gp_Vec,
)
from OCCT.TColgp import TColgp_Array1OfPnt
from OCCT.TopoDS import TopoDS_Compound, TopoDS_Shape

from declaracad.core.utils import log

from ..draw import ProxySvg
from .occ_shape import OccShape

Z_DIR = gp_Dir(0, 0, 1)
NEG_Z_DIR = gp_Dir(0, 0, -1)
Z_AXIS = gp_Ax1(gp_Pnt(0, 0, 0), Z_DIR)

UNITS = {
    "in": 90.0,
    "pt": 1.25,
    "px": 1,
    "mm": 3.5433070866,
    "cm": 35.433070866,
    "m": 3543.3070866,
    "km": 3543307.0866,
    "pc": 15.0,
    "yd": 3240,
    "ft": 1080,
}

# From  simplepath.py's parsePath by Aaron Spike, aaron@ekips.org
PATHDEFS = {
    "M": ["L", 2, [float, float], ["x", "y"]],
    "L": ["L", 2, [float, float], ["x", "y"]],
    "H": ["H", 1, [float], ["x"]],
    "V": ["V", 1, [float], ["y"]],
    "C": [
        "C",
        6,
        [float, float, float, float, float, float],
        ["x", "y", "x", "y", "x", "y"],
    ],
    "S": ["S", 4, [float, float, float, float], ["x", "y", "x", "y"]],
    "Q": ["Q", 4, [float, float, float, float], ["x", "y", "x", "y"]],
    "T": ["T", 2, [float, float], ["x", "y"]],
    "A": [
        "A",
        7,
        [float, float, float, int, int, float, float],
        ["r", "r", "a", "a", "s", "x", "y"],
    ],
    "Z": ["L", 0, [], []],
}


def parse_unit(value):
    """Returns userunits given a string representation of units
    in another system
    """
    if value is None:
        return None

    if isinstance(value, (int, float)):
        return value

    unit = re.compile("(%s)$" % "|".join(UNITS.keys()))
    param = re.compile(r"(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)")

    p = param.match(value)
    u = unit.search(value)
    if p:
        retval = float(p.string[p.start() : p.end()])
    else:
        retval = 0.0
    if u:
        try:
            return retval * UNITS[u.string[u.start() : u.end()]]
        except KeyError:
            pass
    return retval


def make_ellipse(p, rx, ry, rotate=0, direction=Z_DIR):
    """gp_Elips doesn't allow minor > major so swap and rotate instead if
    that's the case.
    """
    c = gp_Pnt(*p)
    if ry > rx:
        rx, ry = ry, rx  # Swap
        rotate += pi / 2
        # This only works when rotate == 0
    ellipse = gp_Elips(gp_Ax2(c, direction), rx, ry)
    ellipse.Rotate(gp_Ax1(c, direction), rotate)
    return ellipse


def compute_arc_center(x1, y1, rx, ry, phi, large_arc_flag, sweep_flag, x2, y2):
    """Compute center point of an arc based SVG parameters given.
    Thanks to Arnout Engelen's work in Inkcut.

    Notes
    -----
    See https://www.w3.org/TR/SVG/implnote.html F.6.6

    """
    rx = abs(rx)
    ry = abs(ry)

    # https://www.w3.org/TR/SVG/implnote.html F.6.5
    x1prime = cos(phi) * (x1 - x2) / 2 + sin(phi) * (y1 - y2) / 2
    y1prime = -sin(phi) * (x1 - x2) / 2 + cos(phi) * (y1 - y2) / 2

    # https://www.w3.org/TR/SVG/implnote.html F.6.6
    lamb = (x1prime * x1prime) / (rx * rx) + (y1prime * y1prime) / (ry * ry)
    if lamb >= 1:
        ry = sqrt(lamb) * ry
        rx = sqrt(lamb) * rx

    # Back to https://www.w3.org/TR/SVG/implnote.html F.6.5
    radicand = (
        rx * rx * ry * ry - rx * rx * y1prime * y1prime - ry * ry * x1prime * x1prime
    )
    radicand /= rx * rx * y1prime * y1prime + ry * ry * x1prime * x1prime

    if radicand < 0:
        radicand = 0

    factor = (-1 if large_arc_flag == sweep_flag else 1) * sqrt(radicand)

    cxprime = factor * rx * y1prime / ry
    cyprime = -factor * ry * x1prime / rx

    cx = cxprime * cos(phi) - cyprime * sin(phi) + (x1 + x2) / 2
    cy = cxprime * sin(phi) + cyprime * cos(phi) + (y1 + y2) / 2
    # if phi == 0:
    # start_theta = -atan2((y1 - cy) * rx, (x1 - cx) * ry)

    # start_phi = -atan2(y1 - cy, x1 - cx)
    # end_phi = -atan2(y2 - cy, x2 - cx)

    # sweep_length = end_phi - start_phi

    # if sweep_length < 0 and not sweep_flag:
    # sweep_length += 2 * pi
    # elif sweep_length > 0 and sweep_flag:
    # sweep_length -= 2 * pi

    # self.arcTo(cx - rx, cy - ry, rx * 2, ry * 2,
    # start_theta * 360 / 2 / pi, sweep_length * 360 / 2 / pi)
    return (cx, cy, rx, ry)


class OccSvgNode(Atom):
    svg = ForwardTyped(lambda: OccSvg)

    #: Transform
    transform = Instance(gp_Trsf, ())

    #: Element
    element = Instance(etree._Element)

    #: Parsed style fields
    style = Dict()

    #: Fill color
    fill = Str()

    def create_shape(self):
        """Create and return the shape for the given svg node."""
        raise NotImplementedError

    def _default_style(self):
        style = {}
        style_attr = self.element.attrib.get("style", "")
        if style_attr:
            for it in style_attr.split(";"):
                k, v = it.strip().split(":", 1)
                style[k.strip()] = v.strip()
        return style

    def _default_fill(self):
        fill = self.element.attrib.get("fill", None)
        if fill is None:
            fill = self.style.get("fill", None)
        if fill and fill != "transparent":
            return fill
        return ""

    def _default_transform(self):
        transform = self.element.attrib.get("transform", None)
        t = gp_Trsf()
        if not transform:
            return t
        pattern = r"([A-z]+)\s*\(\s*((?:\-?\d+\.?\d*\s*[%a-z]*,?\s*)+)\s*\)"
        for m in re.finditer(pattern, transform):
            cmd, args = m.groups()
            try:
                args = args.replace(",", " ")
                args = [parse_unit(arg) for arg in args.split(" ")]
                if cmd == "matrix":
                    a, b, c, d, e, f = args
                    matrix = gp_Trsf2d()
                    matrix.SetValues(a, c, e, b, d, f)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "matrix3d":
                    matrix = gp_Trsf()
                    matrix.SetValues(*args)
                    t.Multiply(matrix)
                elif cmd == "rotate":
                    if len(args) == 3:
                        a, x, y = args
                    else:
                        x = y = 0
                        a = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetRotation(gp_Pnt2d(x, y), radians(a))
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "translate":
                    if len(args) == 2:
                        x, y = args
                    else:
                        x, y = args[0], 0
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, 0, x, 0, 1, y)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "translateX":
                    x = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, 0, x, 0, 1, 0)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "translateY":
                    y = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, 0, 0, 0, 1, y)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "scale":
                    if len(args) == 2:
                        x, y = args
                    else:
                        x = y = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(x, 0, 0, 0, y, 0)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "scaleX":
                    x = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(x, 0, 0, 0, 1, 0)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "scaleY":
                    y = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, 0, 0, 0, y, 0)
                    t.Multiply(gp_Trsf(matrix))
                # elif cmd == 'scaleZ':
                #    z = args[0]
                #    matrix = gp_Trsf2d()
                #    matrix.SetValues(
                #        1, 0, 0,
                #        0, 1, 0,
                #        0, 0, z)
                #    t.Multiply(gp_Trsf(matrix))
                # elif cmd == 'scale3d':
                #    x, y, z = args[0]
                #    matrix = gp_Trsf2d()
                #    matrix.SetValues(
                #        x, 0, 0,
                #        0, y, 0,
                #        0, 0, z)
                #    t.Multiply(gp_Trsf(matrix))
                elif cmd == "skew":
                    x, y = args
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, tan(radians(x)), 0, tan(radians(y)), 1, 0)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "skewX":
                    x = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, tan(radians(x)), 0, 0, 1, 0)
                    t.Multiply(gp_Trsf(matrix))
                elif cmd == "skewY":
                    y = args[0]
                    matrix = gp_Trsf2d()
                    matrix.SetValues(1, 0, 0, tan(radians(y)), 1, 0)
                    t.Multiply(gp_Trsf(matrix))
                else:
                    # TODO: ...
                    warnings.warn(f"Unsupported transform {cmd}")
            except RuntimeError as e:
                log.warning(f"Failed to handled transform {cmd} {args}")
                log.exception(e)
        return t

    def fill_shape(self, wire):
        fill_mode = self.svg.declaration.fill_mode
        if fill_mode == "always" or (fill_mode == "auto" and self.fill):
            return BRepBuilderAPI_MakeFace(wire).Face()
        return wire


class OccSvgEllipse(OccSvgNode):
    def create_shape(self):
        attrs = self.element.attrib
        cx = parse_unit(attrs.get("cx", 0))
        cy = parse_unit(attrs.get("cy", 0))
        rx = parse_unit(attrs.get("rx", 0))
        ry = parse_unit(attrs.get("ry", 0))
        ellipse = make_ellipse((cx, cy, 0), rx, ry)
        edge = BRepBuilderAPI_MakeEdge(ellipse).Edge()
        return self.fill_shape(BRepBuilderAPI_MakeWire(edge).Wire())


class OccSvgCircle(OccSvgNode):
    def create_shape(self):
        attrs = self.element.attrib
        cx = parse_unit(attrs.get("cx", 0))
        cy = parse_unit(attrs.get("cy", 0))
        r = parse_unit(attrs.get("r", 0))
        circle = gp_Circ(gp_Ax2(gp_Pnt(cx, cy, 0), Z_DIR), r)
        edge = BRepBuilderAPI_MakeEdge(circle).Edge()
        return self.fill_shape(BRepBuilderAPI_MakeWire(edge).Wire())


class OccSvgLine(OccSvgNode):
    def create_shape(self):
        attrs = self.element.attrib
        x1 = parse_unit(attrs.get("x1", 0))
        y1 = parse_unit(attrs.get("y1", 0))
        x2 = parse_unit(attrs.get("x2", 0))
        y2 = parse_unit(attrs.get("y2", 0))
        p1 = gp_Pnt(x1, y1, 0)
        p2 = gp_Pnt(x2, y2, 0)
        return BRepBuilderAPI_MakeEdge(p1, p2).Edge()


class OccSvgRect(OccSvgNode):
    def create_shape(self):
        attrs = self.element.attrib
        x = parse_unit(attrs.get("x", 0))
        y = parse_unit(attrs.get("y", 0))
        w = parse_unit(attrs.get("width", 0))
        h = parse_unit(attrs.get("height", 0))
        rx = parse_unit(attrs.get("rx", 0))
        ry = parse_unit(attrs.get("ry", 0))
        if rx == ry == 0:
            shape = BRepBuilderAPI_MakePolygon(
                gp_Pnt(x, y, 0),
                gp_Pnt(x + w, y, 0),
                gp_Pnt(x + w, y + h, 0),
                gp_Pnt(x, y + h, 0),
                True,
            )
            return self.fill_shape(shape.Wire())
        elif rx == 0:
            rx = ry
        elif ry == 0:
            ry = rx

        # Build the rect
        shape = BRepBuilderAPI_MakeWire()

        # Bottom
        p1 = gp_Pnt(x + rx, y, 0)
        p2 = gp_Pnt(x + w - rx, y, 0)

        # Right
        p3 = gp_Pnt(x + w, y + ry, 0)
        p4 = gp_Pnt(x + w, y + h - ry, 0)

        # Top
        p5 = gp_Pnt(x + w - rx, y + h, 0)
        p6 = gp_Pnt(x + rx, y + h, 0)

        # Left
        p7 = gp_Pnt(x, y + h - ry, 0)
        p8 = gp_Pnt(x, y + ry, 0)

        # Bottom
        shape.Add(BRepBuilderAPI_MakeEdge(p1, p2).Edge())

        # Arc bottom right
        c = make_ellipse((x + w - rx, y + ry, 0), rx, ry)
        shape.Add(
            BRepBuilderAPI_MakeEdge(
                GC_MakeArcOfEllipse(c, p2, p3, False).Value()
            ).Edge()
        )

        # Right
        shape.Add(BRepBuilderAPI_MakeEdge(p3, p4).Edge())

        # Arc top right
        c.SetLocation(gp_Pnt(x + w - rx, y + h - ry, 0))
        shape.Add(
            BRepBuilderAPI_MakeEdge(
                GC_MakeArcOfEllipse(c, p4, p5, False).Value()
            ).Edge()
        )

        # Top
        shape.Add(BRepBuilderAPI_MakeEdge(p5, p6).Edge())

        # Arc top left
        c.SetLocation(gp_Pnt(x + rx, y + h - ry, 0))
        shape.Add(
            BRepBuilderAPI_MakeEdge(
                GC_MakeArcOfEllipse(c, p6, p7, False).Value()
            ).Edge()
        )

        # Left
        shape.Add(BRepBuilderAPI_MakeEdge(p7, p8).Edge())

        # Arc bottom left
        c.SetLocation(gp_Pnt(x + rx, y + ry, 0))
        shape.Add(
            BRepBuilderAPI_MakeEdge(
                GC_MakeArcOfEllipse(c, p8, p1, False).Value()
            ).Edge()
        )

        wire = shape.Wire()
        wire.Closed(True)
        return self.fill_shape(wire)


class OccSvgPath(OccSvgNode):
    def path_lexer(self, d):
        """
        From  simplepath.py's parsePath by Aaron Spike, aaron@ekips.org

        returns and iterator that breaks path data
        identifies cmd and parameter tokens
        """
        offset = 0
        length = len(d)
        delim = re.compile(r"[ \t\r\n,]+")
        cmd = re.compile(r"[MLHVCSQTAZmlhvcsqtaz]")
        parameter = re.compile(
            r"(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)"
        )
        while True:
            m = delim.match(d, offset)
            if m:
                offset = m.end()
            if offset >= length:
                break
            m = cmd.match(d, offset)
            if m:
                yield [d[offset : m.end()], True]
                offset = m.end()
                continue
            m = parameter.match(d, offset)
            if m:
                yield [d[offset : m.end()], False]
                offset = m.end()
                continue
            raise ValueError("Invalid path data at %s!" % offset)

    def parse_path(self, d):
        """
        From  simplepath.py's parsePath by Aaron Spike, aaron@ekips.org

        Parse SVG path and return an array of segments.
        Removes all shorthand notation.
        Converts coordinates to absolute.
        """
        lexer = self.path_lexer(d)

        pen = (0.0, 0.0)
        sub_path_start = pen
        last_control = pen
        last_cmd = ""

        while True:
            try:
                token, is_cmd = next(lexer)
            except StopIteration:
                break
            params = []
            need_param = True
            if is_cmd:
                if not last_cmd and token.upper() != "M":
                    raise ValueError(
                        "Invalid path, must begin with moveto ("
                        "M or m), given %s." % last_cmd
                    )
                else:
                    cmd = token
            else:
                # cmd was omited
                # use last cmd's implicit next cmd
                need_param = False
                if last_cmd:
                    if last_cmd.isupper():
                        cmd = PATHDEFS[last_cmd][0]
                    else:
                        cmd = PATHDEFS[last_cmd.upper()][0].lower()
                else:
                    raise ValueError("Invalid path, no initial cmd.")
            num_params = PATHDEFS[cmd.upper()][1]
            while num_params > 0:
                if need_param:
                    try:
                        token, is_cmd = next(lexer)
                        if is_cmd:
                            raise ValueError(
                                "Invalid number of parameters " "for %s" % (cmd,)
                            )
                    except StopIteration:
                        raise Exception("Unexpected end of path")
                cast = PATHDEFS[cmd.upper()][2][-num_params]
                param = cast(token)
                if cmd.islower():
                    if PATHDEFS[cmd.upper()][3][-num_params] == "x":
                        param += pen[0]
                    elif PATHDEFS[cmd.upper()][3][-num_params] == "y":
                        param += pen[1]
                params.append(param)
                need_param = True
                num_params -= 1
            # segment is now absolute so
            output_cmd = cmd.upper()

            # Flesh out shortcut notation
            if output_cmd in ("H", "V"):
                if output_cmd == "H":
                    params.append(pen[1])
                if output_cmd == "V":
                    params.insert(0, pen[0])
                output_cmd = "L"
            if output_cmd in ("S", "T"):
                params.insert(0, pen[1] + (pen[1] - last_control[1]))
                params.insert(0, pen[0] + (pen[0] - last_control[0]))
                if output_cmd == "S":
                    output_cmd = "C"
                if output_cmd == "T":
                    output_cmd = "Q"

            # current values become "last" values
            if output_cmd == "M":
                sub_path_start = tuple(params[0:2])
                pen = sub_path_start
            if output_cmd == "Z":
                pen = sub_path_start
            else:
                pen = tuple(params[-2:])

            if output_cmd in ("Q", "C"):
                last_control = tuple(params[-4:-2])
            else:
                last_control = pen

            last_cmd = cmd

            yield (output_cmd, params)

    def create_shape(self):
        data = self.element.attrib.get("d")
        shapes = []
        path = None
        tol = 1e-6
        for cmd, params in self.parse_path(data):
            if cmd == "M":
                if path is not None:
                    shapes.append(path.Wire())
                path = BRepBuilderAPI_MakeWire()
                last_pnt = gp_Pnt(params[0], params[1], 0)
                start_pnt = last_pnt
            elif cmd in ["L", "H", "V"]:
                pnt = gp_Pnt(params[0], params[1], 0)
                if not last_pnt.IsEqual(pnt, tol):
                    path.Add(BRepBuilderAPI_MakeEdge(last_pnt, pnt).Edge())
                last_pnt = pnt
            elif cmd == "Q":
                # Quadratic Bezier
                pts = TColgp_Array1OfPnt(1, 3)
                pts.SetValue(1, last_pnt)
                pts.SetValue(2, gp_Pnt(params[0], params[1], 0))
                last_pnt = gp_Pnt(params[2], params[3], 0)
                pts.SetValue(3, last_pnt)
                curve = Geom_BezierCurve(pts)
                path.Add(BRepBuilderAPI_MakeEdge(curve).Edge())
            elif cmd == "C":
                # Cubic Bezier
                pts = TColgp_Array1OfPnt(1, 4)
                pts.SetValue(1, last_pnt)
                pts.SetValue(2, gp_Pnt(params[0], params[1], 0))
                pts.SetValue(3, gp_Pnt(params[2], params[3], 0))
                last_pnt = gp_Pnt(params[4], params[5], 0)
                pts.SetValue(4, last_pnt)
                curve = Geom_BezierCurve(pts)
                path.Add(BRepBuilderAPI_MakeEdge(curve).Edge())
            elif cmd == "A":
                # Warning: Play at your own risk!
                x1, y1 = last_pnt.X(), last_pnt.Y()
                rx, ry, phi, large_arc_flag, sweep_flag, x2, y2 = params
                phi = radians(phi)
                pnt = gp_Pnt(x2, y2, 0)
                cx, cy, rx, ry = compute_arc_center(
                    x1, y1, rx, ry, phi, large_arc_flag, sweep_flag, x2, y2
                )
                z_dir = Z_DIR if sweep_flag else NEG_Z_DIR  # sweep_flag
                c = make_ellipse((cx, cy, 0), rx, ry, phi, z_dir)
                curve = GC_MakeArcOfEllipse(c, last_pnt, pnt, True).Value()
                path.Add(BRepBuilderAPI_MakeEdge(curve).Edge())
                last_pnt = pnt
            elif cmd == "Z":
                if not last_pnt.IsEqual(start_pnt, tol):
                    edge = BRepBuilderAPI_MakeEdge(last_pnt, start_pnt).Edge()
                    path.Add(edge)
                shapes.append(self.fill_shape(path.Wire()))
                path = None  # Close path
                last_pnt = start_pnt
        if path is not None:
            shape = path.Wire()
            if last_pnt.IsEqual(start_pnt, tol):
                shape = self.fill_shape(shape)
            shapes.append(shape)

        return shapes


class OccSvgPolyline(OccSvgNode):
    @property
    def points(self):
        return self.element.attrib.get("points", "")

    def create_shape(self):
        shape = BRepBuilderAPI_MakePolygon()
        for m in re.finditer(r"(\-?\d+\.?\d*),(\-?\d+\.?\d*)\s+", self.points):
            x, y = map(float, m.groups())
            shape.Add(gp_Pnt(x, y, 0))
        return shape.Wire()


class OccSvgPolygon(OccSvgNode):
    @property
    def points(self):
        return self.element.attrib.get("points", "")

    def create_shape(self):
        shape = BRepBuilderAPI_MakePolygon()
        for m in re.finditer(r"(\-?\d+\.?\d*),(\-?\d+\.?\d*)\s+", self.points):
            x, y = map(float, m.groups())
            shape.Add(gp_Pnt(x, y, 0))
        shape.Close()
        return self.fill_shape(shape.Wire())


class OccSvgGroup(OccSvgNode):
    # Do not warn for these
    excluded_tags = (
        "{http://www.w3.org/2000/svg}defs",
        "{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview",
        "{http://www.w3.org/2000/svg}metadata",
    )

    def create_shape(self):
        shapes = []
        t = self.transform
        for e in self.element:
            tag = e.tag
            if not isinstance(tag, str):
                continue  # Comment
            if not tag.startswith("{"):
                tag = "{http://www.w3.org/2000/svg}%s" % tag
            OccNode = SVG_NODES.get(tag)
            if OccNode is None:
                if tag not in self.excluded_tags:
                    warnings.warn("SVG tag %s is not yet supported." % tag)
                continue
            node = OccNode(element=e, svg=self.svg)
            result = node.create_shape()
            trsf = node.transform.Multiplied(t)
            if not isinstance(result, list):
                result = [result]
            for s in result:
                shapes.append(BRepBuilderAPI_Transform(s, trsf, True).Shape())
        return shapes


class OccSvgDoc(OccSvgGroup):
    """TODO: Handle transforms"""


SVG_NODES = {
    "{http://www.w3.org/2000/svg}ellipse": OccSvgEllipse,
    "{http://www.w3.org/2000/svg}circle": OccSvgCircle,
    "{http://www.w3.org/2000/svg}line": OccSvgLine,
    "{http://www.w3.org/2000/svg}rect": OccSvgRect,
    "{http://www.w3.org/2000/svg}path": OccSvgPath,
    "{http://www.w3.org/2000/svg}polyline": OccSvgPolyline,
    "{http://www.w3.org/2000/svg}polygon": OccSvgPolygon,
    "{http://www.w3.org/2000/svg}g": OccSvgGroup,
    "{http://www.w3.org/2000/svg}svg": OccSvgDoc,
}


[docs] class OccSvg(OccShape, ProxySvg): #: Update the class reference reference = set_default( "https://dev.opencascade.org/doc/refman/html/" "class_b_rep_builder_a_p_i___make_wire.html" ) #: Make wire shape = Instance(TopoDS_Shape) #: The document doc = Instance(OccSvgDoc) def create_shape(self): d = self.declaration if not d.source: return if os.path.exists(os.path.expanduser(d.source)): svg = etree.parse(os.path.expanduser(d.source)).getroot() else: svg = etree.fromstring(d.source) root = self.doc = OccSvgDoc(element=svg, svg=self) viewbox = svg.attrib.get("viewBox") x, y = (0, 0) sx, sy = (1, 1) if viewbox: x, y, iw, ih = map(parse_unit, viewbox.split()) ow = parse_unit(svg.attrib.get("width", iw)) oh = parse_unit(svg.attrib.get("height", ih)) sx = ow / iw sy = oh / ih builder = BRep_Builder() shape = TopoDS_Compound() builder.MakeCompound(shape) for s in root.create_shape(): builder.Add(shape, s) # Apply viewport scale t = gp_Trsf() t.SetValues(sx, 0, 0, x, 0, sy, 0, y, 0, 0, 1, 0) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() if d.scale != 1: bbox = self.get_bounding_box(shape) t = gp_Trsf() t.SetScale(gp_Pnt(*bbox.center), d.scale) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() if d.center: bbox = self.get_bounding_box(shape) x, y, z = bbox.center t = gp_Trsf() t.SetTranslation(gp_Vec(-x, -y, 0)) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() if d.mirror: bbox = self.get_bounding_box(shape) t = gp_Trsf() t.SetMirror(gp_Ax2(gp_Pnt(*bbox.center), gp_Dir(0, 1, 0))) shape = BRepBuilderAPI_Transform(shape, t, True).Shape() t = self.get_transform() shape = BRepBuilderAPI_Transform(shape, t, True).Shape() self.shape = shape def set_source(self, source): self.create_shape() def set_mirror(self, mirror): self.create_shape() def set_fill_mode(self, mode): self.create_shape() def set_center(self, center): self.create_shape()