UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

739 lines (691 loc) 24.2 kB
/** * @fileoverview * Implementation of line based glyphs defined by a series of nodes and paths. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /core/kekule.common.js * requires /core/kekule.structures.js * requires /chemdoc/kekule.glyph.base.js * requires /chemdoc/kekule.glyph.pathGlyphs.js * requires /chemdoc/kekule.glyph.utils.js */ (function(){ "use strict"; var OU = Kekule.ObjUtils; var NT = Kekule.Glyph.NodeType; var PT = Kekule.Glyph.PathType; var CU = Kekule.CoordUtils; var CM = Kekule.CoordMode; /** * A glyph of straight line. * @class * @augments Kekule.Glyph.PathGlyph */ Kekule.Glyph.StraightLine = Class.create(Kekule.Glyph.PathGlyph, /** @lends Kekule.Glyph.StraightLine# */ { /** @private */ CLASS_NAME: 'Kekule.Glyph.StraightLine', /** @constructs */ initialize: function(/*$super, */id, refLength, initialParams, coord2D, coord3D) { this.tryApplySuper('initialize', [id, refLength, initialParams, coord2D, coord3D]) /* $super(id, refLength, initialParams, coord2D, coord3D) */; }, /** @private */ doCreateDefaultStructure: function(refLength, initialParams) { // initialParams can include additional field: lineLength var C = Kekule.CoordUtils; var coord2D = {'x': 0, 'y': 0}; var coord3D = {'x': 0, 'y': 0, 'z': 0}; var delta = {'x': refLength * (initialParams.lineLength || 1)}; var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D); var node2 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, delta), C.add(coord3D, delta)); var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [node1, node2]); this._applyParamsToConnector(connector, initialParams); this.appendNode(node1); this.appendNode(node2); this.appendConnector(connector); }, /** @private */ _applyParamsToConnector: function(connector, initialParams) { //connector.setPathParams(initialParams); var p = Object.create(initialParams); if (Kekule.ObjUtils.isUnset(initialParams.autoOffset)) p.autoOffset = false; connector.setPathParams(p); }, /** @ignore */ getDirectManipulationTarget: function() { return this.getNodeAt(this.getNodeCount() - 1); } /* @ignore */ /* getAllowChildCoordStickTo: function(child, dest) { return true; }, */ /* @ignore */ /* getChildAcceptCoordStickFrom: function(child, fromObj) { return true; } */ }); /** * A glyph of polygon formed by straight lines. * @class * @augments Kekule.Glyph.PathGlyph */ Kekule.Glyph.Polygon = Class.create(Kekule.Glyph.PathGlyph, /** @lends Kekule.Glyph.Polygon# */ { /** @private */ CLASS_NAME: 'Kekule.Glyph.Polygon', /** @constructs */ initialize: function(/*$super, */id, refLength, initialParams, coord2D, coord3D) { this.tryApplySuper('initialize', [id, refLength, initialParams, coord2D, coord3D]) /* $super(id, refLength, initialParams, coord2D, coord3D) */; }, /** @private */ getRefLengthRatio: function() { return 1; }, /** @private */ doCreateDefaultStructure: function(refLength, initialParams) { // initialParams can include additional field: lineLength, edgeCount // node props, connector props var C = Kekule.CoordUtils; var coord2D = {'x': 0, 'y': 0}; var coord3D = {'x': 0, 'y': 0, 'z': 0}; var nodeProps = initialParams.nodeProps; var connectorProps = initialParams.connectorProps; var r = refLength * (initialParams.lineLength || 1) * this.getRefLengthRatio(); var edgeCount = initialParams.edgeCount || 3; var angleDelta = Math.PI * 2 / edgeCount; var startingAngle = (edgeCount % 2)?0: -angleDelta / 2; var currAngle = startingAngle; var firstNode, lastNode; for (var i = 0; i < edgeCount; ++i) { var c = {'x': r * Math.sin(currAngle), 'y': r * Math.cos(currAngle)}; var node = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, c), C.add(coord3D, c)); if (nodeProps) node.setPropValues(nodeProps); this.appendNode(node); if (i === 0) firstNode = node; if (lastNode) { var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [lastNode, node]); if (connectorProps) connector.setPropValues(connectorProps); this._applyParamsToConnector(connector, initialParams); this.appendConnector(connector); } lastNode = node; currAngle += angleDelta; } var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [node, firstNode]); if (connectorProps) connector.setPropValues(connectorProps); this._applyParamsToConnector(connector, initialParams); this.appendConnector(connector); }, /** @private */ _applyParamsToConnector: function(connector, initialParams) { connector.setPathParams(initialParams); } }); /** * A base glyph of arc segment. * @class * @augments Kekule.Glyph.PathGlyph */ Kekule.Glyph.BaseArc = Class.create(Kekule.Glyph.PathGlyph, /** @lends Kekule.Glyph.BaseArc# */ { /** @private */ CLASS_NAME: 'Kekule.Glyph.BaseArc', /** @constructs */ initialize: function(/*$super, */id, refLength, initialParams, coord2D, coord3D) { this.tryApplySuper('initialize', [id, refLength, initialParams, coord2D, coord3D]) /* $super(id, refLength, initialParams, coord2D, coord3D) */; }, /** @ignore */ doCreateDefaultStructure: function(refLength, initialParams) { // initialParams can include additional field: lineLength var C = Kekule.CoordUtils; var coord2D = {'x': 0, 'y': 0}; var coord3D = {'x': 0, 'y': 0, 'z': 0}; var delta = {'x': refLength * (initialParams.lineLength || 1)}; //var controllerDelta = {'x': 0, 'y': delta.x / 2}; var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D); // starting node var node2 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, delta), C.add(coord3D, delta)); // ending node this.appendNode(node1); this.appendNode(node2); //var node3 = new Kekule.Glyph.PathGlyphNode(null, Kekule.Glyph.NodeType.CONTROLLER, C.add(coord2D, controllerDelta), C.add(coord3D, controllerDelta)); // control node var connector = new Kekule.Glyph.PathGlyphArcConnector(null, [node1, node2]); this._applyParamsToConnector(connector, initialParams); /* var controlPoint = connector.getControlPoint(); //controlPoint.setDistanceToChord(delta.x) controlPoint.setCoord2D(controllerDelta) .setCoord3D(controllerDelta); */ this.appendConnector(connector); connector.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN); //node1.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN); //node2.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN); }, /** @ignore */ getDirectManipulationTarget: function() { return this.getNodeAt(this.getNodeCount() - 1); }, /** @private */ _isValidChemNodeOrConnectorStickTarget: function(targetObj) { /* var result = (targetObj instanceof Kekule.ChemStructureNode) || (targetObj instanceof Kekule.ChemStructureConnector); if (!result && (targetObj instanceof Kekule.ChemMarker.BaseMarker)) { var parent = targetObj.getParent && targetObj.getParent(); if (parent) // the marker of node/connector (e.g., electron pair) can also be a valid target result = (parent instanceof Kekule.ChemStructureNode) || (parent instanceof Kekule.ChemStructureConnector); } return result; */ return Kekule.Glyph.ElectronArrowGlyphUtils.isValidChemNodeOrConnectorStickTarget(targetObj); }, /** @private */ _applyParamsToConnector: function(connector, initialParams) { connector.setPathParams(initialParams); }, /** @ignore */ getAllowChildCoordStickTo: function(child, dest) { var result = !dest // dest not set, a general test, just returns true || (this._isValidChemNodeOrConnectorStickTarget(dest, child) && !this._isChildrenStickingTo(dest, [child])); /* || ((dest instanceof Kekule.ChemStructureNode) || (dest instanceof Kekule.ChemStructureConnector)) && (!this._isChildrenStickingTo(dest, [child])) || (dest instanceof Kekule.ChemMarker.BaseMarker); */ //console.log('allow stick to', dest && dest.getClassName(), result); return result; }, /** @ignore */ getChildUseCoordStickOffset: function(/*$super, */child, stickDest) { if (stickDest instanceof Kekule.ChemStructureNode || stickDest instanceof Kekule.ChemStructureConnector) { return true; } else { return false; //return $super(child, stickDest); } }, /** @private */ _isChildrenStickingTo: function(dest, excludeChildren) { var nodes = this.getNodes(); nodes = Kekule.ArrayUtils.exclude(nodes, excludeChildren || []); for (var i = 0, l = nodes.length; i < l; ++i) { var n = nodes[i]; if (n.getCoordStickTarget() === dest) return true; } return false; } }); /** * A base glyph of double arc glyph (for bond forming electron pushing arrow). * @class * @augments Kekule.Glyph.PathGlyph */ Kekule.Glyph.BaseTwinArc = Class.create(Kekule.Glyph.PathGlyph, /** @lends Kekule.Glyph.BaseTwinArc# */ { /** @private */ CLASS_NAME: 'Kekule.Glyph.BaseTwinArc', /** @private */ DEF_MIN_PATH_END_DISTANCE_RATIO: 0.05, /** @private */ DEF_INIT_PATH_END_GAP_RATIO: 0.2, /** @private */ FIELD_ARC_END_NODE_FLAG: '__$arcEndNode$__', /** * @constructs */ initialize: function(/*$super, */id, refLength, initialParams, coord2D, coord3D) { /* this._insideNodeGetIndirectCoordRefLengthsBind = this._insideNodeGetIndirectCoordRefLengths.bind(this); this._insideNodeGetIndirectCoordRefCoordsBind = this._insideNodeGetIndirectCoordRefCoords.bind(this); this._insideNodeCalcIndirectCoordStorageBind = this._insideNodeCalcIndirectCoordStorage.bind(this); this._insideNodeCalcIndirectCoordValueBind = this._insideNodeCalcIndirectCoordValue.bind(this); */ this.setPropStoreFieldValue('minPathEndDistanceRatio', this.DEF_MIN_PATH_END_DISTANCE_RATIO); this.tryApplySuper('initialize', [id, refLength, initialParams, coord2D, coord3D]) /* $super(id, refLength, initialParams, coord2D, coord3D) */; }, /** @private */ initProperties: function() { this.defineProp('minPathEndDistanceRatio', {'dataType': DataType.FLOAT}); this.defineProp('oppositePathEndArrowSides', { 'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { return this._isPathArrowSideOpposite('end'); }, 'setter': function(value) { this._setPathArrowSideOpposite('end', !!value); } }); this.defineProp('oppositePathStartArrowSides', { 'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { return this._isPathArrowSideOpposite('start'); }, 'setter': function(value) { this._setPathArrowSideOpposite('start', !!value); } }); }, /** @ignore */ loaded: function(rootObj) { var result = this.tryApplySuper('loaded', [rootObj]); // when add new child object, and the two arcs are ready, handle the arc end nodes if (this._isTwinArcSetup()) { this._setupArcEndNode(this.getNodeAt(1)); this._setupArcEndNode(this.getNodeAt(2)); } return result; }, /** @private */ _isPathArrowSideOpposite: function(arrowPos) { // arrowPos is a string, either of 'start' or 'end' var connCount = this.getConnectorCount(); if (connCount < 2) return null; // undeterminated else { var lastSide; var result = null; for (var i = 0; i < connCount; ++i) { var conn = this.getConnectorAt(i); var pathParam = conn.getPathParams(); var arrowType = pathParam[arrowPos + 'ArrowType']; if (arrowType === Kekule.Glyph.ArrowType.NONE) // no side return null; var arrowSide = pathParam[arrowPos + 'ArrowSide']; if (arrowSide === Kekule.Glyph.ArrowSide.BOTH) return null; if (i === 0) lastSide = arrowSide; else { var currResult = (lastSide === arrowSide)? false: true; if (OU.isUnset(result)) result = currResult; else if (result !== currResult) return null; lastSide = arrowSide; } } return result; } }, /** @private */ _setPathArrowSideOpposite: function(arrowPos, reversed) { // arrowPos is a string, either of 'start' or 'end' var connCount = this.getConnectorCount(); if (connCount < 2) return null; // undeterminated else { var ASide = Kekule.Glyph.ArrowSide; var lastSide; var applicable = true; for (var i = 0; i < connCount; ++i) { var conn = this.getConnectorAt(i); var pathParam = conn.getPathParams(); var arrowType = pathParam[arrowPos + 'ArrowType']; if (arrowType === Kekule.Glyph.ArrowType.NONE) // no side applicable = false; var arrowSide = pathParam[arrowPos + 'ArrowSide']; if (arrowSide === Kekule.Glyph.ArrowSide.BOTH) applicable = false; if (!applicable) { lastSide = null; } else { if (OU.isUnset(lastSide)) lastSide = arrowSide; else { var currSide; if (reversed) currSide = (lastSide === ASide.SINGLE)? ASide.REVERSED: ASide.SINGLE; else currSide = lastSide; pathParam[arrowPos + 'ArrowSide'] = currSide; lastSide = currSide; } } } return null; } }, /** @private */ _isTwinArcSetup: function() { return (this.getConnectorCount() === 2) && (this.getNodeCount() === 4); }, /** @ignore */ getDirectManipulationTarget: function() { return this.getNodeAt(this.getNodeCount() - 1); }, /** @ignore */ doCreateDefaultStructure: function(refLength, initialParams) { // initialParams can include additional field: lineLength var C = Kekule.CoordUtils; var coord2D = {'x': 0, 'y': 0}; var coord3D = {'x': 0, 'y': 0, 'z': 0}; var lineLength = initialParams.lineLength || 1; var minPathEndDistanceRatio = (initialParams.minPathEndDistanceRatio || this.getMinPathEndDistanceRatio() || this.DEF_MIN_PATH_END_DISTANCE_RATIO); // min distance bewteen arc ends var minPathEndDistance = lineLength * minPathEndDistanceRatio; this.setMinPathEndDistanceRatio(minPathEndDistanceRatio); var pathEndGap = (initialParams.pathEndGap || lineLength * this.DEF_INIT_PATH_END_GAP_RATIO); pathEndGap = Math.max(pathEndGap, minPathEndDistance); var deltaLine = {'x': refLength * ((lineLength - pathEndGap) / 2)}; var deltaGap = {'x': refLength * pathEndGap}; var deltaTotal = {'x': refLength * lineLength}; var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D); // starting node of first arc var node2 = new Kekule.Glyph.PathGlyphNode(null, null); // ending node of first arc this._setupArcEndNode(node2); var node3 = new Kekule.Glyph.PathGlyphNode(null, null); // ending node of second arc var node4 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, deltaTotal), C.add(coord3D, deltaTotal)); // starting node of second arc this._setupArcEndNode(node3); this.appendNode(node1); this.appendNode(node2); this.appendNode(node3); this.appendNode(node4); node2.setCoord2D(C.add(coord2D, deltaLine)).setCoord3D(C.add(coord3D, deltaLine)); node3.setCoord2D(C.add(coord2D, CU.add(deltaLine, deltaGap))).setCoord3D(C.add(coord3D, CU.add(deltaLine, deltaGap))); var connector1 = new Kekule.Glyph.PathGlyphArcConnector(null, [node1, node2]); var connector2 = new Kekule.Glyph.PathGlyphArcConnector(null, [node4, node3]); this._applyParamsToConnector(connector1, initialParams); this._applyParamsToConnector(connector2, initialParams); connector2.getControlPointAt(0).setDistanceToChord(-connector2.getControlPointAt(0).getDistanceToChord()); // ensure the two arc arrow at same side this.appendConnector(connector1); this.appendConnector(connector2); connector1.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN); connector2.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN); }, /** @private */ _applyParamsToConnector: function(connector, initialParams) { connector.setPathParams(initialParams); }, /** @private */ _setupArcEndNode: function(node) { if (!node[this.FIELD_ARC_END_NODE_FLAG]) { this._overwriteChildPathNodeMethods(node); node.setEnableIndirectCoord(true); node[this.FIELD_ARC_END_NODE_FLAG] = true; } }, /** @private */ _overwriteChildPathNodeMethods: function(node) { node.overwriteMethod('getIndirectCoordRefLengths', this._insideNodeGetIndirectCoordRefLengths) .overwriteMethod('getIndirectCoordRefCoords', this._insideNodeGetIndirectCoordRefCoords) .overwriteMethod('calcIndirectCoordStorage', this._insideNodeCalcIndirectCoordStorage) .overwriteMethod('calcIndirectCoordValue', this._insideNodeCalcIndirectCoordValue) .overwriteMethod('_childPathNodeGetParentGlyph', this._childPathNodeGetParentGlyph) .overwriteMethod('_childPathNodeGetGlyphMinPathEndDistanceRatio', this._childPathNodeGetGlyphMinPathEndDistanceRatio); }, /** @private */ _getArrowStartingNodes: function() { var result = []; if (this._isTwinArcSetup()) { for (var i = 0, l = this.getConnectorCount(); i < l; ++i) { var connector = this.getConnectorAt(i); var node = connector.getConnectedObjAt(0); if (node) result.push(node); } } return result; }, // overwrite methods of two inside nodes of arcs, so this these methods, this var refers to the node but not the glyph /** @private */ _childPathNodeGetParentGlyph: function($old) // return this parent glyph of path node { var p = this.getParent(); if (p instanceof Kekule.Glyph.BaseTwinArc) return p; else return null; }, /** @private */ _childPathNodeGetGlyphMinPathEndDistanceRatio: function($old) { var p = this._childPathNodeGetParentGlyph(); return p && p.getMinPathEndDistanceRatio(); }, /** @private */ _insideNodeGetIndirectCoordRefLengths: function($old, coordMode, allowCoordBorrow) { var glyph = this.getParent(); if (glyph && coordMode === Kekule.CoordMode.COORD2D) { var nodeStart = glyph.getNodeAt(0); var nodeEnd = glyph.getNodeAt(glyph.getNodeCount() - 1); if (nodeStart && nodeEnd) { var coord1 = nodeStart.getAbsCoordOfMode(coordMode); var coord2 = nodeEnd.getAbsCoordOfMode(coordMode); var d = Kekule.CoordUtils.getDistance(coord2, coord1); var result = {'x': d, 'y': d, 'length': d}; //console.log('_insideNodeGetIndirectCoordRefLengths', result); return result; } } else return $old(coordMode, allowCoordBorrow); }, /** @private */ _insideNodeGetIndirectCoordRefCoords: function($old, coordMode, allowCoordBorrow) { var glyph = this.getParent(); if (glyph && coordMode === CM.COORD2D) { var nodeStart = glyph.getNodeAt(0); var nodeEnd = glyph.getNodeAt(glyph.getNodeCount() - 1); if (nodeStart && nodeEnd) { /* var coord1 = nodeStart.getAbsCoordOfMode(coordMode); var coord2 = nodeEnd.getAbsCoordOfMode(coordMode); */ var coord1 = nodeStart.getCoordOfMode(coordMode); var coord2 = nodeEnd.getCoordOfMode(coordMode); //console.log('_insideNodeGetIndirectCoordRefCoords', coord); return [coord1, coord2]; } } else return $old(coordMode, allowCoordBorrow); }, /** @private */ _insideNodeCalcIndirectCoordStorage: function($old, coordMode, coordValue, oldCoordValue, allowCoordBorrow) { var glyph = this.getParent(); if (glyph && coordMode === CM.COORD2D) { //var refLength = this._insideNodeGetIndirectCoordRefLengths(null, coordMode, allowCoordBorrow).length; var refCoords = this.getIndirectCoordRefCoords(coordMode, allowCoordBorrow); //var refLength = CU.getDistance(refCoords[1], refCoords[0]); if (refCoords) { var nodeStart = glyph.getNodeAt(0); var baseCoord = nodeStart.getCoordOfMode(coordMode); var crossPoint = Kekule.GeometryUtils.getPerpendicularCrossPointFromCoordToLine(coordValue, refCoords[0], refCoords[1], false); if (!crossPoint) // outside ref line { var d0 =CU.getDistance(coordValue, refCoords[0]); var d1 =CU.getDistance(coordValue, refCoords[1]); if (d0 < d1) crossPoint = refCoords[0]; else crossPoint = refCoords[1]; } var crossPointDistance = CU.getDistance(baseCoord, crossPoint); var refDistance = CU.getDistance(refCoords[1], refCoords[0]); var ratio = refDistance? crossPointDistance / refDistance: 0; var minDistanceRatio = this._childPathNodeGetGlyphMinPathEndDistanceRatio() || 0; if (ratio < minDistanceRatio) { ratio = minDistanceRatio; } else if (ratio > 1 - minDistanceRatio) { ratio = 1 - minDistanceRatio; } return {'x': ratio, 'y': ratio, 'ratio': ratio}; } } return $old(coordMode, coordValue, oldCoordValue, allowCoordBorrow); }, /** @private */ _insideNodeCalcIndirectCoordValue: function($old, coordMode, allowCoordBorrow) { var glyph = this.getParent(); if (glyph && coordMode === CM.COORD2D) { var refCoords = this.getIndirectCoordRefCoords(coordMode, allowCoordBorrow); if (refCoords) { var refDelta = CU.substract(refCoords[1], refCoords[0]); var nodeStart = glyph.getNodeAt(0); var baseCoord = nodeStart.getCoordOfMode(coordMode); var ratio = this.getIndirectCoordStorageOfMode(coordMode)['ratio']; if (Kekule.ObjUtils.notUnset(ratio)) { var delta = CU.multiply(refDelta, ratio); var result = CU.add(baseCoord, delta); //console.log('calc', this.getIndirectCoordStorageOfMode(coordMode), ratio, delta, result); return result; } } } return $old(coordMode, allowCoordBorrow); }, /** @private */ _isValidChemNodeOrConnectorStickTarget: function(targetObj) { var result = (targetObj instanceof Kekule.ChemStructureNode) || (targetObj instanceof Kekule.ChemStructureConnector); if (!result && (targetObj instanceof Kekule.ChemMarker.BaseMarker)) { var parent = targetObj.getParent && targetObj.getParent(); if (parent) // the marker of node/connector (e.g., electron pair) can also be a valid target result = (parent instanceof Kekule.ChemStructureNode) || (parent instanceof Kekule.ChemStructureConnector); } return result; }, /** @ignore */ getAllowChildCoordStickTo: function(child, dest) { var index = this.indexOfNode(child); if (index === 0 || index === this.getNodeCount() - 1) // only the leading and tailing node can be sticked { var result = !dest // dest not set, a general test, just returns true || (this._isValidChemNodeOrConnectorStickTarget(dest, child) && !this._isChildrenStickingTo(dest, [child])); return result; } else return false; }, /** @ignore */ getChildUseCoordStickOffset: function(/*$super, */child, stickDest) { if (stickDest instanceof Kekule.ChemStructureNode || stickDest instanceof Kekule.ChemStructureConnector) { return true; } else { return false; } }, /** @private */ _isChildrenStickingTo: function(dest, excludeChildren) { var nodes = this._getArrowStartingNodes(); nodes = Kekule.ArrayUtils.exclude(nodes, excludeChildren || []); for (var i = 0, l = nodes.length; i < l; ++i) { var n = nodes[i]; if (n.getCoordStickTarget() === dest) return true; } return false; } }); /** * A general arc glyph. * @class * @augments Kekule.Glyph.BaseArc */ Kekule.Glyph.Arc = Class.create(Kekule.Glyph.BaseArc, /** @lends Kekule.Glyph.Arc# */ { /** @private */ CLASS_NAME: 'Kekule.Glyph.Arc' }); /** * A general twin arc glyph. * @class * @augments Kekule.Glyph.BaseTwinArc */ Kekule.Glyph.TwinArc = Class.create(Kekule.Glyph.BaseTwinArc, /** @lends Kekule.Glyph.TwinArc# */ { /** @private */ CLASS_NAME: 'Kekule.Glyph.TwinArc' }); })();