mirror of
https://github.com/pcottle/learnGitBranching.git
synced 2025-06-29 17:27:22 +02:00
531 lines
13 KiB
JavaScript
531 lines
13 KiB
JavaScript
var _ = require('underscore');
|
|
var Backbone = require('backbone');
|
|
var GRAPHICS = require('../util/constants').GRAPHICS;
|
|
|
|
var VisBase = require('../visuals/visBase').VisBase;
|
|
|
|
var randomHueString = function() {
|
|
var hue = Math.random();
|
|
var str = 'hsb(' + String(hue) + ',0.7,1)';
|
|
return str;
|
|
};
|
|
|
|
var VisBranch = VisBase.extend({
|
|
defaults: {
|
|
pos: null,
|
|
text: null,
|
|
rect: null,
|
|
arrow: null,
|
|
isHead: false,
|
|
flip: 1,
|
|
|
|
fill: GRAPHICS.rectFill,
|
|
stroke: GRAPHICS.rectStroke,
|
|
'stroke-width': GRAPHICS.rectStrokeWidth,
|
|
|
|
offsetX: GRAPHICS.nodeRadius * 4.75,
|
|
offsetY: 0,
|
|
arrowHeight: 14,
|
|
arrowInnerSkew: 0,
|
|
arrowEdgeHeight: 6,
|
|
arrowLength: 14,
|
|
arrowOffsetFromCircleX: 10,
|
|
|
|
vPad: 5,
|
|
hPad: 5,
|
|
|
|
animationSpeed: GRAPHICS.defaultAnimationTime,
|
|
animationEasing: GRAPHICS.defaultEasing
|
|
},
|
|
|
|
validateAtInit: function() {
|
|
if (!this.get('branch')) {
|
|
throw new Error('need a branch!');
|
|
}
|
|
},
|
|
|
|
getID: function() {
|
|
return this.get('branch').get('id');
|
|
},
|
|
|
|
initialize: function() {
|
|
this.validateAtInit();
|
|
|
|
// shorthand notation for the main objects
|
|
this.gitVisuals = this.get('gitVisuals');
|
|
this.gitEngine = this.get('gitEngine');
|
|
if (!this.gitEngine) {
|
|
throw new Error('asd wtf');
|
|
}
|
|
|
|
this.get('branch').set('visBranch', this);
|
|
var id = this.get('branch').get('id');
|
|
|
|
if (id == 'HEAD') {
|
|
// switch to a head ref
|
|
this.set('isHead', true);
|
|
this.set('flip', -1);
|
|
this.refreshOffset();
|
|
|
|
this.set('fill', GRAPHICS.headRectFill);
|
|
} else if (id !== 'master') {
|
|
// we need to set our color to something random
|
|
this.set('fill', randomHueString());
|
|
}
|
|
},
|
|
|
|
getCommitPosition: function() {
|
|
var commit = this.gitEngine.getCommitFromRef(this.get('branch'));
|
|
var visNode = commit.get('visNode');
|
|
|
|
this.set('flip', this.getFlipValue(commit, visNode));
|
|
this.refreshOffset();
|
|
return visNode.getScreenCoords();
|
|
},
|
|
|
|
getFlipValue: function(commit, visNode) {
|
|
var threshold = this.get('gitVisuals').getFlipPos();
|
|
var overThreshold = (visNode.get('pos').x > threshold);
|
|
|
|
// easy logic first
|
|
if (!this.get('isHead')) {
|
|
return (overThreshold) ? -1 : 1;
|
|
}
|
|
|
|
// now for HEAD....
|
|
if (overThreshold) {
|
|
// if by ourselves, then feel free to squeeze in. but
|
|
// if other branches are here, then we need to show separate
|
|
return (this.isBranchStackEmpty()) ? -1 : 1;
|
|
} else {
|
|
return (this.isBranchStackEmpty()) ? 1 : -1;
|
|
}
|
|
},
|
|
|
|
shouldOffsetY: function() {
|
|
return this.gitEngine.getBranches().length > 1;
|
|
},
|
|
|
|
refreshOffset: function() {
|
|
var baseOffsetX = GRAPHICS.nodeRadius * 4.75;
|
|
if (!this.shouldOffsetY()) {
|
|
this.set('offsetY', 0);
|
|
this.set('offsetX', baseOffsetX);
|
|
return;
|
|
}
|
|
|
|
var offsetY = 33;
|
|
var deltaX = 10;
|
|
if (this.get('flip') === 1) {
|
|
this.set('offsetY', -offsetY);
|
|
this.set('offsetX', baseOffsetX - deltaX);
|
|
} else {
|
|
this.set('offsetY', offsetY);
|
|
this.set('offsetX', baseOffsetX - deltaX);
|
|
}
|
|
},
|
|
|
|
getArrowTransform: function() {
|
|
if (!this.shouldOffsetY()) {
|
|
return '';
|
|
} else if (this.get('flip') === 1) {
|
|
return 't-2,-20R-35';
|
|
} else {
|
|
return 't2,20R-35';
|
|
}
|
|
},
|
|
|
|
getBranchStackIndex: function() {
|
|
if (this.get('isHead')) {
|
|
// head is never stacked with other branches
|
|
return 0;
|
|
}
|
|
|
|
var myArray = this.getBranchStackArray();
|
|
var index = -1;
|
|
_.each(myArray, function(branch, i) {
|
|
if (branch.obj == this.get('branch')) {
|
|
index = i;
|
|
}
|
|
}, this);
|
|
return index;
|
|
},
|
|
|
|
getBranchStackLength: function() {
|
|
if (this.get('isHead')) {
|
|
// head is always by itself
|
|
return 1;
|
|
}
|
|
|
|
return this.getBranchStackArray().length;
|
|
},
|
|
|
|
isBranchStackEmpty: function() {
|
|
// useful function for head when computing flip logic
|
|
var arr = this.gitVisuals.branchStackMap[this.getCommitID()];
|
|
return (arr) ?
|
|
arr.length === 0 :
|
|
true;
|
|
},
|
|
|
|
getCommitID: function() {
|
|
var target = this.get('branch').get('target');
|
|
if (target.get('type') === 'branch') {
|
|
// for HEAD
|
|
target = target.get('target');
|
|
}
|
|
return target.get('id');
|
|
},
|
|
|
|
getBranchStackArray: function() {
|
|
var arr = this.gitVisuals.branchStackMap[this.getCommitID()];
|
|
if (arr === undefined) {
|
|
// this only occurs when we are generating graphics inside of
|
|
// a new Branch instantiation, so we need to force the update
|
|
this.gitVisuals.calcBranchStacks();
|
|
return this.getBranchStackArray();
|
|
}
|
|
return arr;
|
|
},
|
|
|
|
getTextPosition: function() {
|
|
var pos = this.getCommitPosition();
|
|
|
|
// then order yourself accordingly. we use alphabetical sorting
|
|
// so everything is independent
|
|
var myPos = this.getBranchStackIndex();
|
|
return {
|
|
x: pos.x + this.get('flip') * this.get('offsetX'),
|
|
y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY')
|
|
};
|
|
},
|
|
|
|
getRectPosition: function() {
|
|
var pos = this.getTextPosition();
|
|
var f = this.get('flip');
|
|
|
|
// first get text width and height
|
|
var textSize = this.getTextSize();
|
|
return {
|
|
x: pos.x - 0.5 * textSize.w - this.get('hPad'),
|
|
y: pos.y - 0.5 * textSize.h - this.get('vPad')
|
|
};
|
|
},
|
|
|
|
getArrowPath: function() {
|
|
// should make these util functions...
|
|
var offset2d = function(pos, x, y) {
|
|
return {
|
|
x: pos.x + x,
|
|
y: pos.y + y
|
|
};
|
|
};
|
|
var toStringCoords = function(pos) {
|
|
return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y));
|
|
};
|
|
var f = this.get('flip');
|
|
|
|
var arrowTip = offset2d(this.getCommitPosition(),
|
|
f * this.get('arrowOffsetFromCircleX'),
|
|
0
|
|
);
|
|
var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight'));
|
|
var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight'));
|
|
|
|
var arrowInnerUp = offset2d(arrowEdgeUp,
|
|
f * this.get('arrowInnerSkew'),
|
|
this.get('arrowEdgeHeight')
|
|
);
|
|
var arrowInnerLow = offset2d(arrowEdgeLow,
|
|
f * this.get('arrowInnerSkew'),
|
|
-this.get('arrowEdgeHeight')
|
|
);
|
|
|
|
var tailLength = 49;
|
|
var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0);
|
|
var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0);
|
|
|
|
var pathStr = '';
|
|
pathStr += 'M' + toStringCoords(arrowStartUp) + ' ';
|
|
var coords = [
|
|
arrowInnerUp,
|
|
arrowEdgeUp,
|
|
arrowTip,
|
|
arrowEdgeLow,
|
|
arrowInnerLow,
|
|
arrowStartLow
|
|
];
|
|
_.each(coords, function(pos) {
|
|
pathStr += 'L' + toStringCoords(pos) + ' ';
|
|
}, this);
|
|
pathStr += 'z';
|
|
return pathStr;
|
|
},
|
|
|
|
getTextSize: function() {
|
|
var getTextWidth = function(visBranch) {
|
|
var textNode = (visBranch.get('text')) ? visBranch.get('text').node : null;
|
|
return (textNode === null) ? 0 : textNode.clientWidth;
|
|
};
|
|
|
|
var firefoxFix = function(obj) {
|
|
if (!obj.w) { obj.w = 75; }
|
|
if (!obj.h) { obj.h = 20; }
|
|
return obj;
|
|
};
|
|
|
|
var textNode = this.get('text').node;
|
|
if (this.get('isHead')) {
|
|
// HEAD is a special case
|
|
return firefoxFix({
|
|
w: textNode.clientWidth,
|
|
h: textNode.clientHeight
|
|
});
|
|
}
|
|
|
|
var maxWidth = 0;
|
|
_.each(this.getBranchStackArray(), function(branch) {
|
|
maxWidth = Math.max(maxWidth, getTextWidth(
|
|
branch.obj.get('visBranch')
|
|
));
|
|
});
|
|
|
|
return firefoxFix({
|
|
w: maxWidth,
|
|
h: textNode.clientHeight
|
|
});
|
|
},
|
|
|
|
getSingleRectSize: function() {
|
|
var textSize = this.getTextSize();
|
|
var vPad = this.get('vPad');
|
|
var hPad = this.get('hPad');
|
|
return {
|
|
w: textSize.w + vPad * 2,
|
|
h: textSize.h + hPad * 2
|
|
};
|
|
},
|
|
|
|
getRectSize: function() {
|
|
var textSize = this.getTextSize();
|
|
// enforce padding
|
|
var vPad = this.get('vPad');
|
|
var hPad = this.get('hPad');
|
|
|
|
// number of other branch names we are housing
|
|
var totalNum = this.getBranchStackLength();
|
|
return {
|
|
w: textSize.w + vPad * 2,
|
|
h: textSize.h * totalNum * 1.1 + hPad * 2
|
|
};
|
|
},
|
|
|
|
getIsRemote: function() {
|
|
return this.get('branch').getIsRemote();
|
|
},
|
|
|
|
getName: function() {
|
|
var name = this.get('branch').getName();
|
|
var selected = this.get('branch') === this.gitEngine.HEAD.get('target');
|
|
var isRemote = this.getIsRemote();
|
|
|
|
var after = (selected && !this.getIsInOrigin() && !isRemote) ? '*' : '';
|
|
return name + after;
|
|
},
|
|
|
|
nonTextToFront: function() {
|
|
this.get('arrow').toFront();
|
|
this.get('rect').toFront();
|
|
},
|
|
|
|
textToFront: function() {
|
|
this.get('text').toFront();
|
|
},
|
|
|
|
textToFrontIfInStack: function() {
|
|
if (this.getBranchStackIndex() !== 0) {
|
|
this.get('text').toFront();
|
|
}
|
|
},
|
|
|
|
getFill: function() {
|
|
// in the easy case, just return your own fill if you are:
|
|
// - the HEAD ref
|
|
// - by yourself (length of 1)
|
|
// - part of a multi branch, but your thing is hidden
|
|
if (this.get('isHead') ||
|
|
this.getBranchStackLength() == 1 ||
|
|
this.getBranchStackIndex() !== 0) {
|
|
return this.get('fill');
|
|
}
|
|
|
|
// woof. now it's hard, we need to blend hues...
|
|
return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray());
|
|
},
|
|
|
|
remove: function() {
|
|
this.removeKeys(['text', 'arrow', 'rect']);
|
|
// also need to remove from this.gitVisuals
|
|
this.gitVisuals.removeVisBranch(this);
|
|
},
|
|
|
|
genGraphics: function(paper) {
|
|
var textPos = this.getTextPosition();
|
|
var name = this.getName();
|
|
var text;
|
|
|
|
// when from a reload, we dont need to generate the text
|
|
text = paper.text(textPos.x, textPos.y, String(name));
|
|
text.attr({
|
|
'font-size': 14,
|
|
'font-family': 'Monaco, Courier, font-monospace',
|
|
opacity: this.getTextOpacity()
|
|
});
|
|
this.set('text', text);
|
|
var attr = this.getAttributes();
|
|
|
|
var rectPos = this.getRectPosition();
|
|
var sizeOfRect = this.getRectSize();
|
|
var rect = paper
|
|
.rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8)
|
|
.attr(attr.rect);
|
|
this.set('rect', rect);
|
|
|
|
var arrowPath = this.getArrowPath();
|
|
var arrow = paper
|
|
.path(arrowPath)
|
|
.attr(attr.arrow);
|
|
this.set('arrow', arrow);
|
|
|
|
// set CSS
|
|
var keys = ['text', 'rect', 'arrow'];
|
|
_.each(keys, function(key) {
|
|
$(this.get(key).node).css(attr.css);
|
|
}, this);
|
|
|
|
this.attachClickHandlers();
|
|
rect.toFront();
|
|
text.toFront();
|
|
},
|
|
|
|
attachClickHandlers: function() {
|
|
if (this.get('gitVisuals').options.noClick) {
|
|
return;
|
|
}
|
|
var objs = [
|
|
this.get('rect'),
|
|
this.get('text'),
|
|
this.get('arrow')
|
|
];
|
|
|
|
_.each(objs, function(rObj) {
|
|
rObj.click(_.bind(this.onClick ,this));
|
|
}, this);
|
|
},
|
|
|
|
shouldDisableClick: function() {
|
|
return this.get('isHead') && !this.gitEngine.getDetachedHead();
|
|
},
|
|
|
|
onClick: function() {
|
|
if (this.shouldDisableClick()) {
|
|
return;
|
|
}
|
|
|
|
var commandStr = 'git checkout ' + this.get('branch').get('id');
|
|
var Main = require('../app');
|
|
Main.getEventBaton().trigger('commandSubmitted', commandStr);
|
|
},
|
|
|
|
updateName: function() {
|
|
this.get('text').attr({
|
|
text: this.getName()
|
|
});
|
|
},
|
|
|
|
getNonTextOpacity: function() {
|
|
if (this.get('isHead')) {
|
|
return this.gitEngine.getDetachedHead() ? 1 : 0;
|
|
}
|
|
return this.getBranchStackIndex() === 0 ? 1 : 0.0;
|
|
},
|
|
|
|
getTextOpacity: function() {
|
|
if (this.get('isHead')) {
|
|
return this.gitEngine.getDetachedHead() ? 1 : 0;
|
|
}
|
|
return 1;
|
|
},
|
|
|
|
getAttributes: function() {
|
|
var nonTextOpacity = this.getNonTextOpacity();
|
|
var textOpacity = this.getTextOpacity();
|
|
this.updateName();
|
|
|
|
var textPos = this.getTextPosition();
|
|
var rectPos = this.getRectPosition();
|
|
var rectSize = this.getRectSize();
|
|
|
|
var arrowPath = this.getArrowPath();
|
|
var dashArray = (this.getIsInOrigin()) ?
|
|
GRAPHICS.originDash : '';
|
|
var cursorStyle = (this.shouldDisableClick()) ?
|
|
'auto' :
|
|
'pointer';
|
|
|
|
return {
|
|
css: {
|
|
cursor: cursorStyle
|
|
},
|
|
text: {
|
|
x: textPos.x,
|
|
y: textPos.y,
|
|
opacity: textOpacity
|
|
},
|
|
rect: {
|
|
x: rectPos.x,
|
|
y: rectPos.y,
|
|
width: rectSize.w,
|
|
height: rectSize.h,
|
|
opacity: nonTextOpacity,
|
|
fill: this.getFill(),
|
|
stroke: this.get('stroke'),
|
|
//'stroke-dasharray': dashArray,
|
|
'stroke-width': this.get('stroke-width')
|
|
},
|
|
arrow: {
|
|
path: arrowPath,
|
|
opacity: nonTextOpacity,
|
|
fill: this.getFill(),
|
|
stroke: this.get('stroke'),
|
|
transform: this.getArrowTransform(),
|
|
'stroke-width': this.get('stroke-width')
|
|
}
|
|
};
|
|
},
|
|
|
|
animateUpdatedPos: function(speed, easing) {
|
|
var attr = this.getAttributes();
|
|
this.animateToAttr(attr, speed, easing);
|
|
},
|
|
|
|
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
|
// an animation of 0 is essentially setting the attribute directly
|
|
this.animateToAttr(fromAttr, 0);
|
|
this.animateToAttr(toAttr, speed, easing);
|
|
},
|
|
|
|
setAttr: function(attr, instant, speed, easing) {
|
|
var keys = ['text', 'rect', 'arrow'];
|
|
this.setAttrBase(keys, attr, instant, speed, easing);
|
|
}
|
|
});
|
|
|
|
var VisBranchCollection = Backbone.Collection.extend({
|
|
model: VisBranch
|
|
});
|
|
|
|
exports.VisBranchCollection = VisBranchCollection;
|
|
exports.VisBranch = VisBranch;
|
|
exports.randomHueString = randomHueString;
|
|
|