diff --git a/src/index.html b/src/index.html index 6e208eb..02c2cec 100644 --- a/src/index.html +++ b/src/index.html @@ -383,9 +383,11 @@ Offset -
Remove path
+
Change side
- + + +
Remove path
@@ -477,7 +479,7 @@
Add Node
Delete Node
-
Open Path
+
Open/close Path
diff --git a/src/js/Text.js b/src/js/Text.js index 5aeb7bf..7188a63 100644 --- a/src/js/Text.js +++ b/src/js/Text.js @@ -22,6 +22,19 @@ MD.Text = function(){ editor.selectedChanged(window, [text]); } + function changeTextOnPath(){ + const elem = svgCanvas.getSelectedElems()[0]; + const textPath = elem.querySelector("textPath"); + if (!textPath) return; + const path = svgCanvas.getTextPath(elem); + const d = path.getAttribute("d"); + const reversed = utils.SVGPathEditor.reverse(d); + path.setAttribute("d", reversed); + const selector = svgCanvas.selectorManager.requestSelector(elem); + selector.resize(); + editor.selectedChanged(window, [elem]); + } + function setBold(){ if ($(this).hasClass("disabled")) return; svgCanvas.setBold( !svgCanvas.getBold() ); @@ -40,6 +53,7 @@ MD.Text = function(){ $('#tool_text_on_path').click(placeTextOnPath); $('#tool_release_text_on_path').click(releaseTextOnPath); + $('#tool_change_text_on_path').click(changeTextOnPath); $("#tool_bold").on("click", setBold); $("#tool_italic").on("click", setItalic); diff --git a/src/js/editor.js b/src/js/editor.js index a80176b..f49eb24 100644 --- a/src/js/editor.js +++ b/src/js/editor.js @@ -104,7 +104,7 @@ MD.Editor = function(){ svgCanvas.convertToPath(); var elems = svgCanvas.getSelectedElems() svgCanvas.selectorManager.requestSelector(elems[0]).reset(elems[0]) - svgCanvas.selectorManager.requestSelector(elems[0]).selectorRect.setAttribute("display", "none"); + //svgCanvas.selectorManager.requestSelector(elems[0]).selectorRect.setAttribute("display", "none"); svgCanvas.setMode("pathedit"); svgCanvas.pathActions.toEditMode(elems[0]); svgCanvas.clearSelection(); diff --git a/src/js/svgcanvas.js b/src/js/svgcanvas.js index 12da7cd..827a922 100644 --- a/src/js/svgcanvas.js +++ b/src/js/svgcanvas.js @@ -3359,6 +3359,11 @@ var getMouseTarget = this.getMouseTarget = function(evt) { canvas.setMode("pathedit"); canvas.pathActions.toEditMode(evt_target); } + + // Reset context + if(tagName === "ellipse" || tagName === "circle" || tagName === "line" || tagName === "rect") { + editor.convertToPath(); + } if((parent.tagName !== 'g' && parent.tagName !== 'a') || parent === getCurrentDrawing().getCurrentLayer() || @@ -4038,13 +4043,15 @@ var pathActions = canvas.pathActions = function() { var endseg = drawn_path.createSVGPathSegClosePath(); seglist.appendItem(newseg); seglist.appendItem(endseg); - selectorManager.requestSelector(newpath).showGrips(true) + selectorManager.requestSelector(newpath).showGrips(true); } else if(len < 3) { keep = false; + return keep; } stretchy.parentNode.removeChild(stretchy); + state.set("canvasMode", "select"); // this will signal to commit the path element = newpath; @@ -4526,6 +4533,15 @@ var pathActions = canvas.pathActions = function() { addToSelection([elem], true); call("changed", selectedElements); }, + + reverse: function() { + + var elem = selectedElements[0]; + if(!elem || !elem.getAttribute("d")) return; + + const d = elem.getAttribute("d"); + var reversed = reverse(path); + }, clear: function(remove) { current_path = null; @@ -4623,113 +4639,114 @@ var pathActions = canvas.pathActions = function() { return svgedit.path.path; }, opencloseSubPath: function() { - var sel_pts = svgedit.path.path.selected_pts; + const path = svgedit.path.path; + const selPts = path.selected_pts; // Only allow one selected node for now - if(sel_pts.length !== 1) return; - - var elem = svgedit.path.path.elem; - var list = elem.pathSegList; + if (selPts.length !== 1) { return; } - var len = list.numberOfItems; + const { elem } = path; + const list = elem.pathSegList; - var index = sel_pts[0]; - - var open_pt = null; - var start_item = null; + // const len = list.numberOfItems; + + const index = selPts[0]; + + let openPt = null; + let startItem = null; // Check if subpath is already open - svgedit.path.path.eachSeg(function(i) { - if(this.type === 2 && i <= index) { - start_item = this.item; + path.eachSeg(function (i) { + if (this.type === 2 && i <= index) { + startItem = this.item; } - if(i <= index) return true; - if(this.type === 2) { + if (i <= index) { return true; } + if (this.type === 2) { // Found M first, so open - open_pt = i; - return false; - } else if(this.type === 1) { - // Found Z first, so closed - open_pt = false; + openPt = i; return false; } + if (this.type === 1) { + // Found Z first, so closed + openPt = false; + return false; + } + return true; }); - - if(open_pt == null) { + + if (openPt == null) { // Single path, so close last seg - open_pt = svgedit.path.path.segs.length - 1; + openPt = path.segs.length - 1; } - if(open_pt !== false) { + if (openPt !== false) { // Close this path - + // Create a line going to the previous "M" - var newseg = elem.createSVGPathSegLinetoAbs(start_item.x, start_item.y); - - var closer = elem.createSVGPathSegClosePath(); - if(open_pt == svgedit.path.path.segs.length) { + const newseg = elem.createSVGPathSegLinetoAbs(startItem.x, startItem.y); + + const closer = elem.createSVGPathSegClosePath(); + if (openPt === path.segs.length - 1) { list.appendItem(newseg); list.appendItem(closer); } else { - svgedit.path.insertItemBefore(elem, closer, open_pt); - svgedit.path.insertItemBefore(elem, newseg, open_pt); + list.insertItemBefore(closer, openPt); + list.insertItemBefore(newseg, openPt); } - - svgedit.path.path.init().selectPt(open_pt+1); + + path.init().selectPt(openPt + 1); return; } - - // M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2 // M 2,2 L 3,3 L 1,1 - - // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z - // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z - - var seg = svgedit.path.path.segs[index]; - - if(seg.mate) { + + // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z + // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z + + const seg = path.segs[index]; + + if (seg.mate) { list.removeItem(index); // Removes last "L" list.removeItem(index); // Removes the "Z" - svgedit.path.path.init().selectPt(index - 1); + path.init().selectPt(index - 1); return; } - - var last_m, z_seg; - - // Find this sub-path's closing point and remove - for(var i=0; i 2) { + for (a = 0; a < alen; a += 2) { + if (op === "m") { + x += args[a]; + y += args[a+1]; + } else { + x = args[a]; + y = args[a+1]; + } + normalized += "L " + x + " " + y + " "; + } + } + } + + // lineto commands + else if(lop === "l") { + for (a = 0; a < alen; a += 2) { + if (op === "l") { + x += args[a]; + y += args[a+1]; + } else { + x = args[a]; + y = args[a+1]; + } + normalized += "L " + x + " " + y + " "; + } + } + else if(lop === "h") { + for (a = 0; a < alen; a++) { + if (op === "h") { + x += args[a]; + } else { + x = args[a]; + } + normalized += "L " + x + " " + y + " "; + } + } + else if(lop === "v") { + for (a = 0; a < alen; a++) { + if (op === "v") { + y += args[a]; + } else { + y = args[a]; + } + normalized += "L " + x + " " + y + " "; + } + } + + // quadratic curveto commands + else if(lop === "q") { + for (a = 0; a < alen; a += 4) { + if (op === "q") { + cx = x + args[a]; + cy = y + args[a+1]; + x += args[a+2]; + y += args[a+3]; + } else { + cx = args[a]; + cy = args[a+1]; + x = args[a+2]; + y = args[a+3]; + } + normalized += "Q " + cx + " " + cy + " " + x + " " + y + " "; + } + } + else if(lop === "t") { + for (a = 0; a < alen; a += 2) { + // reflect previous cx/cy over x/y + cx = x + (x-cx); + cy = y + (y-cy); + // then get real end point + if (op === "t") { + x += args[a]; + y += args[a+1]; + } else { + x = args[a]; + y = args[a+1]; + } + normalized += "Q " + cx + " " + cy + " " + x + " " + y + " "; + } + } + + // cubic curveto commands + else if(lop === "c") { + for (a = 0; a < alen; a += 6) { + if (op === "c") { + cx = x + args[a]; + cy = y + args[a+1]; + cx2 = x + args[a+2]; + cy2 = y + args[a+3]; + x += args[a+4]; + y += args[a+5]; + } else { + cx = args[a]; + cy = args[a+1]; + cx2 = args[a+2]; + cy2 = args[a+3]; + x = args[a+4] + y = args[a+5]; + } + normalized += "C " + cx + " " + cy + " " + cx2 + " " + cy2 + " " + x + " " + y + " "; + } + } + else if(lop === "s") { + for (a = 0; a < alen; a += 4) { + // reflect previous cx2/cy2 over x/y + cx = x + (x-cx2); + cy = y + (y-cy2); + // then get real control and end point + if (op === "s") { + cx2 = x + args[a]; + cy2 = y + args[a+1]; + x += args[a+2]; + y += args[a+3]; + } else { + cx2 = args[a]; + cy2 = args[a+1]; + x = args[a+2] + y = args[a+3]; + } + normalized += "C " + cx + " " + cy + " " + cx2 + " " + cy2 + " " + x + " " + y + " "; + } + } + + // rx ry x-axis-rotation large-arc-flag sweep-flag x y + // a 25,25 -30 0, 1 50,-25 + + // arc command + else if(lop === "a") { + for (a = 0; a < alen; a += 7) { + rx = args[a]; + ry = args[a+1]; + xrot = args[a+2]; + + lflag = oargs[a+3]; // we need the original string to deal with leading zeroes + let fixed = false; + + if(lflag.length > 1) { + let b1 = parseInt(lflag[0]), + b2 = parseInt(lflag[1]), + rest = undefined; + if(lflag.length > 2) rest = parseFloat(lflag.substring(2)); + args[a+3] = b1; + args.splice(a+4, 0, b2); + if (rest!==undefined) args.splice(a+5, 0, rest); + fixed = true; + } + lflag = args[a+3]; + + sweep = fixed ? args[a+4] : oargs[a+4]; // we need the original string to deal with leading zeroes + if(!fixed && sweep.length > 1) { + args[a+4] = parseInt(sweep[0]); + args.splice(a+5, 0, parseFloat(sweep.substring(1))); + } + sweep = args[a+4]; + + if (op === "a") { + x += args[a+5]; + y += args[a+6]; + } else { + x = args[a+5]; + y = args[a+6]; + } + + normalized += "A " + rx + " " + ry + " " + xrot + " " + lflag + " " + sweep + " " + x + " " + y + " "; + } + } + + else if(lop === "z") { + normalized += "Z "; + // not unimportant: path closing changes the current x/y coordinate + x = sx; + y = sy; } } - } \ No newline at end of file + return normalized.trim(); + }; + + /** + * Reverse an SVG path. + * As long as the input path is normalised, this is actually really + * simple to do. As all pathing commands are symmetrical, meaning + * that they render the same when you reverse the coordinate order, + * the grand trick here is to reverse the path (making sure to keep + * coordinates ordered pairwise) and shift the operators left by + * one or two coordinate pairs depending on the operator: + * + * - Z is removed (after noting it existed), + * - L moves to 2 spots earlier (skipping one coordinate), + * - Q moves to 2 spots earlier (skipping one coordinate), + * - C moves to 4 spots earlier (skipping two coordinates) + * and its arguments get reversed, + * - the path start becomes M. + * - the path end becomes Z iff it was there to begin with. + */ + function reverseNormalizedPath(normalized) { + var terms = normalized.trim().split(' '), + term, + tlen = terms.length, + tlen1 = tlen-1, + t, + reversed = [], + x, y, + pair, pairs, + shift, + matcher = new RegExp('[QAZLCM]',''), + closed = terms.slice(-1)[0].toUpperCase() === 'Z'; + + for (t = 0; t < tlen; t++) { + term = terms[t]; + + // Is this an operator? If it is, run through its + // argument list, which we know is fixed length. + if (matcher.test(term)) { + + // Arc processing relies on not-just-coordinates + if (term === "A") { + reversed.push(terms[t+5] === '0' ? '1' : '0'); + reversed.push(terms[t+4]); + reversed.push(terms[t+3]); + reversed.push(terms[t+2]); + reversed.push(terms[t+1]); + reversed.push(term); + reversed.push(terms[t+7]); + reversed.push(terms[t+6]); + t += 7; + continue; + } + + // how many coordinate pairs do we need to read, + // and by how many pairs should this operator be + // shifted left? + else if (term === "C") { pairs = 3; shift = 2; } + else if (term === "Q") { pairs = 2; shift = 1; } + else if (term === "L") { pairs = 1; shift = 1; } + else if (term === "M") { pairs = 1; shift = 0; } + else { continue; } + + // do the argument reading and operator shifting + if (pairs === shift) { + reversed.push(term); + } + for (pair = 0; pair < pairs; pair++) { + if (pair === shift) { + reversed.push(term); + } + x = terms[++t]; + y = terms[++t]; + reversed.push(y); + reversed.push(x); + } + } + // the code has been set up so that every time we + // iterate because of the for() operation, the term + // we see is a pathing operator, not a number. As + // such, if we get to this "else" the path is malformed. + else { + var pre = terms.slice(Math.max(t-3,0),3).join(" "); + post = terms.slice(t+1,Math.min(t+4,tlen1)).join(" "); + range = pre + " [" + term + "] " + post; + throw("Error while trying to reverse normalized SVG path, at position "+t+" ("+range+").\n" + + "Either the path is not normalised, or it's malformed."); } + } + + reversed.push('M'); + + // generating the reversed path string involves + // running through our transformed terms in reverse. + var revstring = "", rlen1 = reversed.length-1, r; + for (r = rlen1; r > 0; r--) { + revstring += reversed[r] + " "; + } + if (closed) revstring += "Z"; + revstring = revstring.replace(/M M/g,'Z M'); + + return revstring; + }; + + /** + * This is the function that you'll actually want to + * make use of, because it lets you reverse individual + * subpaths in some "d" attribute. + */ + function reverseSubPath(path, subpath) { + subpath = parseInt(subpath)==subpath ? subpath : false; + var path = normalizePath(path), + paths = path.replace(/M/g,'|M').split("|"), + revpath; + paths.splice(0,1); + if (subpath !== false && subpath >= paths.length) { + return path; + } + + if (subpath === false) { + paths = paths.map(function(spath) { + return reverseNormalizedPath(spath.trim()); + }); + } else { + var spath = paths[subpath]; + if (spath) { + revpath = reverseNormalizedPath(spath.trim()); + paths[subpath] = revpath; + } + } + + return paths.join(" ").replace(/ +/g,' ').trim(); + }; + + /** + * Our return object + */ + var SVGPathEditor = { + normalize: normalizePath, + reverseNormalized: reverseNormalizedPath, + reverse: reverseSubPath + }; + + return SVGPathEditor; +}));