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 = ()
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)
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
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 __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 __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 __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 __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 = ()
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
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
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
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
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)
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()
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
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)
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
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()