Merge pull request #109 from gec/layerTests

Fix rename layer, merge/cloneLayer. Migrate functions from Canvas to Draw. Tests.
master
Flint O'Brien 2016-05-04 09:58:12 -04:00
commit e35413c5ad
7 changed files with 7234 additions and 358 deletions

View File

@ -300,10 +300,20 @@ svgedit.draw.Drawing.prototype.getCurrentLayerName = function () {
/**
* Set the current layer's name.
* @param {string} name - The new name.
* @returns {Object} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null.
* @param {svgedit.history.HistoryRecordingService} hrService - History recording service
* @returns {string|null} The new name if changed; otherwise, null.
*/
svgedit.draw.Drawing.prototype.setCurrentLayerName = function (name) {
return this.current_layer ? this.current_layer.setName(name) : null;
svgedit.draw.Drawing.prototype.setCurrentLayerName = function (name, hrService) {
var finalName = null;
if (this.current_layer) {
var oldName = this.current_layer.getName();
finalName = this.current_layer.setName(name, hrService);
if (finalName) {
delete this.layer_map[oldName];
this.layer_map[finalName] = this.current_layer;
}
}
return finalName;
};
/**
@ -354,7 +364,7 @@ svgedit.draw.Drawing.prototype.setCurrentLayerPosition = function (newpos) {
svgedit.draw.Drawing.prototype.mergeLayer = function (hrService) {
var current_group = this.current_layer.getGroup();
var prevGroup = $(current_group).prev()[0];
if (!prevGroup) {return null;}
if (!prevGroup) {return;}
hrService.startBatchCommand('Merge Layer');
@ -509,12 +519,13 @@ svgedit.draw.Drawing.prototype.identifyLayers = function() {
/**
* Creates a new top-level layer in the drawing with the given name and
* sets the current layer to it.
* makes it the current layer.
* @param {string} name - The given name. If the layer name exists, a new name will be generated.
* @param {svgedit.history.HistoryRecordingService} hrService - History recording service
* @returns {SVGGElement} The SVGGElement of the new layer, which is
* also the current layer of this drawing.
*/
svgedit.draw.Drawing.prototype.createLayer = function(name) {
svgedit.draw.Drawing.prototype.createLayer = function(name, hrService) {
if (this.current_layer) {
this.current_layer.deactivate();
}
@ -522,13 +533,69 @@ svgedit.draw.Drawing.prototype.createLayer = function(name) {
if (name === undefined || name === null || name === '' || this.layer_map[name]) {
name = getNewLayerName(Object.keys(this.layer_map));
}
// Crate new layer and add to DOM as last layer
var layer = new svgedit.draw.Layer(name, null, this.svgElem_);
// Like to assume hrService exists, but this is backwards compatible with old version of createLayer.
if (hrService) {
hrService.startBatchCommand('Create Layer');
hrService.insertElement(layer.getGroup());
hrService.endBatchCommand();
}
this.all_layers.push(layer);
this.layer_map[name] = layer;
this.current_layer = layer;
return layer.getGroup();
};
/**
* Creates a copy of the current layer with the given name and makes it the current layer.
* @param {string} name - The given name. If the layer name exists, a new name will be generated.
* @param {svgedit.history.HistoryRecordingService} hrService - History recording service
* @returns {SVGGElement} The SVGGElement of the new layer, which is
* also the current layer of this drawing.
*/
svgedit.draw.Drawing.prototype.cloneLayer = function(name, hrService) {
if (!this.current_layer) {return null;}
this.current_layer.deactivate();
// Check for duplicate name.
if (name === undefined || name === null || name === '' || this.layer_map[name]) {
name = getNewLayerName(Object.keys(this.layer_map));
}
// Create new group and add to DOM just after current_layer
var currentGroup = this.current_layer.getGroup();
var layer = new svgedit.draw.Layer(name, currentGroup, this.svgElem_);
var group = layer.getGroup();
// Clone children
var children = currentGroup.childNodes;
var index;
for (index = 0; index < children.length; index++) {
var ch = children[index];
if (ch.localName == 'title') {continue;}
group.appendChild(this.copyElem(ch));
}
if (hrService) {
hrService.startBatchCommand('Duplicate Layer');
hrService.insertElement(group);
hrService.endBatchCommand();
}
// Update layer containers and current_layer.
index = this.all_layers.indexOf(this.current_layer);
if (index >= 0) {
this.all_layers.splice(index + 1, 0, layer);
} else {
this.all_layers.push(layer);
}
this.layer_map[name] = layer;
this.current_layer = layer;
return group;
};
/**
* Returns whether the layer is visible. If the layer name is not valid,
* then this function returns false.
@ -589,4 +656,16 @@ svgedit.draw.Drawing.prototype.setLayerOpacity = function(layername, opacity) {
}
};
/**
* Create a clone of an element, updating its ID and its children's IDs when needed
* @param {Element} el - DOM element to clone
* @returns {Element}
*/
svgedit.draw.Drawing.prototype.copyElem = function(el) {
var self = this;
var getNextIdClosure = function() { return self.getNextId();}
return svgedit.utilities.copyElem(el, getNextIdClosure)
}
}());

View File

@ -1,4 +1,4 @@
/*globals svgedit*/
/*globals $ svgedit*/
/*jslint vars: true, eqeq: true */
/**
* Package: svgedit.history
@ -25,24 +25,37 @@ var NS = svgedit.NS;
/**
* This class encapsulates the concept of a layer in the drawing. It can be constructed with
* an existing group element or, with three parameters, will create a new layer group element.
*
* Usage:
* new Layer'name', group) // Use the existing group for this layer.
* new Layer('name', group, svgElem) // Create a new group and add it to the DOM after group.
* new Layer('name', null, svgElem) // Create a new group and add it to the DOM as the last layer.
*
* @param {string} name - Layer name
* @param {SVGGElement} group - SVG group element that constitutes the layer or null if a group should be created and added to the DOM..
* @param {SVGGElement} svgElem - The SVG DOM element. If defined, use this to add
* @param {SVGGElement|null} group - An existing SVG group element or null.
* If group and no svgElem, use group for this layer.
* If group and svgElem, create a new group element and insert it in the DOM after group.
* If no group and svgElem, create a new group element and insert it in the DOM as the last layer.
* @param {SVGGElement=} svgElem - The SVG DOM element. If defined, use this to add
* a new layer to the document.
*/
var Layer = svgedit.draw.Layer = function(name, group, svgElem) {
this.name_ = name;
this.group_ = group;
this.group_ = svgElem ? null : group;
if (!group) {
if (svgElem) {
// Create a group element with title and add it to the DOM.
var svgdoc = svgElem.ownerDocument;
this.group_ = svgdoc.createElementNS(NS.SVG, "g");
var layer_title = svgdoc.createElementNS(NS.SVG, "title");
layer_title.textContent = name;
this.group_.appendChild(layer_title);
if (group) {
$(group).after(this.group_);
} else {
svgElem.appendChild(this.group_);
}
}
addLayerClass(this.group_);
svgedit.utilities.walkTree(this.group_, function(e){e.setAttribute("style", "pointer-events:inherit");});
@ -155,7 +168,13 @@ Layer.prototype.getTitleElement = function() {
return null;
};
Layer.prototype.setName = function(name) {
/**
* Set the name of this layer.
* @param {string} name - The new name.
* @param {svgedit.history.HistoryRecordingService} hrService - History recording service
* @returns {string|null} The new name if changed; otherwise, null.
*/
Layer.prototype.setName = function(name, hrService) {
var previousName = this.name_;
name = svgedit.utilities.toXml(name);
// now change the underlying title element contents
@ -164,7 +183,10 @@ Layer.prototype.setName = function(name) {
while (title.firstChild) { title.removeChild(title.firstChild); }
title.textContent = name;
this.name_ = name;
return {title: title, previousName: previousName};
if (hrService) {
hrService.changeElement(title, {'#text':previousName});
}
return this.name_;
}
return null;
};

View File

@ -355,6 +355,15 @@ var addCommandToHistory = function(cmd) {
canvas.undoMgr.addCommandToHistory(cmd);
};
/**
* Get a HistoryRecordingService.
* @param {svgedit.history.HistoryRecordingService=} hrService - if exists, return it instead of creating a new service.
* @returns {svgedit.history.HistoryRecordingService}
*/
function historyRecordingService(hrService) {
return hrService ? hrService : new svgedit.history.HistoryRecordingService(canvas.undoMgr);
}
// import from select.js
svgedit.select.init(curConfig, {
createSVGElement: function(jsonMap) { return canvas.addSvgElementFromJson(jsonMap); },
@ -666,56 +675,6 @@ var groupSvgElem = this.groupSvgElem = function(elem) {
$(g).append(elem).data('gsvg', elem)[0].id = getNextId();
};
// Function: copyElem
// Create a clone of an element, updating its ID and its children's IDs when needed
//
// Parameters:
// el - DOM element to clone
//
// Returns: The cloned element
var copyElem = function(el) {
// manually create a copy of the element
var new_el = document.createElementNS(el.namespaceURI, el.nodeName);
$.each(el.attributes, function(i, attr) {
if (attr.localName != '-moz-math-font-style') {
new_el.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value);
}
});
// set the copied element's new id
new_el.removeAttribute('id');
new_el.id = getNextId();
// Opera's "d" value needs to be reset for Opera/Win/non-EN
// Also needed for webkit (else does not keep curved segments on clone)
if (svgedit.browser.isWebkit() && el.nodeName == 'path') {
var fixed_d = pathActions.convertPath(el);
new_el.setAttribute('d', fixed_d);
}
// now create copies of all children
$.each(el.childNodes, function(i, child) {
switch(child.nodeType) {
case 1: // element node
new_el.appendChild(copyElem(child));
break;
case 3: // text node
new_el.textContent = child.nodeValue;
break;
default:
break;
}
});
if ($(el).data('gsvg')) {
$(new_el).data('gsvg', new_el.firstChild);
} else if ($(el).data('symbol')) {
var ref = $(el).data('symbol');
$(new_el).data('ref', ref).data('symbol', ref);
} else if (new_el.tagName == 'image') {
preventClickDefault(new_el);
}
return new_el;
};
// Set scope for these functions
var getId, getNextId;
@ -882,9 +841,6 @@ var recalculateAllSelectedDimensions = this.recalculateAllSelectedDimensions = f
}
};
// this is how we map paths to our preferred relative segment types
var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];
// Debug tool to easily see the current matrix in the browser's console
var logMatrix = function(m) {
@ -1404,7 +1360,7 @@ var getMouseTarget = this.getMouseTarget = function(evt) {
}
});
setHref(newImage, last_good_img_url);
preventClickDefault(newImage);
svgedit.utilities.preventClickDefault(newImage);
break;
case 'square':
// FIXME: once we create the rect, we lose information that this was a square
@ -2353,14 +2309,6 @@ var getMouseTarget = this.getMouseTarget = function(evt) {
}());
// Function: preventClickDefault
// Prevents default browser click behaviour on the given element
//
// Parameters:
// img - The DOM element to prevent the cilck on
var preventClickDefault = function(img) {
$(img).click(function(e){e.preventDefault();});
};
// Group: Text edit functions
// Functions relating to editing text elements
@ -3684,165 +3632,7 @@ pathActions = canvas.pathActions = function() {
if (svgedit.browser.isWebkit()) {resetD(elem);}
},
// Convert a path to one with only absolute or relative values
convertPath: function(path, toRel) {
var i;
var segList = path.pathSegList;
var len = segList.numberOfItems;
var curx = 0, cury = 0;
var d = '';
var last_m = null;
for (i = 0; i < len; ++i) {
var seg = segList.getItem(i);
// if these properties are not in the segment, set them to zero
var x = seg.x || 0,
y = seg.y || 0,
x1 = seg.x1 || 0,
y1 = seg.y1 || 0,
x2 = seg.x2 || 0,
y2 = seg.y2 || 0;
var type = seg.pathSegType;
var letter = pathMap[type]['to'+(toRel?'Lower':'Upper')+'Case']();
var addToD = function(pnts, more, last) {
var str = '';
more = more ? ' ' + more.join(' ') : '';
last = last ? ' ' + svgedit.units.shortFloat(last) : '';
$.each(pnts, function(i, pnt) {
pnts[i] = svgedit.units.shortFloat(pnt);
});
d += letter + pnts.join(' ') + more + last;
};
switch (type) {
case 1: // z,Z closepath (Z/z)
d += 'z';
break;
case 12: // absolute horizontal line (H)
x -= curx;
case 13: // relative horizontal line (h)
if (toRel) {
curx += x;
letter = 'l';
} else {
x += curx;
curx = x;
letter = 'L';
}
// Convert to "line" for easier editing
addToD([[x, cury]]);
break;
case 14: // absolute vertical line (V)
y -= cury;
case 15: // relative vertical line (v)
if (toRel) {
cury += y;
letter = 'l';
} else {
y += cury;
cury = y;
letter = 'L';
}
// Convert to "line" for easier editing
addToD([[curx, y]]);
break;
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
x -= curx;
y -= cury;
case 5: // relative line (l)
case 3: // relative move (m)
// If the last segment was a "z", this must be relative to
if (last_m && segList.getItem(i-1).pathSegType === 1 && !toRel) {
curx = last_m[0];
cury = last_m[1];
}
case 19: // relative smooth quad (t)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx;
y += cury;
curx = x;
cury = y;
}
if (type === 3) {last_m = [curx, cury];}
addToD([[x, y]]);
break;
case 6: // absolute cubic (C)
x -= curx; x1 -= curx; x2 -= curx;
y -= cury; y1 -= cury; y2 -= cury;
case 7: // relative cubic (c)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx; x1 += curx; x2 += curx;
y += cury; y1 += cury; y2 += cury;
curx = x;
cury = y;
}
addToD([[x1, y1], [x2, y2], [x, y]]);
break;
case 8: // absolute quad (Q)
x -= curx; x1 -= curx;
y -= cury; y1 -= cury;
case 9: // relative quad (q)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx; x1 += curx;
y += cury; y1 += cury;
curx = x;
cury = y;
}
addToD([[x1, y1],[x, y]]);
break;
case 10: // absolute elliptical arc (A)
x -= curx;
y -= cury;
case 11: // relative elliptical arc (a)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx;
y += cury;
curx = x;
cury = y;
}
addToD([[seg.r1, seg.r2]], [
seg.angle,
(seg.largeArcFlag ? 1 : 0),
(seg.sweepFlag ? 1 : 0)
], [x, y]
);
break;
case 16: // absolute smooth cubic (S)
x -= curx; x2 -= curx;
y -= cury; y2 -= cury;
case 17: // relative smooth cubic (s)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx; x2 += curx;
y += cury; y2 += cury;
curx = x;
cury = y;
}
addToD([[x2, y2],[x, y]]);
break;
} // switch on path segment type
} // for each segment
return d;
}
convertPath: svgedit.utilities.convertPath
};
}();
// end pathActions
@ -4676,7 +4466,7 @@ this.setSvgString = function(xmlString) {
// change image href vals if possible
content.find('image').each(function() {
var image = this;
preventClickDefault(image);
svgedit.utilities.preventClickDefault(image);
var val = getHref(this);
if (val) {
if (val.indexOf('data:') === 0) {
@ -4966,44 +4756,25 @@ var identifyLayers = canvas.identifyLayers = function() {
//
// Parameters:
// name - The given name
this.createLayer = function(name) {
var batchCmd = new svgedit.history.BatchCommand('Create Layer');
var new_layer = getCurrentDrawing().createLayer(name);
batchCmd.addSubCommand(new svgedit.history.InsertElementCommand(new_layer));
addCommandToHistory(batchCmd);
this.createLayer = function(name, hrService) {
var new_layer = getCurrentDrawing().createLayer(name, historyRecordingService(hrService));
clearSelection();
call('changed', [new_layer]);
};
// Function: cloneLayer
// Creates a new top-level layer in the drawing with the given name, copies all the current layer's contents
// to it, and then clears the selection. This function then calls the 'changed' handler.
// This is an undoable action.
//
// Parameters:
// name - The given name
this.cloneLayer = function(name) {
var batchCmd = new svgedit.history.BatchCommand('Duplicate Layer');
var new_layer = svgdoc.createElementNS(NS.SVG, 'g');
var layer_title = svgdoc.createElementNS(NS.SVG, 'title');
layer_title.textContent = name;
new_layer.appendChild(layer_title);
var current_layer = getCurrentDrawing().getCurrentLayer();
$(current_layer).after(new_layer);
var childs = current_layer.childNodes;
var i;
for (i = 0; i < childs.length; i++) {
var ch = childs[i];
if (ch.localName == 'title') {continue;}
new_layer.appendChild(copyElem(ch));
}
/**
* Creates a new top-level layer in the drawing with the given name, copies all the current layer's contents
* to it, and then clears the selection. This function then calls the 'changed' handler.
* This is an undoable action.
* @param {string} name - The given name. If the layer name exists, a new name will be generated.
* @param {svgedit.history.HistoryRecordingService} hrService - History recording service
*/
this.cloneLayer = function(name, hrService) {
// Clone the current layer and make the cloned layer the new current layer
var new_layer = getCurrentDrawing().cloneLayer(name, historyRecordingService(hrService));
clearSelection();
identifyLayers();
batchCmd.addSubCommand(new svgedit.history.InsertElementCommand(new_layer));
addCommandToHistory(batchCmd);
canvas.setCurrentLayer(name);
leaveContext();
call('changed', [new_layer]);
};
@ -5058,11 +4829,8 @@ this.renameCurrentLayer = function(newname) {
var drawing = getCurrentDrawing();
var layer = drawing.getCurrentLayer();
if (layer) {
var result = drawing.setCurrentLayerName( newname);
var result = drawing.setCurrentLayerName(newname, historyRecordingService());
if (result) {
var batchCmd = new svgedit.history.BatchCommand('Rename Layer');
batchCmd.addSubCommand(new svgedit.history.ChangeElementCommand(result.title, {'#text':result.previousName}));
addCommandToHistory(batchCmd);
call('changed', [layer]);
return true;
}
@ -5158,20 +4926,14 @@ this.moveSelectedToLayer = function(layername) {
this.mergeLayer = function(hrService) {
if (!hrService) {
hrService = new svgedit.history.HistoryRecordingService(this.undoMgr);
}
getCurrentDrawing().mergeLayer(hrService);
getCurrentDrawing().mergeLayer(historyRecordingService(hrService));
clearSelection();
leaveContext();
call('changed', [svgcontent]);
};
this.mergeAllLayers = function(hrService) {
if (!hrService) {
hrService = new svgedit.history.HistoryRecordingService(this.undoMgr);
}
getCurrentDrawing().mergeAllLayers(hrService);
getCurrentDrawing().mergeAllLayers(historyRecordingService(hrService));
clearSelection();
leaveContext();
call('changed', [svgcontent]);
@ -6622,19 +6384,19 @@ this.pasteElements = function(type, x, y) {
var pasted = [];
var batchCmd = new svgedit.history.BatchCommand('Paste elements');
var drawing = getCurrentDrawing();
// Move elements to lastClickPoint
while (len--) {
var elem = cb[len];
if (!elem) {continue;}
var copy = copyElem(elem);
var copy = drawing.copyElem(elem);
// See if elem with elem ID is in the DOM already
if (!svgedit.utilities.getElem(elem.id)) {copy.id = elem.id;}
pasted.push(copy);
(current_group || getCurrentDrawing().getCurrentLayer()).appendChild(copy);
(current_group || drawing.getCurrentLayer()).appendChild(copy);
batchCmd.addSubCommand(new svgedit.history.InsertElementCommand(copy));
restoreRefElems(copy);
@ -6758,6 +6520,7 @@ var pushGroupProperties = this.pushGroupProperties = function(g, undoable) {
var gattrs = $(g).attr(['filter', 'opacity']);
var gfilter, gblur, changes;
var drawing = getCurrentDrawing();
for (i = 0; i < len; i++) {
var elem = children[i];
@ -6788,7 +6551,7 @@ var pushGroupProperties = this.pushGroupProperties = function(g, undoable) {
gfilter = svgedit.utilities.getRefElem(gattrs.filter);
} else {
// Clone the group's filter
gfilter = copyElem(gfilter);
gfilter = drawing.copyElem(gfilter);
svgedit.utilities.findDefs().appendChild(gfilter);
}
} else {
@ -7167,11 +6930,12 @@ this.cloneSelectedElements = function(x, y) {
this.clearSelection(true);
// note that we loop in the reverse way because of the way elements are added
// to the selectedElements array (top-first)
var drawing = getCurrentDrawing();
i = copiedElements.length;
while (i--) {
// clone each element and replace it within copiedElements
elem = copiedElements[i] = copyElem(copiedElements[i]);
(current_group || getCurrentDrawing().getCurrentLayer()).appendChild(elem);
elem = copiedElements[i] = drawing.copyElem(copiedElements[i]);
(current_group || drawing.getCurrentLayer()).appendChild(elem);
batchCmd.addSubCommand(new svgedit.history.InsertElementCommand(elem));
}
@ -7410,7 +7174,7 @@ this.getPrivateMethods = function() {
BatchCommand: BatchCommand,
call: call,
ChangeElementCommand: ChangeElementCommand,
copyElem: copyElem,
copyElem: function(elem) {return getCurrentDrawing().copyElem(elem)},
ffClone: ffClone,
findDefs: findDefs,
findDuplicateGradient: findDuplicateGradient,
@ -7428,7 +7192,7 @@ this.getPrivateMethods = function() {
logMatrix: logMatrix,
matrixMultiply: matrixMultiply,
MoveElementCommand: MoveElementCommand,
preventClickDefault: preventClickDefault,
preventClickDefault: svgedit.utilities.preventClickDefault,
recalculateAllSelectedDimensions: recalculateAllSelectedDimensions,
recalculateDimensions: recalculateDimensions,
remapElement: remapElement,

View File

@ -1137,4 +1137,250 @@ svgedit.utilities.buildJSPDFCallback = function (callJSPDF) {
});
};
/**
* Prevents default browser click behaviour on the given element
* @param img - The DOM element to prevent the click on
*/
svgedit.utilities.preventClickDefault = function(img) {
$(img).click(function(e){e.preventDefault();});
};
/**
* Create a clone of an element, updating its ID and its children's IDs when needed
* @param {Element} el - DOM element to clone
* @param {function()} getNextId - function the get the next unique ID.
* @returns {Element}
*/
svgedit.utilities.copyElem = function(el, getNextId) {
// manually create a copy of the element
var new_el = document.createElementNS(el.namespaceURI, el.nodeName);
$.each(el.attributes, function(i, attr) {
if (attr.localName != '-moz-math-font-style') {
new_el.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value);
}
});
// set the copied element's new id
new_el.removeAttribute('id');
new_el.id = getNextId();
// Opera's "d" value needs to be reset for Opera/Win/non-EN
// Also needed for webkit (else does not keep curved segments on clone)
if (svgedit.browser.isWebkit() && el.nodeName == 'path') {
var fixed_d = svgedit.utilities.convertPath(el);
new_el.setAttribute('d', fixed_d);
}
// now create copies of all children
$.each(el.childNodes, function(i, child) {
switch(child.nodeType) {
case 1: // element node
new_el.appendChild(svgedit.utilities.copyElem(child, getNextId));
break;
case 3: // text node
new_el.textContent = child.nodeValue;
break;
default:
break;
}
});
if ($(el).data('gsvg')) {
$(new_el).data('gsvg', new_el.firstChild);
} else if ($(el).data('symbol')) {
var ref = $(el).data('symbol');
$(new_el).data('ref', ref).data('symbol', ref);
} else if (new_el.tagName == 'image') {
preventClickDefault(new_el);
}
return new_el;
};
/**
* TODO: refactor callers in convertPath to use getPathDFromSegments instead of this function.
* Legacy code refactored from svgcanvas.pathActions.convertPath
* @param letter - path segment command
* @param {Array.<Array.<number>>} points - x,y points.
* @param {Array.<Array.<number>>=} morePoints - x,y points
* @param {Array.<number>=}lastPoint - x,y point
* @returns {string}
*/
function pathDSegment(letter, points, morePoints, lastPoint) {
$.each(points, function(i, pnt) {
points[i] = svgedit.units.shortFloat(pnt);
});
var segment = letter + points.join(' ');
if (morePoints) {
segment += ' ' + morePoints.join(' ');
}
if (lastPoint) {
segment += ' ' + svgedit.units.shortFloat(lastPoint);
}
return segment;
}
// this is how we map paths to our preferred relative segment types
var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];
/**
* TODO: move to pathActions.js when migrating rest of pathActions out of svgcanvas.js
* Convert a path to one with only absolute or relative values
* @param {Object} path - the path to convert
* @param {boolean} toRel - true of convert to relative
* @returns {string}
*/
svgedit.utilities.convertPath = function(path, toRel) {
var i;
var segList = path.pathSegList;
var len = segList.numberOfItems;
var curx = 0, cury = 0;
var d = '';
var last_m = null;
for (i = 0; i < len; ++i) {
var seg = segList.getItem(i);
// if these properties are not in the segment, set them to zero
var x = seg.x || 0,
y = seg.y || 0,
x1 = seg.x1 || 0,
y1 = seg.y1 || 0,
x2 = seg.x2 || 0,
y2 = seg.y2 || 0;
var type = seg.pathSegType;
var letter = pathMap[type]['to'+(toRel?'Lower':'Upper')+'Case']();
switch (type) {
case 1: // z,Z closepath (Z/z)
d += 'z';
break;
case 12: // absolute horizontal line (H)
x -= curx;
case 13: // relative horizontal line (h)
if (toRel) {
curx += x;
letter = 'l';
} else {
x += curx;
curx = x;
letter = 'L';
}
// Convert to "line" for easier editing
d += pathDSegment(letter,[[x, cury]]);
break;
case 14: // absolute vertical line (V)
y -= cury;
case 15: // relative vertical line (v)
if (toRel) {
cury += y;
letter = 'l';
} else {
y += cury;
cury = y;
letter = 'L';
}
// Convert to "line" for easier editing
d += pathDSegment(letter,[[curx, y]]);
break;
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
x -= curx;
y -= cury;
case 5: // relative line (l)
case 3: // relative move (m)
// If the last segment was a "z", this must be relative to
if (last_m && segList.getItem(i-1).pathSegType === 1 && !toRel) {
curx = last_m[0];
cury = last_m[1];
}
case 19: // relative smooth quad (t)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx;
y += cury;
curx = x;
cury = y;
}
if (type === 3) {last_m = [curx, cury];}
d += pathDSegment(letter,[[x, y]]);
break;
case 6: // absolute cubic (C)
x -= curx; x1 -= curx; x2 -= curx;
y -= cury; y1 -= cury; y2 -= cury;
case 7: // relative cubic (c)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx; x1 += curx; x2 += curx;
y += cury; y1 += cury; y2 += cury;
curx = x;
cury = y;
}
d += pathDSegment(letter,[[x1, y1], [x2, y2], [x, y]]);
break;
case 8: // absolute quad (Q)
x -= curx; x1 -= curx;
y -= cury; y1 -= cury;
case 9: // relative quad (q)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx; x1 += curx;
y += cury; y1 += cury;
curx = x;
cury = y;
}
d += pathDSegment(letter,[[x1, y1],[x, y]]);
break;
case 10: // absolute elliptical arc (A)
x -= curx;
y -= cury;
case 11: // relative elliptical arc (a)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx;
y += cury;
curx = x;
cury = y;
}
d += pathDSegment(letter,[[seg.r1, seg.r2]], [
seg.angle,
(seg.largeArcFlag ? 1 : 0),
(seg.sweepFlag ? 1 : 0)
], [x, y]
);
break;
case 16: // absolute smooth cubic (S)
x -= curx; x2 -= curx;
y -= cury; y2 -= cury;
case 17: // relative smooth cubic (s)
if (toRel) {
curx += x;
cury += y;
} else {
x += curx; x2 += curx;
y += cury; y2 += cury;
curx = x;
cury = y;
}
d += pathDSegment(letter,[[x2, y2],[x, y]]);
break;
} // switch on path segment type
} // for each segment
return d;
};
}());

View File

@ -8,10 +8,13 @@
<script src='../editor/svgedit.js'></script>
<script src='../editor/pathseg.js'></script>
<script src='../editor/browser.js'></script>
<script src='../editor/units.js'></script>
<script src='../editor/svgutils.js'></script>
<script src='../editor/draw.js'></script>
<script src='../editor/layer.js'></script>
<script src='qunit/qunit.js'></script>
<script src="sinon/sinon-1.17.3.js"></script>
<script src="sinon/sinon-qunit-1.0.0.js"></script>
<script>
$(function() {
// log function
@ -26,6 +29,15 @@
var LAYER1 = 'Layer 1';
var LAYER2 = 'Layer 2';
var LAYER3 = 'Layer 3';
var PATH_ATTR = {
// clone will convert relative to absolute, so the test for equality fails.
//'d': 'm7.38867,57.38867c0,-27.62431 22.37569,-50 50,-50c27.62431,0 50,22.37569 50,50c0,27.62431 -22.37569,50 -50,50c-27.62431,0 -50,-22.37569 -50,-50z',
'd': 'M7.389,57.389C7.389,29.764 29.764,7.389 57.389,7.389C85.013,7.389 107.389,29.764 107.389,57.389C107.389,85.013 85.013,107.389 57.389,107.389C29.764,107.389 7.389,85.013 7.389,57.389z',
'transform': 'rotate(45 57.388671875000036,57.388671874999986) ',
'stroke-width': '5',
'stroke': '#660000',
'fill':'#ff0000'
};
var svg = document.createElementNS(NS.SVG, 'svg');
var sandbox = document.getElementById('sandbox');
@ -37,6 +49,19 @@
svg_n.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE);
svg_n.setAttributeNS(NS.SE, 'se:nonce', NONCE);
svgedit.units.init({
// used by svgedit.units.shortFloat - call path: cloneLayer -> copyElem -> convertPath -> pathDSegment -> shortFloat
getRoundDigits: function() { return 3; }
});
function createSVGElement(jsonMap) {
var elem = document.createElementNS(svgedit.NS.SVG, jsonMap['element']);
for (var attr in jsonMap['attr']) {
elem.setAttribute(attr, jsonMap['attr'][attr]);
}
return elem;
}
var setupSvgWith3Layers = function(svgElem) {
var layer1 = document.createElementNS(NS.SVG, 'g');
var layer1_title = document.createElementNS(NS.SVG, 'title');
@ -55,12 +80,51 @@
layer3_title.appendChild(document.createTextNode(LAYER3));
layer3.appendChild(layer3_title);
svgElem.appendChild(layer3);
return [layer1, layer2, layer3];
};
var createSomeElementsInGroup = function(group) {
group.appendChild(createSVGElement({
'element': 'path',
'attr': PATH_ATTR
}));
// group.appendChild(createSVGElement({
// 'element': 'path',
// 'attr': {'d': 'M0,1L2,3'}
// }));
group.appendChild(createSVGElement({
'element': 'rect',
'attr': {'x': '0', 'y': '1', 'width': '5', 'height': '10'}
}));
group.appendChild(createSVGElement({
'element': 'line',
'attr': {'x1': '0', 'y1': '1', 'x2': '5', 'y2': '6'}
}));
var g = createSVGElement({
'element': 'g',
'attr': {}
});
g.appendChild(createSVGElement({
'element': 'rect',
'attr': {'x': '0', 'y': '1', 'width': '5', 'height': '10'}
}));
group.appendChild(g);
return 4;
};
var cleanupSvg = function(svgElem) {
while(svgElem.firstChild) {svgElem.removeChild(svgElem.firstChild);}
};
module('svgedit.draw.Drawing', {
setup: function() {
},
teardown: function() {
}
});
test('Test draw module', function() {
expect(4);
@ -376,18 +440,55 @@
equals(typeof drawing.setCurrentLayer, typeof function(){});
drawing.setCurrentLayer(LAYER2);
equals(drawing.getCurrentLayerName(LAYER2), LAYER2);
equals(drawing.getCurrentLayerName(), LAYER2);
equals(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup());
drawing.setCurrentLayer(LAYER3);
equals(drawing.getCurrentLayerName(LAYER3), LAYER3);
equals(drawing.getCurrentLayerName(), LAYER3);
equals(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup());
cleanupSvg(svg);
});
test('Test setCurrentLayerName()', function() {
var mockHrService = {
changeElement: this.spy()
};
var drawing = new svgedit.draw.Drawing(svg);
setupSvgWith3Layers(svg);
drawing.identifyLayers();
ok(drawing.setCurrentLayerName);
equals(typeof drawing.setCurrentLayerName, typeof function(){});
var oldName = drawing.getCurrentLayerName();
var newName = 'New Name'
ok(drawing.layer_map[oldName]);
equals(drawing.layer_map[newName], undefined); // newName shouldn't exist.
var result = drawing.setCurrentLayerName(newName, mockHrService);
equals(result, newName);
equals(drawing.getCurrentLayerName(), newName);
// Was the map updated?
equals(drawing.layer_map[oldName], undefined);
equals(drawing.layer_map[newName], drawing.current_layer);
// Was mockHrService called?
ok(mockHrService.changeElement.calledOnce);
equals(oldName, mockHrService.changeElement.getCall(0).args[1]['#text']);
equals(newName, mockHrService.changeElement.getCall(0).args[0].textContent);
cleanupSvg(svg);
});
test('Test createLayer()', function() {
expect(7);
expect(10);
var mockHrService = {
startBatchCommand: this.spy(),
endBatchCommand: this.spy(),
insertElement: this.spy()
};
var drawing = new svgedit.draw.Drawing(svg);
setupSvgWith3Layers(svg);
@ -397,13 +498,178 @@
equals(typeof drawing.createLayer, typeof function(){});
var NEW_LAYER_NAME = 'Layer A';
var layer_g = drawing.createLayer(NEW_LAYER_NAME);
var layer_g = drawing.createLayer(NEW_LAYER_NAME, mockHrService);
equals(drawing.getNumLayers(), 4);
equals(layer_g, drawing.getCurrentLayer());
equals(layer_g.getAttribute('class'), LAYER_CLASS);
equals(NEW_LAYER_NAME, drawing.getCurrentLayerName());
equals(NEW_LAYER_NAME, drawing.getLayerName(3));
equals(layer_g, mockHrService.insertElement.getCall(0).args[0]);
ok(mockHrService.startBatchCommand.calledOnce);
ok(mockHrService.endBatchCommand.calledOnce);
cleanupSvg(svg);
});
test('Test mergeLayer()', function() {
var mockHrService = {
startBatchCommand: this.spy(),
endBatchCommand: this.spy(),
moveElement: this.spy(),
removeElement: this.spy()
};
var drawing = new svgedit.draw.Drawing(svg);
var layers = setupSvgWith3Layers(svg);
var elementCount = createSomeElementsInGroup(layers[2]) + 1; // +1 for title element
equals(layers[1].childElementCount, 1);
equals(layers[2].childElementCount, elementCount);
drawing.identifyLayers();
equals(drawing.getCurrentLayer(), layers[2])
ok(drawing.mergeLayer);
equals(typeof drawing.mergeLayer, typeof function(){});
drawing.mergeLayer(mockHrService);
equals(drawing.getNumLayers(), 2);
equals(svg.childElementCount, 2);
equals(drawing.getCurrentLayer(), layers[1]);
equals(layers[1].childElementCount, elementCount);
// check history record
ok(mockHrService.startBatchCommand.calledOnce);
ok(mockHrService.endBatchCommand.calledOnce);
equals(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge Layer');
equals(mockHrService.moveElement.callCount, elementCount - 1); // -1 because the title was not moved.
equals(mockHrService.removeElement.callCount, 2); // remove group and title.
cleanupSvg(svg);
});
test('Test mergeLayer() when no previous layer to merge', function() {
var mockHrService = {
startBatchCommand: this.spy(),
endBatchCommand: this.spy(),
moveElement: this.spy(),
removeElement: this.spy()
};
var drawing = new svgedit.draw.Drawing(svg);
var layers = setupSvgWith3Layers(svg);
drawing.identifyLayers();
drawing.setCurrentLayer(LAYER1);
equals(drawing.getCurrentLayer(), layers[0]);
drawing.mergeLayer(mockHrService);
equals(drawing.getNumLayers(), 3);
equals(svg.childElementCount, 3);
equals(drawing.getCurrentLayer(), layers[0]);
equals(layers[0].childElementCount, 1);
equals(layers[1].childElementCount, 1);
equals(layers[2].childElementCount, 1);
// check history record
equals(mockHrService.startBatchCommand.callCount, 0);
equals(mockHrService.endBatchCommand.callCount, 0);
equals(mockHrService.moveElement.callCount, 0);
equals(mockHrService.removeElement.callCount, 0);
cleanupSvg(svg);
});
test('Test mergeAllLayers()', function() {
var mockHrService = {
startBatchCommand: this.spy(),
endBatchCommand: this.spy(),
moveElement: this.spy(),
removeElement: this.spy()
};
var drawing = new svgedit.draw.Drawing(svg);
var layers = setupSvgWith3Layers(svg);
var elementCount = createSomeElementsInGroup(layers[0]) + 1; // +1 for title element
createSomeElementsInGroup(layers[1]);
createSomeElementsInGroup(layers[2]);
equals(layers[0].childElementCount, elementCount);
equals(layers[1].childElementCount, elementCount);
equals(layers[2].childElementCount, elementCount);
drawing.identifyLayers();
ok(drawing.mergeAllLayers);
equals(typeof drawing.mergeAllLayers, typeof function(){});
drawing.mergeAllLayers(mockHrService);
equals(drawing.getNumLayers(), 1);
equals(svg.childElementCount, 1);
equals(drawing.getCurrentLayer(), layers[0]);
equals(layers[0].childElementCount, elementCount * 3 - 2); // -2 because two titles were deleted.
// check history record
equals(mockHrService.startBatchCommand.callCount, 3); // mergeAllLayers + 2 * mergeLayer
equals(mockHrService.endBatchCommand.callCount, 3);
equals(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge all Layers');
equals(mockHrService.startBatchCommand.getCall(1).args[0], 'Merge Layer');
equals(mockHrService.startBatchCommand.getCall(2).args[0], 'Merge Layer');
// moveElement count is times 3 instead of 2, because one layer's elements were moved twice.
// moveElement count is minus 3 because the three titles were not moved.
equals(mockHrService.moveElement.callCount, elementCount * 3 - 3);
equals(mockHrService.removeElement.callCount, 2 * 2); // remove group and title twice.
cleanupSvg(svg);
});
test('Test cloneLayer()', function() {
var mockHrService = {
startBatchCommand: this.spy(),
endBatchCommand: this.spy(),
insertElement: this.spy()
};
var drawing = new svgedit.draw.Drawing(svg);
var layers = setupSvgWith3Layers(svg);
var layer3 = layers[2];
var elementCount = createSomeElementsInGroup(layer3) + 1; // +1 for title element
equals(layer3.childElementCount, elementCount);
drawing.identifyLayers();
ok(drawing.cloneLayer);
equals(typeof drawing.cloneLayer, typeof function(){});
var clone = drawing.cloneLayer('clone', mockHrService);
equals(drawing.getNumLayers(), 4);
equals(svg.childElementCount, 4);
equals(drawing.getCurrentLayer(), clone);
equals(clone.childElementCount, elementCount);
// check history record
ok(mockHrService.startBatchCommand.calledOnce); // mergeAllLayers + 2 * mergeLayer
ok(mockHrService.endBatchCommand.calledOnce);
equals(mockHrService.startBatchCommand.getCall(0).args[0], 'Duplicate Layer');
equals(mockHrService.insertElement.callCount, 1);
equals(mockHrService.insertElement.getCall(0).args[0], clone);
// check that path is cloned properly
equals(clone.childNodes.length, elementCount);
var path = clone.childNodes[1]
equals(path.id, 'svg_1');
equals(path.getAttribute('d'), PATH_ATTR.d);
equals(path.getAttribute('transform'), PATH_ATTR.transform);
equals(path.getAttribute('fill'), PATH_ATTR.fill);
equals(path.getAttribute('stroke'), PATH_ATTR.stroke);
equals(path.getAttribute('stroke-width'), PATH_ATTR['stroke-width']);
// check that g is cloned properly
var g = clone.childNodes[4]
equals(g.childNodes.length, 1);
equals(g.id, 'svg_4');
cleanupSvg(svg);
});

6437
test/sinon/sinon-1.17.3.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
/**
* sinon-qunit 1.0.0, 2010/12/09
*
* @author Christian Johansen (christian@cjohansen.no)
*
* (The BSD License)
*
* Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Christian Johansen nor the names of his contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*global sinon, QUnit, test*/
sinon.assert.fail = function (msg) {
QUnit.ok(false, msg);
};
sinon.assert.pass = function (assertion) {
QUnit.ok(true, assertion);
};
sinon.config = {
injectIntoThis: true,
injectInto: null,
properties: ["spy", "stub", "mock", "clock", "sandbox"],
useFakeTimers: true,
useFakeServer: false
};
(function (global) {
var qTest = QUnit.test;
QUnit.test = global.test = function (testName, expected, callback, async) {
if (arguments.length === 2) {
callback = expected;
expected = null;
}
return qTest(testName, expected, sinon.test(callback), async);
};
}(this));