예제 #1
0
파일: layout.py 프로젝트: johnnovak/twyg
    def __init__(self, config):
        properties = {
            'horizontalBalance': (NumberProperty, {}),
            'verticalAlignFactor': (NumberProperty, {
                'min': 0.0,
                'max': 1.0
            }),
            'rootPadX': (NumberProperty, {}),
            'nodePadX': (NumberProperty, {}),
            'nodePadY': (NumberProperty, {}),
            'branchPadY': (NumberProperty, {}),
            'sameWidthSiblings': (BooleanProperty, {}),
            'snapParentToChildren': (BooleanProperty, {}),
            'snapToHalfPositions': (BooleanProperty, {}),
            'radialMinNodes': (NumberProperty, {
                'min': 0.0
            }),
            'radialFactor': (NumberProperty, {})
        }

        self._props = Properties(properties, self._defaults_path('layout'),
                                 config)

        self._leftnodes = ()
        self._rightnodes = ()
예제 #2
0
파일: colorizer.py 프로젝트: johnnovak/twyg
    def __init__(self, childproperties, defaults, config,
                 colorscheme_path=None):

        properties = {
            'colorscheme':            (StringProperty,  {}),
            'fillColor':              (ColorProperty,   {}),
            'strokeColor':            (ColorProperty,   {}),
            'connectionColor':        (ColorProperty,   {}),
            'fontColor':              (ColorProperty,   {}),
            'fontColorAuto':          (BooleanProperty, {}),
            'fontColorAutoDark':      (ColorProperty,   {}),
            'fontColorAutoLight':     (ColorProperty,   {}),
            'fontColorAutoThreshold': (NumberProperty,  {'min': 0.0})
        }

        colorscheme_properties = {
            'backgroundColor': (ColorProperty, {}),
            'rootColor':       (ColorProperty, {}),
            'nodeColors':      (ArrayProperty, {'type': ColorProperty})
        }
        properties.update(childproperties)
        self._props = Properties(properties, self._defaults_path(defaults),
                                 config)

        E = self._eval_func()
        if not colorscheme_path:
            colorscheme_path = E('colorscheme')

        colorscheme = loadconfig(colors_path(colorscheme_path), flat=True)

        self._colorscheme_props = Properties(colorscheme_properties,
                                             'colorizer/colorscheme',
                                             colorscheme)
예제 #3
0
파일: node.py 프로젝트: johnnovak/twyg
    def __init__(self, childproperties, defaults, config):
        align = ('auto', 'left', 'right', 'center', 'justify')
        anchor = ('auto', 'center')

        properties = {
            'fontName': (StringProperty, {}),
            'fontSize': (NumberProperty, {
                'min': 0.0
            }),
            'lineHeight': (NumberProperty, {
                'min': 0.0
            }),
            'textAlign': (EnumProperty, {
                'values': align
            }),
            'justifyMinLines': (NumberProperty, {
                'min': 0.0
            }),
            'textBaselineCorrection': (NumberProperty, {}),
            'maxTextWidth': (NumberProperty, {
                'min': 0.0
            }),
            'hyphenate': (BooleanProperty, {}),
            'textPadX': (NumberProperty, {
                'min': 0.0
            }),
            'textPadY': (NumberProperty, {
                'min': 0.0
            }),
            'strokeWidth': (NumberProperty, {
                'min': 0.0
            }),
            'nodeDrawShadow': (BooleanProperty, {}),
            'nodeShadowColor': (ColorProperty, {}),
            'nodeShadowBlur': (NumberProperty, {
                'min': 0.0
            }),
            'nodeShadowOffsX': (NumberProperty, {}),
            'nodeShadowOffsY': (NumberProperty, {}),
            'textDrawShadow': (BooleanProperty, {}),
            'textShadowColor': (ColorProperty, {}),
            'textShadowOffsX': (NumberProperty, {}),
            'textShadowOffsY': (NumberProperty, {}),
            'drawGradient': (BooleanProperty, {}),
            'gradientTopColor': (ColorProperty, {}),
            'gradientBottomColor': (ColorProperty, {}),
            'connectionAnchorPoint': (EnumProperty, {
                'values': anchor
            })
        }
        properties.update(childproperties)
        self._props = Properties(properties, self._defaults_path(defaults),
                                 config)
        self._wraprect = True
예제 #4
0
    def __init__(self, config={}):
        properties = {
            'nodeLineWidthStart': (NumberProperty, {
                'min': 0.0
            }),
            'nodeLineWidthEnd': (NumberProperty, {
                'min': 0.0
            }),
            'nodeCx1Factor': (NumberProperty, {}),
            'nodeCx2Factor': (NumberProperty, {}),
            'nodeCy1Factor': (NumberProperty, {}),
            'nodeCy2Factor': (NumberProperty, {})
        }

        self._props = Properties(properties, defaults_path('curve'), config)
예제 #5
0
    def __init__(self, config={}):
        corner_styles = ('square', 'beveled', 'rounded')
        junction_styles = ('none', 'square', 'disc', 'diamond')
        junction_sign = ('none', 'plus', 'minus')

        properties = {
            'lineWidth':        (NumberProperty, {'min': 0.0}),
            'junctionXFactor':  (NumberProperty, {}),

            'cornerStyle':      (EnumProperty, {'values': corner_styles}),
            'cornerRadius':     (NumberProperty, {'min': 0.0}),
            'cornerPad':        (NumberProperty, {'min': 0.0}),

            'junctionStyle':    (EnumProperty,{'values': junction_styles}),

            'junctionRadius':       (NumberProperty, {'min': 0.0}),
            'junctionFillColor':    (ColorProperty,  {}),
            'junctionStrokeWidth':  (NumberProperty, {'min': 0.0}),
            'junctionStrokeColor':  (ColorProperty,  {}),

            'junctionSign':             (EnumProperty,
                                        {'values': junction_sign}),

            'junctionSignSize':         (NumberProperty, {'min': 0.0}),
            'junctionSignStrokeWidth':  (NumberProperty, {'min': 0.0}),
            'junctionSignColor':        (ColorProperty,  {})
        }

        self._props = Properties(properties, defaults_path('junction'),
                                 config)
예제 #6
0
    def __init__(self, config={}):
        corner_styles = ('square', 'beveled', 'rounded')
        junction_styles = ('none', 'square', 'disc', 'diamond')
        junction_sign = ('none', 'plus', 'minus')

        properties = {
            'lineWidth': (NumberProperty, {
                'min': 0.0
            }),
            'junctionXFactor': (NumberProperty, {}),
            'cornerStyle': (EnumProperty, {
                'values': corner_styles
            }),
            'cornerRadius': (NumberProperty, {
                'min': 0.0
            }),
            'cornerPad': (NumberProperty, {
                'min': 0.0
            }),
            'junctionStyle': (EnumProperty, {
                'values': junction_styles
            }),
            'junctionRadius': (NumberProperty, {
                'min': 0.0
            }),
            'junctionFillColor': (ColorProperty, {}),
            'junctionStrokeWidth': (NumberProperty, {
                'min': 0.0
            }),
            'junctionStrokeColor': (ColorProperty, {}),
            'junctionSign': (EnumProperty, {
                'values': junction_sign
            }),
            'junctionSignSize': (NumberProperty, {
                'min': 0.0
            }),
            'junctionSignStrokeWidth': (NumberProperty, {
                'min': 0.0
            }),
            'junctionSignColor': (ColorProperty, {})
        }

        self._props = Properties(properties, defaults_path('junction'), config)
예제 #7
0
    def __init__(self, config={}):
        properties = {
            'nodeLineWidthStart': (NumberProperty, {'min': 0.0}),
            'nodeLineWidthEnd':   (NumberProperty, {'min': 0.0}),
            'nodeCx1Factor':      (NumberProperty, {}),
            'nodeCx2Factor':      (NumberProperty, {}),
            'nodeCy1Factor':      (NumberProperty, {}),
            'nodeCy2Factor':      (NumberProperty, {})
        }

        self._props = Properties(properties, defaults_path('curve'), config)
예제 #8
0
파일: layout.py 프로젝트: johnnovak/twyg
    def __init__(self, config):
        properties = {
            'horizontalBalance':       (NumberProperty,  {}),
            'verticalAlignFactor':     (NumberProperty,  {'min': 0.0,
                                                          'max': 1.0}),
            'rootPadX':                (NumberProperty,  {}),
            'nodePadX':                (NumberProperty,  {}),
            'nodePadY':                (NumberProperty,  {}),
            'branchPadY':              (NumberProperty,  {}),
            'sameWidthSiblings':       (BooleanProperty, {}),
            'snapParentToChildren':    (BooleanProperty, {}),
            'snapToHalfPositions':     (BooleanProperty, {}),
            'radialMinNodes':          (NumberProperty,  {'min': 0.0}),
            'radialFactor':            (NumberProperty,  {})
        }

        self._props = Properties(properties, self._defaults_path('layout'),
                                 config)

        self._leftnodes = ()
        self._rightnodes = ()
예제 #9
0
파일: node.py 프로젝트: johnnovak/twyg
    def __init__(self, childproperties, defaults, config):
        align = ('auto', 'left', 'right', 'center', 'justify')
        anchor = ('auto', 'center')

        properties = {
            'fontName':               (StringProperty,  {}),
            'fontSize':               (NumberProperty,  {'min': 0.0}),
            'lineHeight':             (NumberProperty,  {'min': 0.0}),
            'textAlign':              (EnumProperty,    {'values': align}),
            'justifyMinLines':        (NumberProperty,  {'min': 0.0}),
            'textBaselineCorrection': (NumberProperty,  {}),
            'maxTextWidth':           (NumberProperty,  {'min': 0.0}),
            'hyphenate':              (BooleanProperty, {}),
            'textPadX':               (NumberProperty,  {'min': 0.0}),
            'textPadY':               (NumberProperty,  {'min': 0.0}),

            'strokeWidth':            (NumberProperty,  {'min': 0.0}),

            'nodeDrawShadow':         (BooleanProperty, {}),
            'nodeShadowColor':        (ColorProperty,   {}),
            'nodeShadowBlur':         (NumberProperty,  {'min': 0.0}),
            'nodeShadowOffsX':        (NumberProperty,  {}),
            'nodeShadowOffsY':        (NumberProperty,  {}),

            'textDrawShadow':         (BooleanProperty, {}),
            'textShadowColor':        (ColorProperty,   {}),
            'textShadowOffsX':        (NumberProperty,  {}),
            'textShadowOffsY':        (NumberProperty,  {}),

            'drawGradient':           (BooleanProperty, {}),
            'gradientTopColor':       (ColorProperty,   {}),
            'gradientBottomColor':    (ColorProperty,   {}),

            'connectionAnchorPoint':  (EnumProperty,    {'values': anchor})
        }
        properties.update(childproperties)
        self._props = Properties(properties, self._defaults_path(defaults),
                                 config)
        self._wraprect = True
예제 #10
0
파일: node.py 프로젝트: johnnovak/twyg
class NodeDrawer(object):
    def __init__(self, childproperties, defaults, config):
        align = ('auto', 'left', 'right', 'center', 'justify')
        anchor = ('auto', 'center')

        properties = {
            'fontName': (StringProperty, {}),
            'fontSize': (NumberProperty, {
                'min': 0.0
            }),
            'lineHeight': (NumberProperty, {
                'min': 0.0
            }),
            'textAlign': (EnumProperty, {
                'values': align
            }),
            'justifyMinLines': (NumberProperty, {
                'min': 0.0
            }),
            'textBaselineCorrection': (NumberProperty, {}),
            'maxTextWidth': (NumberProperty, {
                'min': 0.0
            }),
            'hyphenate': (BooleanProperty, {}),
            'textPadX': (NumberProperty, {
                'min': 0.0
            }),
            'textPadY': (NumberProperty, {
                'min': 0.0
            }),
            'strokeWidth': (NumberProperty, {
                'min': 0.0
            }),
            'nodeDrawShadow': (BooleanProperty, {}),
            'nodeShadowColor': (ColorProperty, {}),
            'nodeShadowBlur': (NumberProperty, {
                'min': 0.0
            }),
            'nodeShadowOffsX': (NumberProperty, {}),
            'nodeShadowOffsY': (NumberProperty, {}),
            'textDrawShadow': (BooleanProperty, {}),
            'textShadowColor': (ColorProperty, {}),
            'textShadowOffsX': (NumberProperty, {}),
            'textShadowOffsY': (NumberProperty, {}),
            'drawGradient': (BooleanProperty, {}),
            'gradientTopColor': (ColorProperty, {}),
            'gradientBottomColor': (ColorProperty, {}),
            'connectionAnchorPoint': (EnumProperty, {
                'values': anchor
            })
        }
        properties.update(childproperties)
        self._props = Properties(properties, self._defaults_path(defaults),
                                 config)
        self._wraprect = True

    # TODO util function in common?
    def _defaults_path(self, conf):
        return os.path.join('node', conf)

    def _eval_func(self, node):
        if node:
            vars = {
                'depth': node.depth(),
                'numChildren': len(node.getchildren())
            }
        else:
            vars = {}
        return lambda name: self._props.eval(name, node, vars)

    def precalc_node(self, node):
        """
        Precalculate node properties that are needed by the layout algorithms.
        """

        E = self._eval_func(node)

        node.fontsize = E('fontSize')
        self._precalc_text(node)

        padx = E('textPadX')
        pady = E('textPadY')

        node.width = node.textwidth + padx * 2
        node.height = node.textheight + pady * 2
        node.bboxwidth = node.width
        node.bboxheight = node.height

        node._textxoffs = padx
        node._textyoffs = pady

        node.text_has_background = True

        y = node.bboxheight / 2
        node._conn_point_left = Vector2(0, y)
        node._conn_point_right = Vector2(node.bboxwidth, y)

    def connection_point(self, node, direction):
        E = self._eval_func(node)

        anchor = E('connectionAnchorPoint')

        if anchor == 'auto':
            if direction == Direction.Left:
                p = node._conn_point_left
            else:
                p = node._conn_point_right

            x, y = p.x, p.y

        elif anchor == 'center':
            x = node.bboxwidth / 2
            y = node.bboxheight / 2

        return node.x + x, node.y + y

    def draw(self, node):
        """
        Draw the node at its (x,y) anchor point. Relies on internal
        properties precalculated by precalc_node.
        """

        E = self._eval_func(node)

        path = self._calc_shape_path(node)

        _ctx.push()
        _ctx.translate(node.x, node.y)
        _ctx.fill(node.fillcolor)
        _ctx.stroke(node.strokecolor)
        _ctx.strokewidth(E('strokeWidth'))

        if E('nodeDrawShadow'):
            _ctx.shadow(dx=E('nodeShadowOffsX'),
                        dy=E('nodeShadowOffsY'),
                        blur=E('nodeShadowBlur'),
                        clr=E('nodeShadowColor'))

        self._draw_gradient_shape(node, path, node.fillcolor)
        _ctx.noshadow()

        # Draw text shadow
        if E('textDrawShadow'):
            shadowcolor = E('textShadowColor')
            _ctx.fill(shadowcolor)

            self._drawtext(node, node._textxoffs - E('textShadowOffsX'),
                           node._textyoffs - E('textShadowOffsY'))

        _ctx.fill(node.fontcolor)
        self._drawtext(node, node._textxoffs, node._textyoffs)

        _ctx.pop()

    def _draw_gradient_shape(self, node, path, basecolor):
        E = self._eval_func(node)

        if E('drawGradient'):
            _ctx.gradientfill(path,
                              E('gradientBottomColor'),
                              E('gradientTopColor'),
                              type='linear')
        else:
            _ctx.fill(basecolor)
            _ctx.drawpath(path)

    def _precalc_text(self, node):
        E = self._eval_func(node)

        node.fontname = E('fontName')
        node.lineheight = E('lineHeight')
        node.max_text_width = E('maxTextWidth')
        node.hyphenate = E('hyphenate')

        _ctx.font(node.fontname, node.fontsize)
        _ctx.lineheight(node.lineheight)

        lineheight = node.lineheight * node.fontsize
        textwidth_func = lambda (txt): textwidth(_ctx, txt, node.fontname, node
                                                 .fontsize)

        if self._wraprect:
            (node._textlines, node._textlinewidths, node._textrects,
             node.textwidth,
             node.textheight) = textwrap.wrap_rect(node.label, lineheight,
                                                   textwidth_func,
                                                   node.max_text_width)
        else:
            (node._textlines, node._textlinewidths, node._textrects,
             node.textwidth,
             node.textheight) = textwrap.wrap_shape(node.label,
                                                    lineheight,
                                                    textwidth_func,
                                                    self._shapefunc,
                                                    hyphenate=node.hyphenate,
                                                    **self._shapefunc_args)

    def _calc_shape_path(self, node):
        raise NotImplementedError

    def _drawtext(self, node, xoffs, yoffs):
        E = self._eval_func(node)

        if not node._textrects:
            return

        # Text alignment
        alignment = LEFT
        text_align = E('textAlign')
        if text_align == 'right': alignment = RIGHT
        elif text_align == 'center': alignment = CENTER
        elif text_align == 'justify': alignment = JUSTIFY
        elif text_align == 'auto':
            if node.isroot():
                alignment = CENTER
            else:
                alignment = (RIGHT
                             if node.direction() == Direction.Left else LEFT)

        # Draw text
        baseline_corr = E('textBaselineCorrection')

        _ctx.font(node.fontname, node.fontsize)
        _ctx.lineheight(node.lineheight)

        justify_min_lines = E('justifyMinLines')
        ystep = node._textrects[0].h

        nonblank_lines = 0
        for l in node._textlines:
            if l:
                nonblank_lines += 1

        if nonblank_lines <= justify_min_lines:
            self._center_text_vertically(node)

        baseline_offs = node.fontsize * baseline_corr
        lineheight_offs = -(node.lineheight - 1) / 2 * node.fontsize

        if alignment == JUSTIFY and nonblank_lines <= justify_min_lines:
            alignment = CENTER

        if alignment == JUSTIFY:
            spacewidth = textwidth(_ctx, ' ', node.fontname, node.fontsize)

            for i, l in enumerate(node._textlines):
                x, y, w, h = node._textrects[i].params()
                x += xoffs
                y += yoffs + baseline_offs + lineheight_offs + ystep

                if DEBUG:
                    _ctx.save()
                    _ctx.nofill()
                    _ctx.stroke(node.fontcolor)
                    _ctx.rect(x, y - h, w, h)
                    _ctx.stroke(1, 0, 0)
                    _ctx.rect(tx, ty, node.textwidth, node.textheight)
                    _ctx.restore()

                words = l.split()
                numspaces = float(l.count(' '))
                x_spacing = spacewidth
                if numspaces:
                    charswidth = (node._textlinewidths[i] -
                                  numspaces * spacewidth)
                    x_spacing = (w - charswidth) / numspaces

                # TODO remove 7 magic constant
                if ((i == 0 or i == len(node._textlines) - 1)
                        and (x_spacing > spacewidth * 7 or len(words) == 1)):

                    x_spacing = spacewidth
                    x += (w - node._textlinewidths[i]) / 2.

                for w in words:
                    _ctx.text(w, x, y)
                    x += x_spacing + textwidth(_ctx, w, node.fontname,
                                               node.fontsize)
        else:
            for i, l in enumerate(node._textlines):
                x, y, w, h = node._textrects[i].params()
                x += xoffs
                y += yoffs + ystep

                if DEBUG:
                    _ctx.save()
                    _ctx.nofill()
                    _ctx.stroke(node.fontcolor)
                    _ctx.rect(x, y - h, w, h)
                    _ctx.stroke(1, 0, 0)
                    _ctx.rect(tx, ty, node.textwidth, node.textheight)
                    _ctx.restore()

                y += baseline_offs + lineheight_offs

                if alignment == RIGHT:
                    x += w - node._textlinewidths[i]
                elif alignment == CENTER:
                    x += (w - node._textlinewidths[i]) / 2.

                _ctx.text(l, x, y)

    def _center_text_vertically(self, node):
        """
        Shift rects so that the text will appear vertically centered
        within a containing rectangle of the specified height, taking blank
        lines into account as well.

        lines    -- list of strings containing the text in each line
        rects    -- list of equi-height rectangles in [x, y, w, h] format
        height   -- height of the containing rectangle
        """

        if not node._textrects:
            return

        preblanks = 0
        for l in node._textlines:
            if not l:
                preblanks += 1
            else:
                break

        postblanks = 0
        for l in reversed(node._textlines):
            if not l:
                postblanks += 1
            else:
                break

        lineheight = node._textrects[0].h

        numnonblanks = len(node._textlines) - preblanks - postblanks
        textheight = numnonblanks * lineheight
        ystart = (node.textheight - textheight) / 2.
        ystart -= preblanks * lineheight

        yoffs = ystart - node._textrects[0].y

        for r in node._textrects:
            r.y += yoffs
예제 #11
0
파일: colorizer.py 프로젝트: johnnovak/twyg
class Colorizer(object):

    def __init__(self, childproperties, defaults, config,
                 colorscheme_path=None):

        properties = {
            'colorscheme':            (StringProperty,  {}),
            'fillColor':              (ColorProperty,   {}),
            'strokeColor':            (ColorProperty,   {}),
            'connectionColor':        (ColorProperty,   {}),
            'fontColor':              (ColorProperty,   {}),
            'fontColorAuto':          (BooleanProperty, {}),
            'fontColorAutoDark':      (ColorProperty,   {}),
            'fontColorAutoLight':     (ColorProperty,   {}),
            'fontColorAutoThreshold': (NumberProperty,  {'min': 0.0})
        }

        colorscheme_properties = {
            'backgroundColor': (ColorProperty, {}),
            'rootColor':       (ColorProperty, {}),
            'nodeColors':      (ArrayProperty, {'type': ColorProperty})
        }
        properties.update(childproperties)
        self._props = Properties(properties, self._defaults_path(defaults),
                                 config)

        E = self._eval_func()
        if not colorscheme_path:
            colorscheme_path = E('colorscheme')

        colorscheme = loadconfig(colors_path(colorscheme_path), flat=True)

        self._colorscheme_props = Properties(colorscheme_properties,
                                             'colorizer/colorscheme',
                                             colorscheme)

    # TODO util function in common?
    def _defaults_path(self, conf):
        return os.path.join('colorizer', conf)

    def _eval_func(self, node=None):
        # TODO duplicate from node.py
        if node:
            vars = {
                'depth':       node.depth(),
                'numChildren': len(node.getchildren()),
                'bgColor':     self.background_color()
            }
        else:
            vars = {}
        return lambda name: self._props.eval(name, node, vars)

    def colorize(self, node):
        C = self._colorscheme_props.eval

        node.bgcolor = self.background_color()
        self._set_basecolor(node)

        E = self._eval_func(node)

        node.fillcolor = E('fillColor')
        node.strokecolor = E('strokeColor')
        node.connectioncolor = E('connectionColor')

        # Determine font color
        if E('fontColorAuto'):
            node.fontcolor = self._calc_auto_textcolor(node)
        else:
            node.fontcolor = E('fontColor')

    def background_color(self):
        C = self._colorscheme_props.eval
        return C('backgroundColor')

    def _calc_auto_textcolor(self, node):
        E = self._eval_func(node)

        text_bgcolor = (node.fillcolor if node.text_has_background
                                       else self.background_color())
        textcolor = node.fillcolor

        if abs(  brightness(text_bgcolor)
               - brightness(textcolor)) < E('fontColorAutoThreshold'):
            textcolor_dark  = E('fontColorAutoDark')
            textcolor_light = E('fontColorAutoLight')

            b = brightness(text_bgcolor)

            if (  abs(b - brightness(textcolor_dark))
                > abs(b - brightness(textcolor_light))):
                textcolor = textcolor_dark
            else:
                textcolor = textcolor_light

        return textcolor

    def _set_basecolor(self, node):
        raise NotImplementedError
예제 #12
0
파일: node.py 프로젝트: johnnovak/twyg
class NodeDrawer(object):

    def __init__(self, childproperties, defaults, config):
        align = ('auto', 'left', 'right', 'center', 'justify')
        anchor = ('auto', 'center')

        properties = {
            'fontName':               (StringProperty,  {}),
            'fontSize':               (NumberProperty,  {'min': 0.0}),
            'lineHeight':             (NumberProperty,  {'min': 0.0}),
            'textAlign':              (EnumProperty,    {'values': align}),
            'justifyMinLines':        (NumberProperty,  {'min': 0.0}),
            'textBaselineCorrection': (NumberProperty,  {}),
            'maxTextWidth':           (NumberProperty,  {'min': 0.0}),
            'hyphenate':              (BooleanProperty, {}),
            'textPadX':               (NumberProperty,  {'min': 0.0}),
            'textPadY':               (NumberProperty,  {'min': 0.0}),

            'strokeWidth':            (NumberProperty,  {'min': 0.0}),

            'nodeDrawShadow':         (BooleanProperty, {}),
            'nodeShadowColor':        (ColorProperty,   {}),
            'nodeShadowBlur':         (NumberProperty,  {'min': 0.0}),
            'nodeShadowOffsX':        (NumberProperty,  {}),
            'nodeShadowOffsY':        (NumberProperty,  {}),

            'textDrawShadow':         (BooleanProperty, {}),
            'textShadowColor':        (ColorProperty,   {}),
            'textShadowOffsX':        (NumberProperty,  {}),
            'textShadowOffsY':        (NumberProperty,  {}),

            'drawGradient':           (BooleanProperty, {}),
            'gradientTopColor':       (ColorProperty,   {}),
            'gradientBottomColor':    (ColorProperty,   {}),

            'connectionAnchorPoint':  (EnumProperty,    {'values': anchor})
        }
        properties.update(childproperties)
        self._props = Properties(properties, self._defaults_path(defaults),
                                 config)
        self._wraprect = True

    # TODO util function in common?
    def _defaults_path(self, conf):
        return os.path.join('node', conf)

    def _eval_func(self, node):
        if node:
            vars = {
                'depth':       node.depth(),
                'numChildren': len(node.getchildren())
            }
        else:
            vars = {}
        return lambda name: self._props.eval(name, node, vars)

    def precalc_node(self, node):
        """
        Precalculate node properties that are needed by the layout algorithms.
        """

        E = self._eval_func(node)

        node.fontsize = E('fontSize')
        self._precalc_text(node)

        padx = E('textPadX')
        pady = E('textPadY')

        node.width  = node.textwidth  + padx * 2
        node.height = node.textheight + pady * 2
        node.bboxwidth = node.width
        node.bboxheight = node.height

        node._textxoffs = padx
        node._textyoffs = pady

        node.text_has_background = True

        y = node.bboxheight / 2
        node._conn_point_left = Vector2(0, y)
        node._conn_point_right = Vector2(node.bboxwidth, y)


    def connection_point(self, node, direction):
        E = self._eval_func(node)

        anchor = E('connectionAnchorPoint')

        if anchor == 'auto':
            if direction == Direction.Left:
                p = node._conn_point_left
            else:
                p = node._conn_point_right

            x, y = p.x, p.y

        elif anchor == 'center':
            x = node.bboxwidth / 2
            y = node.bboxheight / 2

        return node.x + x, node.y + y

    def draw(self, node):
        """
        Draw the node at its (x,y) anchor point. Relies on internal
        properties precalculated by precalc_node.
        """

        E = self._eval_func(node)

        path = self._calc_shape_path(node)

        _ctx.push()
        _ctx.translate(node.x, node.y)
        _ctx.fill(node.fillcolor)
        _ctx.stroke(node.strokecolor)
        _ctx.strokewidth(E('strokeWidth'))

        if E('nodeDrawShadow'):
            _ctx.shadow(dx=E('nodeShadowOffsX'), dy=E('nodeShadowOffsY'),
                        blur=E('nodeShadowBlur'), clr=E('nodeShadowColor'))

        self._draw_gradient_shape(node, path, node.fillcolor)
        _ctx.noshadow()

        # Draw text shadow
        if E('textDrawShadow'):
            shadowcolor = E('textShadowColor');
            _ctx.fill(shadowcolor)

            self._drawtext(node, node._textxoffs - E('textShadowOffsX'),
                                 node._textyoffs - E('textShadowOffsY'))

        _ctx.fill(node.fontcolor)
        self._drawtext(node, node._textxoffs, node._textyoffs)

        _ctx.pop()

    def _draw_gradient_shape(self, node, path, basecolor):
        E = self._eval_func(node)

        if E('drawGradient'):
            _ctx.gradientfill(path,
                              E('gradientBottomColor'), E('gradientTopColor'),
                              type='linear')
        else:
            _ctx.fill(basecolor)
            _ctx.drawpath(path)

    def _precalc_text(self, node):
        E = self._eval_func(node)

        node.fontname       = E('fontName')
        node.lineheight     = E('lineHeight')
        node.max_text_width = E('maxTextWidth')
        node.hyphenate      = E('hyphenate')

        _ctx.font(node.fontname, node.fontsize)
        _ctx.lineheight(node.lineheight)

        lineheight = node.lineheight * node.fontsize
        textwidth_func = lambda(txt): textwidth(_ctx, txt,
                                                node.fontname, node.fontsize)

        if self._wraprect:
            (node._textlines, node._textlinewidths, node._textrects,
             node.textwidth,
             node.textheight) = textwrap.wrap_rect(node.label, lineheight,
                                                   textwidth_func,
                                                   node.max_text_width)
        else:
            (node._textlines, node._textlinewidths, node._textrects,
             node.textwidth,
             node.textheight) = textwrap.wrap_shape(node.label, lineheight,
                                                    textwidth_func,
                                                    self._shapefunc,
                                                    hyphenate=node.hyphenate,
                                                    **self._shapefunc_args)

    def _calc_shape_path(self, node):
        raise NotImplementedError

    def _drawtext(self, node, xoffs, yoffs):
        E = self._eval_func(node)

        if not node._textrects:
            return

        # Text alignment
        alignment = LEFT
        text_align = E('textAlign')
        if   text_align == 'right':   alignment = RIGHT
        elif text_align == 'center':  alignment = CENTER
        elif text_align == 'justify': alignment = JUSTIFY
        elif text_align == 'auto':
            if node.isroot():
                alignment = CENTER
            else:
                alignment = (RIGHT if node.direction() == Direction.Left
                             else LEFT)

        # Draw text
        baseline_corr = E('textBaselineCorrection')

        _ctx.font(node.fontname, node.fontsize)
        _ctx.lineheight(node.lineheight)

        justify_min_lines = E('justifyMinLines')
        ystep = node._textrects[0].h

        nonblank_lines = 0
        for l in node._textlines:
            if l:
                nonblank_lines += 1

        if nonblank_lines <= justify_min_lines:
            self._center_text_vertically(node)

        baseline_offs = node.fontsize * baseline_corr
        lineheight_offs = -(node.lineheight - 1) / 2 * node.fontsize

        if alignment == JUSTIFY and nonblank_lines <= justify_min_lines:
            alignment = CENTER

        if alignment == JUSTIFY:
            spacewidth = textwidth(_ctx, ' ', node.fontname, node.fontsize)

            for i, l in enumerate(node._textlines):
                x, y, w, h = node._textrects[i].params()
                x += xoffs
                y += yoffs + baseline_offs + lineheight_offs + ystep

                if DEBUG:
                    _ctx.save()
                    _ctx.nofill()
                    _ctx.stroke(node.fontcolor)
                    _ctx.rect(x, y - h, w, h)
                    _ctx.stroke(1, 0, 0)
                    _ctx.rect(tx, ty, node.textwidth, node.textheight)
                    _ctx.restore()

                words = l.split()
                numspaces = float(l.count(' '))
                x_spacing = spacewidth
                if numspaces:
                    charswidth = (node._textlinewidths[i]
                                  - numspaces * spacewidth)
                    x_spacing = (w - charswidth) / numspaces

                # TODO remove 7 magic constant
                if ((i == 0 or i == len(node._textlines) - 1)
                    and (x_spacing > spacewidth * 7 or len(words) == 1)):

                    x_spacing = spacewidth
                    x += (w - node._textlinewidths[i]) / 2.

                for w in words:
                    _ctx.text(w, x, y)
                    x += x_spacing + textwidth(_ctx, w,
                                               node.fontname, node.fontsize)
        else:
            for i, l in enumerate(node._textlines):
                x, y, w, h = node._textrects[i].params()
                x += xoffs
                y += yoffs + ystep

                if DEBUG:
                    _ctx.save()
                    _ctx.nofill()
                    _ctx.stroke(node.fontcolor)
                    _ctx.rect(x, y - h, w, h)
                    _ctx.stroke(1, 0, 0)
                    _ctx.rect(tx, ty, node.textwidth, node.textheight)
                    _ctx.restore()

                y += baseline_offs + lineheight_offs

                if alignment == RIGHT:
                    x += w - node._textlinewidths[i]
                elif alignment == CENTER:
                    x += (w - node._textlinewidths[i]) / 2.

                _ctx.text(l, x, y)

    def _center_text_vertically(self, node):
        """
        Shift rects so that the text will appear vertically centered
        within a containing rectangle of the specified height, taking blank
        lines into account as well.

        lines    -- list of strings containing the text in each line
        rects    -- list of equi-height rectangles in [x, y, w, h] format
        height   -- height of the containing rectangle
        """

        if not node._textrects:
            return

        preblanks = 0
        for l in node._textlines:
            if not l:
                preblanks += 1
            else:
                break

        postblanks = 0
        for l in reversed(node._textlines):
            if not l:
                postblanks += 1
            else:
                break

        lineheight = node._textrects[0].h

        numnonblanks = len(node._textlines) - preblanks - postblanks
        textheight = numnonblanks * lineheight
        ystart = (node.textheight - textheight) / 2.
        ystart -= preblanks * lineheight

        yoffs = ystart - node._textrects[0].y

        for r in node._textrects:
            r.y += yoffs
예제 #13
0
class JunctionConnectionDrawer(object):
    def __init__(self, config={}):
        corner_styles = ('square', 'beveled', 'rounded')
        junction_styles = ('none', 'square', 'disc', 'diamond')
        junction_sign = ('none', 'plus', 'minus')

        properties = {
            'lineWidth': (NumberProperty, {
                'min': 0.0
            }),
            'junctionXFactor': (NumberProperty, {}),
            'cornerStyle': (EnumProperty, {
                'values': corner_styles
            }),
            'cornerRadius': (NumberProperty, {
                'min': 0.0
            }),
            'cornerPad': (NumberProperty, {
                'min': 0.0
            }),
            'junctionStyle': (EnumProperty, {
                'values': junction_styles
            }),
            'junctionRadius': (NumberProperty, {
                'min': 0.0
            }),
            'junctionFillColor': (ColorProperty, {}),
            'junctionStrokeWidth': (NumberProperty, {
                'min': 0.0
            }),
            'junctionStrokeColor': (ColorProperty, {}),
            'junctionSign': (EnumProperty, {
                'values': junction_sign
            }),
            'junctionSignSize': (NumberProperty, {
                'min': 0.0
            }),
            'junctionSignStrokeWidth': (NumberProperty, {
                'min': 0.0
            }),
            'junctionSignColor': (ColorProperty, {})
        }

        self._props = Properties(properties, defaults_path('junction'), config)

    def _eval_func(self, node):
        return lambda name: self._props.eval(name, node)

    def draw(self, node):
        if node.isroot():
            self._draw(node, Direction.Left)
            self._draw(node, Direction.Right)
        else:
            self._draw(node)

    def _draw(self, node, direction=None):
        """
        Draw a curved connection between a node and its child nodes.
        """

        E = self._eval_func(node)

        children = node.getchildren(direction)
        if not children:
            return

        linewidth = E('lineWidth')

        _ctx.autoclosepath(True)
        _ctx.stroke(node.connectioncolor)
        _ctx.fill(node.connectioncolor)
        _ctx.strokewidth(linewidth)

        firstchild = children[0]
        lastchild = children[-1]

        direction = firstchild.direction()
        opp_direction = opposite_dir(direction)
        x1, y1 = node.connection_point(direction)
        xfirst, yfirst = firstchild.connection_point(opp_direction)

        # Special case: draw straight line if there's only one child
        if len(children) == 1:
            _ctx.line(x1, y1, xfirst, yfirst)
            return

        # Calculate junction point position
        jx = x1 + (xfirst - x1) * E('junctionXFactor')
        jy = y1

        # Draw line from parent node to junction point
        _ctx.line(x1, y1, jx, jy)

        # Limit first & last corner radius to the available area
        ylast = lastchild.connection_point(opp_direction)[1]
        ysecond = children[1].connection_point(opp_direction)[1]
        ypenultimate = children[-2].connection_point(opp_direction)[1]

        # Starting corner radius
        cornerPad = E('cornerPad')
        r = min(E('cornerRadius'), abs(jx - xfirst) - cornerPad)
        r = max(r, 0)

        # Adjusted first (top) corner radius
        r1 = min(r, abs(yfirst - jy) - cornerPad)
        r1 = max(r1, 0)
        if ysecond < jy:
            r1 = min(r, abs(yfirst - ysecond) - cornerPad)
            r1 = max(r1, 0)

        # Adjusted last (bottom) corner radius
        r2 = min(r, abs(ylast - jy) - cornerPad)
        r2 = max(r2, 0)
        if ypenultimate > jy:
            r2 = min(r, abs(ylast - ypenultimate) - cornerPad)
            r2 = max(r2, 0)

        # Draw main branch as a single path to ensure line continuity
        p1 = Vector2(jx, yfirst + r1)
        p2 = Vector2(jx, ylast - r2)
        segments = [[p1, p2]]

        corner_style = E('cornerStyle')

        for i, child in enumerate(children):
            direction = child.direction()
            opp_direction = opposite_dir(direction)

            x2, y2 = child.connection_point(opp_direction)
            if direction == Direction.Left:
                x2 -= linewidth / 2
            elif direction == Direction.Right:
                x2 += linewidth / 2

            # Draw corners
            if direction == Direction.Left:
                a1 = 90
                da = -90
                dx1 = r1 * 2
                dx2 = r2 * 2
            else:
                a1 = da = 90
                dx1 = dx2 = 0

            x1 = jx
            if child is firstchild:
                x1 += -r1 if direction == Direction.Left else r1

                if (corner_style == 'square' or abs(y2 - jy) < .001):
                    p1 = Vector2(jx, y2)
                    p2 = Vector2(jx, y2 + r1)
                    segments.insert(0, [p1, p2])

                    p1 = Vector2(x1, y2)
                    p2 = Vector2(jx, y2)
                    segments.insert(0, [p1, p2])

                elif corner_style == 'beveled':
                    p1 = Vector2(x1, y2)
                    p2 = Vector2(jx, y2 + r1)
                    segments.insert(0, [p1, p2])

                elif corner_style == 'rounded':
                    arc = arcpath(jx - dx1, y2, r1 * 2, r1 * 2, a1, da)
                    segments = arc + segments

                p1 = Vector2(x2, y2)
                p2 = Vector2(x1, y2)
                segments.insert(0, [p1, p2])

            elif child is lastchild:
                x1 += -r2 if direction == Direction.Left else r2

                if (corner_style == 'square' or abs(y2 - jy) < .001):
                    p1 = Vector2(jx, y2 - r2)
                    p2 = Vector2(jx, y2)
                    segments.append([p1, p2])

                    p1 = Vector2(jx, y2)
                    p2 = Vector2(x1, y2)
                    segments.append([p1, p2])

                elif corner_style == 'beveled':
                    p1 = Vector2(jx, y2 - r2)
                    p2 = Vector2(x1, y2)
                    segments.append([p1, p2])

                elif corner_style == 'rounded':
                    arc = arcpath(jx - dx2, y2 - r2 * 2, r2 * 2, r2 * 2,
                                  a1 + da, da)
                    segments = segments + arc

                p1 = Vector2(x1, y2)
                p2 = Vector2(x2, y2)
                segments.append([p1, p2])

            else:
                _ctx.line(x1, y2, x2, y2)

        # Draw main branch path
        _ctx.nofill()

        path = createpath(_ctx, segments, close=False)
        _ctx.drawpath(path)

        # Draw junction point
        style = E('junctionStyle')
        if style == 'none':
            return

        r = E('junctionRadius')
        r2 = r / 2.

        _ctx.fill(E('junctionFillColor'))
        _ctx.stroke(E('junctionStrokeColor'))
        _ctx.strokewidth(E('junctionStrokeWidth'))

        if style == 'square':
            _ctx.rect(jx - r2, jy - r2, r, r)

        elif style == 'disc':
            _ctx.oval(jx - r2, jy - r2, r, r)

        elif style == 'diamond':
            _ctx.beginpath(jx, jy - r2)
            _ctx.lineto(jx + r2, jy)
            _ctx.lineto(jx, jy + r2)
            _ctx.lineto(jx - r2, jy)
            _ctx.lineto(jx, jy - r2)
            _ctx.endpath()

        # Draw junction sign
        sign = E('junctionSign')
        if sign == 'none':
            return

        _ctx.stroke(E('junctionSignColor'))

        d = E('junctionSignSize') / 2.
        _ctx.strokewidth(E('junctionSignStrokeWidth'))

        if sign in ('minus', 'plus'):
            _ctx.line(jx - d, jy, jx + d, jy)

        if sign == 'plus':
            _ctx.line(jx, jy - d, jx, jy + d)
예제 #14
0
class CurveConnectionDrawer(object):
    def __init__(self, config={}):
        properties = {
            'nodeLineWidthStart': (NumberProperty, {
                'min': 0.0
            }),
            'nodeLineWidthEnd': (NumberProperty, {
                'min': 0.0
            }),
            'nodeCx1Factor': (NumberProperty, {}),
            'nodeCx2Factor': (NumberProperty, {}),
            'nodeCy1Factor': (NumberProperty, {}),
            'nodeCy2Factor': (NumberProperty, {})
        }

        self._props = Properties(properties, defaults_path('curve'), config)

    def _eval_func(self, node):
        return lambda name: self._props.eval(name, node)

    def draw(self, node):
        """
        Draw a curved connection between a node and its child nodes.
        """

        E = self._eval_func(node)

        if node.isleaf():
            return

        _ctx.autoclosepath(True)
        _ctx.stroke(node.connectioncolor)
        _ctx.fill(node.connectioncolor)

        children = node.children

        for child in children:
            linewidth = E('nodeLineWidthEnd')

            _ctx.strokewidth(linewidth)

            direction = child.direction()
            opp_direction = opposite_dir(direction)

            x1, y1 = node.connection_point(direction)
            x2, y2 = child.connection_point(opp_direction)

            if direction == Direction.Left:
                x2 -= linewidth / 2
            elif direction == Direction.Right:
                x2 += linewidth / 2

            if len(children) == 1:
                _ctx.line(x1, y1, x2, y2)
            else:
                cx1 = (x2 - x1) * E('nodeCx1Factor')
                cx2 = (x2 - x1) * E('nodeCx2Factor')

                cy1 = (y2 - y1) * E('nodeCy1Factor')
                cy2 = (y2 - y1) * E('nodeCy2Factor')

                p1x = x1 + cx1
                p1y = y1 + cy1
                p2x = x2 - cx2
                p2y = y2 - cy2

                startwidth = E('nodeLineWidthStart') - 1
                sw = startwidth / 2.

                _ctx.beginpath(x1, y1 - sw)
                _ctx.curveto(p1x, p1y, p2x, p2y, x2, y2)
                _ctx.curveto(p2x, p2y, p1x, p1y, x1, y1 + sw)
                _ctx.endpath()
예제 #15
0
파일: layout.py 프로젝트: johnnovak/twyg
class Layout(object):
    def __init__(self, config):
        properties = {
            'horizontalBalance': (NumberProperty, {}),
            'verticalAlignFactor': (NumberProperty, {
                'min': 0.0,
                'max': 1.0
            }),
            'rootPadX': (NumberProperty, {}),
            'nodePadX': (NumberProperty, {}),
            'nodePadY': (NumberProperty, {}),
            'branchPadY': (NumberProperty, {}),
            'sameWidthSiblings': (BooleanProperty, {}),
            'snapParentToChildren': (BooleanProperty, {}),
            'snapToHalfPositions': (BooleanProperty, {}),
            'radialMinNodes': (NumberProperty, {
                'min': 0.0
            }),
            'radialFactor': (NumberProperty, {})
        }

        self._props = Properties(properties, self._defaults_path('layout'),
                                 config)

        self._leftnodes = ()
        self._rightnodes = ()

    # TODO util function in common?
    def _defaults_path(self, conf):
        return os.path.join('layout', conf)

    def _eval_func(self, node=None, direction=None):
        if node:
            vars = {'childrenHeight': self.childrenheight(node, direction)}
        else:
            vars = {}
        return lambda name: self._props.eval(name, node, vars)

    def precalc_layout(self, root):
        self.root = root
        self._splitnodes()

    def node_orientation(self, node):
        if node.isroot():
            return None
        elif node.ancestor(-1) in self._leftnodes:
            return Direction.Left
        elif node.ancestor(-1) in self._rightnodes:
            return Direction.Right

    def _splitnodes(self):
        """
        Split the first level nodes into left and right directed nodes.
        """

        E = self._eval_func()

        children = self.root.children
        n = int((len(children) + 1) * E('horizontalBalance'))

        self._leftnodes = children[:n]
        self._rightnodes = children[n:]

    def calclayout(self, root):
        self.root = root

        # Vertical layout
        self._leaf_y = 0
        self._branch_pad = False
        self._calc_y(self.root, Direction.Left)

        oy = self.root.y

        self._leaf_y = 0
        self._branch_pad = False
        self._calc_y(self.root, Direction.Right)

        dy = oy - self.root.y

        # Align right side to the left
        self.root.y += dy
        for node in self._rightnodes:
            node.shiftbranch(0, dy)

        # Horizontal layout
        self._calc_child_maxwidth(self.root, Direction.Left)
        self._calc_child_maxwidth(self.root, Direction.Right)
        self._calc_x(self.root, Direction.Left, 0)
        self._calc_x(self.root, Direction.Right, self.root.x)

    def _getchildren(self, node, direction):
        if node.isroot():
            return (self._leftnodes
                    if direction == Direction.Left else self._rightnodes)
        return node.children

    def _calc_child_maxwidth(self, node, direction):
        for child in self._getchildren(node, direction):
            self._calc_child_maxwidth(child, direction)

        p = node.parent
        if not p:
            return
        if not hasattr(p, 'child_bboxwidth_max'):
            p.child_bboxwidth_max = node.bboxwidth
        else:
            p.child_bboxwidth_max = max(p.child_bboxwidth_max, node.bboxwidth)

    def _calc_x(self, node, direction, x):
        E = self._eval_func(node, direction)

        children = self._getchildren(node, direction)

        xpad = E('rootPadX') if node.isroot() else E('nodePadX')

        if E('sameWidthSiblings'):
            if not node.isroot() and not node.isleaf():
                maxwidth = node.parent.child_bboxwidth_max
            else:
                maxwidth = node.bboxwidth
        else:
            maxwidth = node.bboxwidth

        xoffs = 0
        if not node.isroot():
            siblings = self._getchildren(node.parent, direction)
            if len(siblings) >= E('radialMinNodes'):
                _dir = -1 if direction == Direction.Left else 1
                xoffs = halfcircle(0, siblings[0].y, siblings[-1].y,
                                   E('radialFactor'), _dir, node.y)

        if direction == Direction.Left:
            width = node.bboxwidth
            x += xoffs - width
            node.x = x
            x -= xpad + maxwidth - width
        else:
            x += xoffs
            node.x = x
            x += maxwidth + xpad

        for child in children:
            self._calc_x(child, direction, x)

    def _calc_y(self, node, direction):
        E = self._eval_func(node)

        # Initialise branch bounding box
        node._branch_bboxtop = node.y
        node._branch_bboxbottom = node.y + node.bboxheight

        node_pady = E('nodePadY')

        if node.isleaf():
            # Set the position of a leaf node node a calculate the y
            # position for the next leaf. Because of the way we traverse
            # the tree, all leaf nodes are positioned consecutively on
            # the y axis, separated by the node and branch paddings.
            node.y = self._leaf_y
            self._leaf_y += node.bboxheight + node_pady
            self._branch_pad = False
        else:
            # Depth-first traversal: we are going to calculate the
            # layout starting from the leaf nodes, progressing upwards
            # in the tree, and from top to bottom (from lower to higher
            # y coordinates) in terms of vertical positioning
            children = self._getchildren(node, direction)
            if not children:
                return

            branch_pad_y = E('branchPadY')

            if not self._branch_pad:
                self._leaf_y += branch_pad_y - node_pady
                self._branch_pad = True

            for child in children:
                self._calc_y(child, direction)

            if not self._branch_pad:
                self._leaf_y += branch_pad_y - node_pady
                self._branch_pad = True

            # At this point the whole subtree under 'node' has been
            # positioned correctly. The only remainig thing is to
            # calculate the y position of 'node' (the parent).
            node_dir = direction
            child_dir = opposite_dir(node_dir)

            # Calculate the y coord of the connection point for the
            # children
            firstchild = children[0]
            lastchild = children[-1]

            child_conn_ytop = firstchild.connection_point(child_dir)[1]
            child_conn_ybottom = lastchild.connection_point(child_dir)[1]

            # The actual connection point will be in between
            child_conn_y = (child_conn_ytop +
                            (child_conn_ybottom - child_conn_ytop) *
                            E('verticalAlignFactor'))

            # Snap parent connection position to children connection
            # positions vertically
            if E('snapParentToChildren'):
                children_conn_y = [
                    c.connection_point(child_dir)[1] for c in children
                ]

                # Enable snapping to half-positions 50% inbetween two
                # child connections
                if E('snapToHalfPositions'):
                    l = []
                    for i in range(len(children_conn_y) - 1):
                        y1 = children_conn_y[i]
                        y2 = children_conn_y[i + 1]
                        l.append(y1)
                        l.append((y1 + y2) / 2.)

                    l.append(children_conn_y[-1])
                    children_conn_y = l

                # Get closest connection point index
                d = [abs(child_conn_y - y) for y in children_conn_y]
                closest = d.index(min(d))

                child_conn_y = children_conn_y[closest]

            # Calculate the y offset of the parent node in relation to
            # it's connection point
            node_conn_y = node.connection_point(node_dir)[1]
            node_yoffs = node_conn_y - node.y

            # Calculate the top and bottom y coords of the parent
            node_ytop = child_conn_y - node_yoffs
            node_ybottom = child_conn_y + (node.bboxheight - node_yoffs)

            # Set the position of the parent node
            node.y = node_ytop

            # Calculate the top and bottom y coords of the children
            children_ytop = firstchild.y
            children_ybottom = lastchild.y + lastchild.bboxheight

            # If the parent extends above the topmost child node, shift
            # the whole branch downwards by the same amount
            dy_top = children_ytop - node_ytop

            if dy_top > 0:
                if node.isroot():
                    node.y += dy_top
                    for n in self._getchildren(node, direction):
                        n.shiftbranch(0, dy_top)
                else:
                    node.shiftbranch(0, dy_top)
            else:
                dy_top = 0

            # If the parent extends below the bottommost child node,
            # offset the y start the next leaf node by the same amount
            if node_ybottom > children_ybottom:
                y = node_ybottom + branch_pad_y
                if y > self._leaf_y:
                    self._leaf_y = y

            # Adjust y coordinates if the branch has been shifted
            # downwards
            self._leaf_y += dy_top

            node_ytop += dy_top
            node_ybottom += dy_top
            children_bboxtop = firstchild._branch_bboxtop + dy_top
            children_bboxbottom = lastchild._branch_bboxbottom + dy_top

            # Calculate the bounding box of the branch
            node._branch_bboxtop = min(children_bboxtop, node_ytop)
            node._branch_bboxbottom = max(children_bboxbottom, node_ybottom)

    def childrenheight(self, node, direction):
        # direction is None for leaf nodes
        if direction == None:
            return 0

        children = self._getchildren(node, direction)
        if not children or node.isleaf():
            return 0

        firstchild = children[0]
        lastchild = children[-1]

        child_dir = opposite_dir(direction)
        child_conn_ytop = firstchild.connection_point(child_dir)[1]
        child_conn_ybottom = lastchild.connection_point(child_dir)[1]

        return child_conn_ybottom - child_conn_ytop
예제 #16
0
class JunctionConnectionDrawer(object):

    def __init__(self, config={}):
        corner_styles = ('square', 'beveled', 'rounded')
        junction_styles = ('none', 'square', 'disc', 'diamond')
        junction_sign = ('none', 'plus', 'minus')

        properties = {
            'lineWidth':        (NumberProperty, {'min': 0.0}),
            'junctionXFactor':  (NumberProperty, {}),

            'cornerStyle':      (EnumProperty, {'values': corner_styles}),
            'cornerRadius':     (NumberProperty, {'min': 0.0}),
            'cornerPad':        (NumberProperty, {'min': 0.0}),

            'junctionStyle':    (EnumProperty,{'values': junction_styles}),

            'junctionRadius':       (NumberProperty, {'min': 0.0}),
            'junctionFillColor':    (ColorProperty,  {}),
            'junctionStrokeWidth':  (NumberProperty, {'min': 0.0}),
            'junctionStrokeColor':  (ColorProperty,  {}),

            'junctionSign':             (EnumProperty,
                                        {'values': junction_sign}),

            'junctionSignSize':         (NumberProperty, {'min': 0.0}),
            'junctionSignStrokeWidth':  (NumberProperty, {'min': 0.0}),
            'junctionSignColor':        (ColorProperty,  {})
        }

        self._props = Properties(properties, defaults_path('junction'),
                                 config)

    def _eval_func(self, node):
        return lambda name: self._props.eval(name, node)

    def draw(self, node):
        if node.isroot():
            self._draw(node, Direction.Left)
            self._draw(node, Direction.Right)
        else:
            self._draw(node)

    def _draw(self, node, direction=None):
        """
        Draw a curved connection between a node and its child nodes.
        """

        E = self._eval_func(node)

        children = node.getchildren(direction)
        if not children:
            return

        linewidth = E('lineWidth')

        _ctx.autoclosepath(True)
        _ctx.stroke(node.connectioncolor)
        _ctx.fill(node.connectioncolor)
        _ctx.strokewidth(linewidth)

        firstchild = children[0]
        lastchild = children[-1]

        direction = firstchild.direction()
        opp_direction = opposite_dir(direction)
        x1, y1 = node.connection_point(direction)
        xfirst, yfirst = firstchild.connection_point(opp_direction)

        # Special case: draw straight line if there's only one child
        if len(children) == 1:
            _ctx.line(x1, y1, xfirst, yfirst)
            return

        # Calculate junction point position
        jx = x1 + (xfirst - x1) * E('junctionXFactor')
        jy = y1

        # Draw line from parent node to junction point
        _ctx.line(x1, y1, jx, jy)

        # Limit first & last corner radius to the available area
        ylast = lastchild.connection_point(opp_direction)[1]
        ysecond = children[1].connection_point(opp_direction)[1]
        ypenultimate = children[-2].connection_point(opp_direction)[1]

        # Starting corner radius
        cornerPad = E('cornerPad')
        r = min(E('cornerRadius'), abs(jx - xfirst) - cornerPad)
        r = max(r, 0)

        # Adjusted first (top) corner radius
        r1 = min(r, abs(yfirst - jy) - cornerPad)
        r1 = max(r1, 0)
        if ysecond < jy:
            r1 = min(r, abs(yfirst - ysecond) - cornerPad)
            r1 = max(r1, 0)

        # Adjusted last (bottom) corner radius
        r2 = min(r, abs(ylast - jy) - cornerPad)
        r2 = max(r2, 0)
        if ypenultimate > jy:
            r2 = min(r, abs(ylast - ypenultimate) - cornerPad)
            r2 = max(r2, 0)

        # Draw main branch as a single path to ensure line continuity
        p1 = Vector2(jx, yfirst + r1)
        p2 = Vector2(jx, ylast - r2)
        segments = [[p1, p2]]

        corner_style = E('cornerStyle')

        for i, child in enumerate(children):
            direction = child.direction()
            opp_direction = opposite_dir(direction)

            x2, y2 = child.connection_point(opp_direction)
            if direction == Direction.Left:
                x2 -= linewidth / 2
            elif direction == Direction.Right:
                x2 += linewidth / 2

            # Draw corners
            if direction == Direction.Left:
                a1 = 90
                da = -90
                dx1 = r1 * 2
                dx2 = r2 * 2
            else:
                a1 = da = 90
                dx1 = dx2 = 0

            x1 = jx
            if child is firstchild:
                x1 += -r1 if direction == Direction.Left else r1

                if (corner_style == 'square' or abs(y2 - jy) < .001):
                    p1 = Vector2(jx, y2)
                    p2 = Vector2(jx, y2 + r1)
                    segments.insert(0, [p1, p2])

                    p1 = Vector2(x1, y2)
                    p2 = Vector2(jx, y2)
                    segments.insert(0, [p1, p2])

                elif corner_style == 'beveled':
                    p1 = Vector2(x1, y2)
                    p2 = Vector2(jx, y2 + r1)
                    segments.insert(0, [p1, p2])

                elif corner_style == 'rounded':
                    arc = arcpath(jx - dx1, y2, r1 * 2, r1 * 2, a1, da)
                    segments = arc + segments

                p1 = Vector2(x2, y2)
                p2 = Vector2(x1, y2)
                segments.insert(0, [p1, p2])

            elif child is lastchild:
                x1 += -r2 if direction == Direction.Left else r2

                if (corner_style == 'square' or abs(y2 - jy) < .001):
                    p1 = Vector2(jx, y2 - r2)
                    p2 = Vector2(jx, y2)
                    segments.append([p1, p2])

                    p1 = Vector2(jx, y2)
                    p2 = Vector2(x1, y2)
                    segments.append([p1, p2])

                elif corner_style == 'beveled':
                    p1 = Vector2(jx, y2 - r2)
                    p2 = Vector2(x1, y2)
                    segments.append([p1, p2])

                elif corner_style == 'rounded':
                    arc = arcpath(jx - dx2, y2 - r2 * 2, r2 * 2, r2 * 2,
                                  a1 + da, da)
                    segments = segments + arc

                p1 = Vector2(x1, y2)
                p2 = Vector2(x2, y2)
                segments.append([p1, p2])

            else:
                _ctx.line(x1, y2, x2, y2)

        # Draw main branch path
        _ctx.nofill()

        path = createpath(_ctx, segments, close=False)
        _ctx.drawpath(path)

        # Draw junction point
        style = E('junctionStyle')
        if style == 'none':
            return

        r = E('junctionRadius')
        r2 = r / 2.

        _ctx.fill(E('junctionFillColor'))
        _ctx.stroke(E('junctionStrokeColor'))
        _ctx.strokewidth(E('junctionStrokeWidth'))

        if style == 'square':
            _ctx.rect(jx - r2, jy - r2, r, r)

        elif style == 'disc':
            _ctx.oval(jx - r2, jy - r2, r, r)

        elif style == 'diamond':
            _ctx.beginpath(jx, jy - r2)
            _ctx.lineto(jx + r2, jy)
            _ctx.lineto(jx, jy + r2)
            _ctx.lineto(jx - r2, jy)
            _ctx.lineto(jx, jy - r2)
            _ctx.endpath()

        # Draw junction sign
        sign = E('junctionSign')
        if sign == 'none':
            return

        _ctx.stroke(E('junctionSignColor'))

        d = E('junctionSignSize') / 2.
        _ctx.strokewidth(E('junctionSignStrokeWidth'))

        if sign in ('minus', 'plus'):
            _ctx.line(jx - d, jy, jx + d, jy)

        if sign == 'plus':
            _ctx.line(jx, jy - d, jx, jy + d)
예제 #17
0
파일: layout.py 프로젝트: johnnovak/twyg
class Layout(object):

    def __init__(self, config):
        properties = {
            'horizontalBalance':       (NumberProperty,  {}),
            'verticalAlignFactor':     (NumberProperty,  {'min': 0.0,
                                                          'max': 1.0}),
            'rootPadX':                (NumberProperty,  {}),
            'nodePadX':                (NumberProperty,  {}),
            'nodePadY':                (NumberProperty,  {}),
            'branchPadY':              (NumberProperty,  {}),
            'sameWidthSiblings':       (BooleanProperty, {}),
            'snapParentToChildren':    (BooleanProperty, {}),
            'snapToHalfPositions':     (BooleanProperty, {}),
            'radialMinNodes':          (NumberProperty,  {'min': 0.0}),
            'radialFactor':            (NumberProperty,  {})
        }

        self._props = Properties(properties, self._defaults_path('layout'),
                                 config)

        self._leftnodes = ()
        self._rightnodes = ()

    # TODO util function in common?
    def _defaults_path(self, conf):
        return os.path.join('layout', conf)

    def _eval_func(self, node=None, direction=None):
        if node:
            vars = {
                'childrenHeight': self.childrenheight(node, direction)
            }
        else:
            vars = {}
        return lambda name: self._props.eval(name, node, vars)

    def precalc_layout(self, root):
        self.root = root
        self._splitnodes()

    def node_orientation(self, node):
        if node.isroot():
            return None
        elif node.ancestor(-1) in self._leftnodes:
            return Direction.Left
        elif node.ancestor(-1) in self._rightnodes:
            return Direction.Right

    def _splitnodes(self):
        """
        Split the first level nodes into left and right directed nodes.
        """

        E = self._eval_func()

        children = self.root.children
        n = int((len(children) + 1) * E('horizontalBalance'))

        self._leftnodes = children[:n]
        self._rightnodes = children[n:]

    def calclayout(self, root):
        self.root = root

        # Vertical layout
        self._leaf_y = 0
        self._branch_pad = False
        self._calc_y(self.root, Direction.Left)

        oy = self.root.y

        self._leaf_y = 0
        self._branch_pad = False
        self._calc_y(self.root, Direction.Right)

        dy = oy - self.root.y

        # Align right side to the left
        self.root.y += dy
        for node in self._rightnodes:
            node.shiftbranch(0, dy)

        # Horizontal layout
        self._calc_child_maxwidth(self.root, Direction.Left)
        self._calc_child_maxwidth(self.root, Direction.Right)
        self._calc_x(self.root, Direction.Left, 0)
        self._calc_x(self.root, Direction.Right, self.root.x)

    def _getchildren(self, node, direction):
        if node.isroot():
            return (self._leftnodes if direction == Direction.Left
                    else self._rightnodes)
        return node.children

    def _calc_child_maxwidth(self, node, direction):
        for child in self._getchildren(node, direction):
            self._calc_child_maxwidth(child, direction)

        p = node.parent
        if not p:
            return
        if not hasattr(p, 'child_bboxwidth_max'):
            p.child_bboxwidth_max = node.bboxwidth
        else:
            p.child_bboxwidth_max = max(p.child_bboxwidth_max, node.bboxwidth)

    def _calc_x(self, node, direction, x):
        E = self._eval_func(node, direction)

        children = self._getchildren(node, direction)

        xpad = E('rootPadX') if node.isroot() else E('nodePadX')

        if E('sameWidthSiblings'):
            if not node.isroot() and not node.isleaf():
                maxwidth = node.parent.child_bboxwidth_max
            else:
                maxwidth = node.bboxwidth
        else:
            maxwidth = node.bboxwidth

        xoffs = 0
        if not node.isroot():
            siblings = self._getchildren(node.parent, direction)
            if len(siblings) >= E('radialMinNodes'):
                _dir = -1 if direction == Direction.Left else 1
                xoffs = halfcircle(0, siblings[0].y, siblings[-1].y,
                                   E('radialFactor'), _dir, node.y)

        if direction == Direction.Left:
            width = node.bboxwidth
            x += xoffs - width
            node.x = x
            x -= xpad + maxwidth - width
        else:
            x += xoffs
            node.x = x
            x += maxwidth + xpad

        for child in children:
            self._calc_x(child, direction, x)

    def _calc_y(self, node, direction):
        E = self._eval_func(node)

        # Initialise branch bounding box
        node._branch_bboxtop = node.y
        node._branch_bboxbottom = node.y + node.bboxheight

        node_pady = E('nodePadY')

        if node.isleaf():
            # Set the position of a leaf node node a calculate the y
            # position for the next leaf. Because of the way we traverse
            # the tree, all leaf nodes are positioned consecutively on
            # the y axis, separated by the node and branch paddings.
            node.y = self._leaf_y
            self._leaf_y += node.bboxheight + node_pady
            self._branch_pad = False
        else:
            # Depth-first traversal: we are going to calculate the
            # layout starting from the leaf nodes, progressing upwards
            # in the tree, and from top to bottom (from lower to higher
            # y coordinates) in terms of vertical positioning
            children = self._getchildren(node, direction)
            if not children:
                return

            branch_pad_y = E('branchPadY')

            if not self._branch_pad:
                self._leaf_y += branch_pad_y - node_pady
                self._branch_pad = True

            for child in children:
                self._calc_y(child, direction)

            if not self._branch_pad:
                self._leaf_y += branch_pad_y - node_pady
                self._branch_pad = True

            # At this point the whole subtree under 'node' has been
            # positioned correctly. The only remainig thing is to
            # calculate the y position of 'node' (the parent).
            node_dir = direction
            child_dir = opposite_dir(node_dir)

            # Calculate the y coord of the connection point for the
            # children
            firstchild = children[0]
            lastchild = children[-1]

            child_conn_ytop = firstchild.connection_point(child_dir)[1]
            child_conn_ybottom = lastchild.connection_point(child_dir)[1]

            # The actual connection point will be in between 
            child_conn_y = (child_conn_ytop
                           + (child_conn_ybottom - child_conn_ytop)
                           * E('verticalAlignFactor'))

            # Snap parent connection position to children connection
            # positions vertically
            if E('snapParentToChildren'):
                children_conn_y = [c.connection_point(child_dir)[1]
                                   for c in children]

                # Enable snapping to half-positions 50% inbetween two
                # child connections
                if E('snapToHalfPositions'):
                    l = []
                    for i in range(len(children_conn_y) - 1):
                        y1 = children_conn_y[i]
                        y2 = children_conn_y[i + 1]
                        l.append(y1)
                        l.append((y1 + y2) / 2.)

                    l.append(children_conn_y[-1])
                    children_conn_y = l

                # Get closest connection point index
                d = [abs(child_conn_y - y) for y in children_conn_y]
                closest = d.index(min(d))

                child_conn_y = children_conn_y[closest]

            # Calculate the y offset of the parent node in relation to
            # it's connection point
            node_conn_y = node.connection_point(node_dir)[1]
            node_yoffs = node_conn_y - node.y

            # Calculate the top and bottom y coords of the parent
            node_ytop = child_conn_y - node_yoffs
            node_ybottom = child_conn_y + (node.bboxheight - node_yoffs)

            # Set the position of the parent node
            node.y = node_ytop

            # Calculate the top and bottom y coords of the children
            children_ytop = firstchild.y
            children_ybottom = lastchild.y + lastchild.bboxheight

            # If the parent extends above the topmost child node, shift
            # the whole branch downwards by the same amount 
            dy_top = children_ytop - node_ytop

            if dy_top > 0:
                if node.isroot():
                    node.y += dy_top
                    for n in self._getchildren(node, direction):
                        n.shiftbranch(0, dy_top)
                else:
                    node.shiftbranch(0, dy_top)
            else:
                dy_top = 0

            # If the parent extends below the bottommost child node,
            # offset the y start the next leaf node by the same amount
            if node_ybottom > children_ybottom:
                y = node_ybottom + branch_pad_y
                if y > self._leaf_y:
                    self._leaf_y = y

            # Adjust y coordinates if the branch has been shifted
            # downwards
            self._leaf_y += dy_top

            node_ytop += dy_top
            node_ybottom += dy_top
            children_bboxtop = firstchild._branch_bboxtop + dy_top
            children_bboxbottom = lastchild._branch_bboxbottom + dy_top

            # Calculate the bounding box of the branch
            node._branch_bboxtop = min(children_bboxtop, node_ytop)
            node._branch_bboxbottom = max(children_bboxbottom, node_ybottom)

    def childrenheight(self, node, direction):
        # direction is None for leaf nodes
        if direction == None:
            return 0

        children = self._getchildren(node, direction)
        if not children or node.isleaf():
            return 0

        firstchild = children[0]
        lastchild = children[-1]

        child_dir = opposite_dir(direction)
        child_conn_ytop = firstchild.connection_point(child_dir)[1]
        child_conn_ybottom = lastchild.connection_point(child_dir)[1]

        return child_conn_ybottom - child_conn_ytop
예제 #18
0
class CurveConnectionDrawer(object):

    def __init__(self, config={}):
        properties = {
            'nodeLineWidthStart': (NumberProperty, {'min': 0.0}),
            'nodeLineWidthEnd':   (NumberProperty, {'min': 0.0}),
            'nodeCx1Factor':      (NumberProperty, {}),
            'nodeCx2Factor':      (NumberProperty, {}),
            'nodeCy1Factor':      (NumberProperty, {}),
            'nodeCy2Factor':      (NumberProperty, {})
        }

        self._props = Properties(properties, defaults_path('curve'), config)

    def _eval_func(self, node):
        return lambda name: self._props.eval(name, node)

    def draw(self, node):
        """
        Draw a curved connection between a node and its child nodes.
        """

        E = self._eval_func(node)

        if node.isleaf():
            return

        _ctx.autoclosepath(True)
        _ctx.stroke(node.connectioncolor)
        _ctx.fill(node.connectioncolor)

        children = node.children

        for child in children:
            linewidth = E('nodeLineWidthEnd')

            _ctx.strokewidth(linewidth)

            direction = child.direction()
            opp_direction = opposite_dir(direction)

            x1, y1 = node.connection_point(direction)
            x2, y2 = child.connection_point(opp_direction)

            if direction == Direction.Left:
                x2 -= linewidth / 2
            elif direction == Direction.Right:
                x2 += linewidth / 2

            if len(children) == 1:
                _ctx.line(x1, y1, x2, y2)
            else:
                cx1 = (x2 - x1) * E('nodeCx1Factor')
                cx2 = (x2 - x1) * E('nodeCx2Factor')

                cy1 = (y2 - y1) * E('nodeCy1Factor')
                cy2 = (y2 - y1) * E('nodeCy2Factor')

                p1x = x1 + cx1
                p1y = y1 + cy1
                p2x = x2 - cx2
                p2y = y2 - cy2

                startwidth = E('nodeLineWidthStart') - 1
                sw = startwidth / 2.

                _ctx.beginpath(x1, y1 - sw)
                _ctx.curveto(p1x, p1y, p2x, p2y, x2, y2)
                _ctx.curveto(p2x, p2y, p1x, p1y, x1, y1 + sw)
                _ctx.endpath()