path.js 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. /*globals $, svgedit, svgroot*/
  2. /*jslint vars: true, eqeq: true, continue: true*/
  3. /**
  4. * Package: svgedit.path
  5. *
  6. * Licensed under the MIT License
  7. *
  8. * Copyright(c) 2011 Alexis Deveria
  9. * Copyright(c) 2011 Jeff Schiller
  10. */
  11. // Dependencies:
  12. // 1) jQuery
  13. // 2) browser.js
  14. // 3) math.js
  15. // 4) svgutils.js
  16. (function() {'use strict';
  17. if (!svgedit.path) {
  18. svgedit.path = {};
  19. }
  20. var NS = svgedit.NS;
  21. var uiStrings = {
  22. 'pathNodeTooltip': 'Drag node to move it. Double-click node to change segment type',
  23. 'pathCtrlPtTooltip': 'Drag control point to adjust curve properties'
  24. };
  25. var segData = {
  26. 2: ['x', 'y'],
  27. 4: ['x', 'y'],
  28. 6: ['x', 'y', 'x1', 'y1', 'x2', 'y2'],
  29. 8: ['x', 'y', 'x1', 'y1'],
  30. 10: ['x', 'y', 'r1', 'r2', 'angle', 'largeArcFlag', 'sweepFlag'],
  31. 12: ['x'],
  32. 14: ['y'],
  33. 16: ['x', 'y', 'x2', 'y2'],
  34. 18: ['x', 'y']
  35. };
  36. var pathFuncs = [];
  37. var link_control_pts = true;
  38. // Stores references to paths via IDs.
  39. // TODO: Make this cross-document happy.
  40. var pathData = {};
  41. svgedit.path.setLinkControlPoints = function(lcp) {
  42. link_control_pts = lcp;
  43. };
  44. svgedit.path.path = null;
  45. var editorContext_ = null;
  46. svgedit.path.init = function(editorContext) {
  47. editorContext_ = editorContext;
  48. pathFuncs = [0, 'ClosePath'];
  49. var pathFuncsStrs = ['Moveto', 'Lineto', 'CurvetoCubic', 'CurvetoQuadratic', 'Arc',
  50. 'LinetoHorizontal', 'LinetoVertical', 'CurvetoCubicSmooth', 'CurvetoQuadraticSmooth'];
  51. $.each(pathFuncsStrs, function(i, s) {
  52. pathFuncs.push(s+'Abs');
  53. pathFuncs.push(s+'Rel');
  54. });
  55. };
  56. svgedit.path.insertItemBefore = function(elem, newseg, index) {
  57. // Support insertItemBefore on paths for FF2
  58. var list = elem.pathSegList;
  59. if (svgedit.browser.supportsPathInsertItemBefore()) {
  60. list.insertItemBefore(newseg, index);
  61. return;
  62. }
  63. var len = list.numberOfItems;
  64. var arr = [];
  65. var i;
  66. for (i=0; i < len; i++) {
  67. var cur_seg = list.getItem(i);
  68. arr.push(cur_seg);
  69. }
  70. list.clear();
  71. for (i=0; i < len; i++) {
  72. if (i == index) { //index+1
  73. list.appendItem(newseg);
  74. }
  75. list.appendItem(arr[i]);
  76. }
  77. };
  78. // TODO: See if this should just live in replacePathSeg
  79. svgedit.path.ptObjToArr = function(type, seg_item) {
  80. var arr = segData[type], len = arr.length;
  81. var i, out = [];
  82. for (i = 0; i < len; i++) {
  83. out[i] = seg_item[arr[i]];
  84. }
  85. return out;
  86. };
  87. svgedit.path.getGripPt = function(seg, alt_pt) {
  88. var out = {
  89. x: alt_pt? alt_pt.x : seg.item.x,
  90. y: alt_pt? alt_pt.y : seg.item.y
  91. }, path = seg.path;
  92. if (path.matrix) {
  93. var pt = svgedit.math.transformPoint(out.x, out.y, path.matrix);
  94. out = pt;
  95. }
  96. out.x *= editorContext_.getCurrentZoom();
  97. out.y *= editorContext_.getCurrentZoom();
  98. return out;
  99. };
  100. svgedit.path.getPointFromGrip = function(pt, path) {
  101. var out = {
  102. x: pt.x,
  103. y: pt.y
  104. };
  105. if (path.matrix) {
  106. pt = svgedit.math.transformPoint(out.x, out.y, path.imatrix);
  107. out.x = pt.x;
  108. out.y = pt.y;
  109. }
  110. out.x /= editorContext_.getCurrentZoom();
  111. out.y /= editorContext_.getCurrentZoom();
  112. return out;
  113. };
  114. svgedit.path.addPointGrip = function(index, x, y) {
  115. // create the container of all the point grips
  116. var pointGripContainer = svgedit.path.getGripContainer();
  117. var pointGrip = svgedit.utilities.getElem('pathpointgrip_'+index);
  118. // create it
  119. if (!pointGrip) {
  120. pointGrip = document.createElementNS(NS.SVG, 'circle');
  121. svgedit.utilities.assignAttributes(pointGrip, {
  122. 'id': 'pathpointgrip_' + index,
  123. 'display': 'none',
  124. 'r': 4,
  125. 'fill': '#0FF',
  126. 'stroke': '#00F',
  127. 'stroke-width': 2,
  128. 'cursor': 'move',
  129. 'style': 'pointer-events:all',
  130. 'xlink:title': uiStrings.pathNodeTooltip
  131. });
  132. pointGrip = pointGripContainer.appendChild(pointGrip);
  133. var grip = $('#pathpointgrip_'+index);
  134. grip.dblclick(function() {
  135. if (svgedit.path.path) {
  136. svgedit.path.path.setSegType();
  137. }
  138. });
  139. }
  140. if (x && y) {
  141. // set up the point grip element and display it
  142. svgedit.utilities.assignAttributes(pointGrip, {
  143. 'cx': x,
  144. 'cy': y,
  145. 'display': 'inline'
  146. });
  147. }
  148. return pointGrip;
  149. };
  150. svgedit.path.getGripContainer = function() {
  151. var c = svgedit.utilities.getElem('pathpointgrip_container');
  152. if (!c) {
  153. var parent = svgedit.utilities.getElem('selectorParentGroup');
  154. c = parent.appendChild(document.createElementNS(NS.SVG, 'g'));
  155. c.id = 'pathpointgrip_container';
  156. }
  157. return c;
  158. };
  159. svgedit.path.addCtrlGrip = function(id) {
  160. var pointGrip = svgedit.utilities.getElem('ctrlpointgrip_'+id);
  161. if (pointGrip) {return pointGrip;}
  162. pointGrip = document.createElementNS(NS.SVG, 'circle');
  163. svgedit.utilities.assignAttributes(pointGrip, {
  164. 'id': 'ctrlpointgrip_' + id,
  165. 'display': 'none',
  166. 'r': 4,
  167. 'fill': '#0FF',
  168. 'stroke': '#55F',
  169. 'stroke-width': 1,
  170. 'cursor': 'move',
  171. 'style': 'pointer-events:all',
  172. 'xlink:title': uiStrings.pathCtrlPtTooltip
  173. });
  174. svgedit.path.getGripContainer().appendChild(pointGrip);
  175. return pointGrip;
  176. };
  177. svgedit.path.getCtrlLine = function(id) {
  178. var ctrlLine = svgedit.utilities.getElem('ctrlLine_'+id);
  179. if (ctrlLine) {return ctrlLine;}
  180. ctrlLine = document.createElementNS(NS.SVG, 'line');
  181. svgedit.utilities.assignAttributes(ctrlLine, {
  182. 'id': 'ctrlLine_'+id,
  183. 'stroke': '#555',
  184. 'stroke-width': 1,
  185. 'style': 'pointer-events:none'
  186. });
  187. svgedit.path.getGripContainer().appendChild(ctrlLine);
  188. return ctrlLine;
  189. };
  190. svgedit.path.getPointGrip = function(seg, update) {
  191. var index = seg.index;
  192. var pointGrip = svgedit.path.addPointGrip(index);
  193. if (update) {
  194. var pt = svgedit.path.getGripPt(seg);
  195. svgedit.utilities.assignAttributes(pointGrip, {
  196. 'cx': pt.x,
  197. 'cy': pt.y,
  198. 'display': 'inline'
  199. });
  200. }
  201. return pointGrip;
  202. };
  203. svgedit.path.getControlPoints = function(seg) {
  204. var item = seg.item;
  205. var index = seg.index;
  206. if (!('x1' in item) || !('x2' in item)) {return null;}
  207. var cpt = {};
  208. var pointGripContainer = svgedit.path.getGripContainer();
  209. // Note that this is intentionally not seg.prev.item
  210. var prev = svgedit.path.path.segs[index-1].item;
  211. var seg_items = [prev, item];
  212. var i;
  213. for (i = 1; i < 3; i++) {
  214. var id = index + 'c' + i;
  215. var ctrlLine = cpt['c' + i + '_line'] = svgedit.path.getCtrlLine(id);
  216. var pt = svgedit.path.getGripPt(seg, {x:item['x' + i], y:item['y' + i]});
  217. var gpt = svgedit.path.getGripPt(seg, {x:seg_items[i-1].x, y:seg_items[i-1].y});
  218. svgedit.utilities.assignAttributes(ctrlLine, {
  219. 'x1': pt.x,
  220. 'y1': pt.y,
  221. 'x2': gpt.x,
  222. 'y2': gpt.y,
  223. 'display': 'inline'
  224. });
  225. cpt['c' + i + '_line'] = ctrlLine;
  226. // create it
  227. var pointGrip = cpt['c' + i] = svgedit.path.addCtrlGrip(id);
  228. svgedit.utilities.assignAttributes(pointGrip, {
  229. 'cx': pt.x,
  230. 'cy': pt.y,
  231. 'display': 'inline'
  232. });
  233. cpt['c' + i] = pointGrip;
  234. }
  235. return cpt;
  236. };
  237. // This replaces the segment at the given index. Type is given as number.
  238. svgedit.path.replacePathSeg = function(type, index, pts, elem) {
  239. var path = elem || svgedit.path.path.elem;
  240. var func = 'createSVGPathSeg' + pathFuncs[type];
  241. var seg = path[func].apply(path, pts);
  242. if (svgedit.browser.supportsPathReplaceItem()) {
  243. path.pathSegList.replaceItem(seg, index);
  244. } else {
  245. var segList = path.pathSegList;
  246. var len = segList.numberOfItems;
  247. var arr = [];
  248. var i;
  249. for (i = 0; i < len; i++) {
  250. var cur_seg = segList.getItem(i);
  251. arr.push(cur_seg);
  252. }
  253. segList.clear();
  254. for (i = 0; i < len; i++) {
  255. if (i == index) {
  256. segList.appendItem(seg);
  257. } else {
  258. segList.appendItem(arr[i]);
  259. }
  260. }
  261. }
  262. };
  263. svgedit.path.getSegSelector = function(seg, update) {
  264. var index = seg.index;
  265. var segLine = svgedit.utilities.getElem('segline_' + index);
  266. if (!segLine) {
  267. var pointGripContainer = svgedit.path.getGripContainer();
  268. // create segline
  269. segLine = document.createElementNS(NS.SVG, 'path');
  270. svgedit.utilities.assignAttributes(segLine, {
  271. 'id': 'segline_' + index,
  272. 'display': 'none',
  273. 'fill': 'none',
  274. 'stroke': '#0FF',
  275. 'stroke-width': 2,
  276. 'style':'pointer-events:none',
  277. 'd': 'M0,0 0,0'
  278. });
  279. pointGripContainer.appendChild(segLine);
  280. }
  281. if (update) {
  282. var prev = seg.prev;
  283. if (!prev) {
  284. segLine.setAttribute('display', 'none');
  285. return segLine;
  286. }
  287. var pt = svgedit.path.getGripPt(prev);
  288. // Set start point
  289. svgedit.path.replacePathSeg(2, 0, [pt.x, pt.y], segLine);
  290. var pts = svgedit.path.ptObjToArr(seg.type, seg.item, true);
  291. var i;
  292. for (i = 0; i < pts.length; i += 2) {
  293. pt = svgedit.path.getGripPt(seg, {x:pts[i], y:pts[i+1]});
  294. pts[i] = pt.x;
  295. pts[i+1] = pt.y;
  296. }
  297. svgedit.path.replacePathSeg(seg.type, 1, pts, segLine);
  298. }
  299. return segLine;
  300. };
  301. // Function: smoothControlPoints
  302. // Takes three points and creates a smoother line based on them
  303. //
  304. // Parameters:
  305. // ct1 - Object with x and y values (first control point)
  306. // ct2 - Object with x and y values (second control point)
  307. // pt - Object with x and y values (third point)
  308. //
  309. // Returns:
  310. // Array of two "smoothed" point objects
  311. svgedit.path.smoothControlPoints = function(ct1, ct2, pt) {
  312. // each point must not be the origin
  313. var x1 = ct1.x - pt.x,
  314. y1 = ct1.y - pt.y,
  315. x2 = ct2.x - pt.x,
  316. y2 = ct2.y - pt.y;
  317. if ( (x1 != 0 || y1 != 0) && (x2 != 0 || y2 != 0) ) {
  318. var anglea = Math.atan2(y1, x1),
  319. angleb = Math.atan2(y2, x2),
  320. r1 = Math.sqrt(x1*x1+y1*y1),
  321. r2 = Math.sqrt(x2*x2+y2*y2),
  322. nct1 = editorContext_.getSVGRoot().createSVGPoint(),
  323. nct2 = editorContext_.getSVGRoot().createSVGPoint();
  324. if (anglea < 0) { anglea += 2*Math.PI; }
  325. if (angleb < 0) { angleb += 2*Math.PI; }
  326. var angleBetween = Math.abs(anglea - angleb),
  327. angleDiff = Math.abs(Math.PI - angleBetween)/2;
  328. var new_anglea, new_angleb;
  329. if (anglea - angleb > 0) {
  330. new_anglea = angleBetween < Math.PI ? (anglea + angleDiff) : (anglea - angleDiff);
  331. new_angleb = angleBetween < Math.PI ? (angleb - angleDiff) : (angleb + angleDiff);
  332. }
  333. else {
  334. new_anglea = angleBetween < Math.PI ? (anglea - angleDiff) : (anglea + angleDiff);
  335. new_angleb = angleBetween < Math.PI ? (angleb + angleDiff) : (angleb - angleDiff);
  336. }
  337. // rotate the points
  338. nct1.x = r1 * Math.cos(new_anglea) + pt.x;
  339. nct1.y = r1 * Math.sin(new_anglea) + pt.y;
  340. nct2.x = r2 * Math.cos(new_angleb) + pt.x;
  341. nct2.y = r2 * Math.sin(new_angleb) + pt.y;
  342. return [nct1, nct2];
  343. }
  344. return undefined;
  345. };
  346. svgedit.path.Segment = function(index, item) {
  347. this.selected = false;
  348. this.index = index;
  349. this.item = item;
  350. this.type = item.pathSegType;
  351. this.ctrlpts = [];
  352. this.ptgrip = null;
  353. this.segsel = null;
  354. };
  355. svgedit.path.Segment.prototype.showCtrlPts = function(y) {
  356. var i;
  357. for (i in this.ctrlpts) {
  358. if (this.ctrlpts.hasOwnProperty(i)) {
  359. this.ctrlpts[i].setAttribute('display', y ? 'inline' : 'none');
  360. }
  361. }
  362. };
  363. svgedit.path.Segment.prototype.selectCtrls = function(y) {
  364. $('#ctrlpointgrip_' + this.index + 'c1, #ctrlpointgrip_' + this.index + 'c2').
  365. attr('fill', y ? '#0FF' : '#EEE');
  366. };
  367. svgedit.path.Segment.prototype.show = function(y) {
  368. if (this.ptgrip) {
  369. this.ptgrip.setAttribute('display', y ? 'inline' : 'none');
  370. this.segsel.setAttribute('display', y ? 'inline' : 'none');
  371. // Show/hide all control points if available
  372. this.showCtrlPts(y);
  373. }
  374. };
  375. svgedit.path.Segment.prototype.select = function(y) {
  376. if (this.ptgrip) {
  377. this.ptgrip.setAttribute('stroke', y ? '#0FF' : '#00F');
  378. this.segsel.setAttribute('display', y ? 'inline' : 'none');
  379. if (this.ctrlpts) {
  380. this.selectCtrls(y);
  381. }
  382. this.selected = y;
  383. }
  384. };
  385. svgedit.path.Segment.prototype.addGrip = function() {
  386. this.ptgrip = svgedit.path.getPointGrip(this, true);
  387. this.ctrlpts = svgedit.path.getControlPoints(this, true);
  388. this.segsel = svgedit.path.getSegSelector(this, true);
  389. };
  390. svgedit.path.Segment.prototype.update = function(full) {
  391. if (this.ptgrip) {
  392. var pt = svgedit.path.getGripPt(this);
  393. svgedit.utilities.assignAttributes(this.ptgrip, {
  394. 'cx': pt.x,
  395. 'cy': pt.y
  396. });
  397. svgedit.path.getSegSelector(this, true);
  398. if (this.ctrlpts) {
  399. if (full) {
  400. this.item = svgedit.path.path.elem.pathSegList.getItem(this.index);
  401. this.type = this.item.pathSegType;
  402. }
  403. svgedit.path.getControlPoints(this);
  404. }
  405. // this.segsel.setAttribute('display', y?'inline':'none');
  406. }
  407. };
  408. svgedit.path.Segment.prototype.move = function(dx, dy) {
  409. var cur_pts, item = this.item;
  410. // fix for path tool dom breakage, amending item does bad things now, so we take a copy and use that. Can probably improve to just take a shallow copy of object
  411. var cloneItem = $.extend({}, item);
  412. if (this.ctrlpts) {
  413. cur_pts = [cloneItem.x += dx, cloneItem.y += dy,
  414. cloneItem.x1, cloneItem.y1, cloneItem.x2 += dx, cloneItem.y2 += dy];
  415. } else {
  416. cur_pts = [cloneItem.x += dx, cloneItem.y += dy];
  417. }
  418. //fix
  419. svgedit.path.replacePathSeg(this.type, this.index, cur_pts);
  420. if (this.next && this.next.ctrlpts) {
  421. var next = this.next.item;
  422. var next_pts = [next.x, next.y,
  423. next.x1 += dx, next.y1 += dy, next.x2, next.y2];
  424. svgedit.path.replacePathSeg(this.next.type, this.next.index, next_pts);
  425. }
  426. if (this.mate) {
  427. // The last point of a closed subpath has a 'mate',
  428. // which is the 'M' segment of the subpath
  429. item = this.mate.item;
  430. var pts = [item.x += dx, item.y += dy];
  431. svgedit.path.replacePathSeg(this.mate.type, this.mate.index, pts);
  432. // Has no grip, so does not need 'updating'?
  433. }
  434. this.update(true);
  435. if (this.next) {this.next.update(true);}
  436. };
  437. svgedit.path.Segment.prototype.setLinked = function(num) {
  438. var seg, anum, pt;
  439. if (num == 2) {
  440. anum = 1;
  441. seg = this.next;
  442. if (!seg) {return;}
  443. pt = this.item;
  444. } else {
  445. anum = 2;
  446. seg = this.prev;
  447. if (!seg) {return;}
  448. pt = seg.item;
  449. }
  450. var item = seg.item;
  451. // fix for path tool dom breakage, amending item does bad things now, so we take a copy and use that. Can probably improve to just take a shallow copy of object
  452. var cloneItem = $.extend({}, item);
  453. cloneItem['x' + anum ] = pt.x + (pt.x - this.item['x' + num]);
  454. cloneItem['y' + anum ] = pt.y + (pt.y - this.item['y' + num]);
  455. var pts = [cloneItem.x, cloneItem.y,
  456. cloneItem.x1, cloneItem.y1,
  457. cloneItem.x2, cloneItem.y2];
  458. //end fix
  459. svgedit.path.replacePathSeg(seg.type, seg.index, pts);
  460. seg.update(true);
  461. };
  462. svgedit.path.Segment.prototype.moveCtrl = function(num, dx, dy) {
  463. var item = this.item;
  464. // fix for path tool dom breakage, amending item does bad things now, so we take a copy and use that. Can probably improve to just take a shallow copy of object
  465. var cloneItem = $.extend({}, item);
  466. cloneItem['x' + num] += dx;
  467. cloneItem['y' + num] += dy;
  468. var pts = [cloneItem.x,cloneItem.y,
  469. cloneItem.x1,cloneItem.y1, cloneItem.x2,cloneItem.y2];
  470. // end fix
  471. svgedit.path.replacePathSeg(this.type, this.index, pts);
  472. this.update(true);
  473. };
  474. svgedit.path.Segment.prototype.setType = function(new_type, pts) {
  475. svgedit.path.replacePathSeg(new_type, this.index, pts);
  476. this.type = new_type;
  477. this.item = svgedit.path.path.elem.pathSegList.getItem(this.index);
  478. this.showCtrlPts(new_type === 6);
  479. this.ctrlpts = svgedit.path.getControlPoints(this);
  480. this.update(true);
  481. };
  482. svgedit.path.Path = function(elem) {
  483. if (!elem || elem.tagName !== 'path') {
  484. throw 'svgedit.path.Path constructed without a <path> element';
  485. }
  486. this.elem = elem;
  487. this.segs = [];
  488. this.selected_pts = [];
  489. svgedit.path.path = this;
  490. this.init();
  491. };
  492. // Reset path data
  493. svgedit.path.Path.prototype.init = function() {
  494. // Hide all grips, etc
  495. //fixed, needed to work on all found elements, not just first
  496. $(svgedit.path.getGripContainer()).find('*').each( function() { $(this).attr('display', 'none') });
  497. var segList = this.elem.pathSegList;
  498. var len = segList.numberOfItems;
  499. this.segs = [];
  500. this.selected_pts = [];
  501. this.first_seg = null;
  502. // Set up segs array
  503. var i;
  504. for (i = 0; i < len; i++) {
  505. var item = segList.getItem(i);
  506. var segment = new svgedit.path.Segment(i, item);
  507. segment.path = this;
  508. this.segs.push(segment);
  509. }
  510. var segs = this.segs;
  511. var start_i = null;
  512. for (i = 0; i < len; i++) {
  513. var seg = segs[i];
  514. var next_seg = (i+1) >= len ? null : segs[i+1];
  515. var prev_seg = (i-1) < 0 ? null : segs[i-1];
  516. var start_seg;
  517. if (seg.type === 2) {
  518. if (prev_seg && prev_seg.type !== 1) {
  519. // New sub-path, last one is open,
  520. // so add a grip to last sub-path's first point
  521. start_seg = segs[start_i];
  522. start_seg.next = segs[start_i+1];
  523. start_seg.next.prev = start_seg;
  524. start_seg.addGrip();
  525. }
  526. // Remember that this is a starter seg
  527. start_i = i;
  528. } else if (next_seg && next_seg.type === 1) {
  529. // This is the last real segment of a closed sub-path
  530. // Next is first seg after "M"
  531. seg.next = segs[start_i+1];
  532. // First seg after "M"'s prev is this
  533. seg.next.prev = seg;
  534. seg.mate = segs[start_i];
  535. seg.addGrip();
  536. if (this.first_seg == null) {
  537. this.first_seg = seg;
  538. }
  539. } else if (!next_seg) {
  540. if (seg.type !== 1) {
  541. // Last seg, doesn't close so add a grip
  542. // to last sub-path's first point
  543. start_seg = segs[start_i];
  544. start_seg.next = segs[start_i+1];
  545. start_seg.next.prev = start_seg;
  546. start_seg.addGrip();
  547. seg.addGrip();
  548. if (!this.first_seg) {
  549. // Open path, so set first as real first and add grip
  550. this.first_seg = segs[start_i];
  551. }
  552. }
  553. } else if (seg.type !== 1){
  554. // Regular segment, so add grip and its "next"
  555. seg.addGrip();
  556. // Don't set its "next" if it's an "M"
  557. if (next_seg && next_seg.type !== 2) {
  558. seg.next = next_seg;
  559. seg.next.prev = seg;
  560. }
  561. }
  562. }
  563. return this;
  564. };
  565. svgedit.path.Path.prototype.eachSeg = function(fn) {
  566. var i;
  567. var len = this.segs.length;
  568. for (i = 0; i < len; i++) {
  569. var ret = fn.call(this.segs[i], i);
  570. if (ret === false) {break;}
  571. }
  572. };
  573. svgedit.path.Path.prototype.addSeg = function(index) {
  574. // Adds a new segment
  575. var seg = this.segs[index];
  576. if (!seg.prev) {return;}
  577. var prev = seg.prev;
  578. var newseg, new_x, new_y;
  579. switch(seg.item.pathSegType) {
  580. case 4:
  581. new_x = (seg.item.x + prev.item.x) / 2;
  582. new_y = (seg.item.y + prev.item.y) / 2;
  583. newseg = this.elem.createSVGPathSegLinetoAbs(new_x, new_y);
  584. break;
  585. case 6: //make it a curved segment to preserve the shape (WRS)
  586. // http://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm#Geometric_interpretation
  587. var p0_x = (prev.item.x + seg.item.x1)/2;
  588. var p1_x = (seg.item.x1 + seg.item.x2)/2;
  589. var p2_x = (seg.item.x2 + seg.item.x)/2;
  590. var p01_x = (p0_x + p1_x)/2;
  591. var p12_x = (p1_x + p2_x)/2;
  592. new_x = (p01_x + p12_x)/2;
  593. var p0_y = (prev.item.y + seg.item.y1)/2;
  594. var p1_y = (seg.item.y1 + seg.item.y2)/2;
  595. var p2_y = (seg.item.y2 + seg.item.y)/2;
  596. var p01_y = (p0_y + p1_y)/2;
  597. var p12_y = (p1_y + p2_y)/2;
  598. new_y = (p01_y + p12_y)/2;
  599. newseg = this.elem.createSVGPathSegCurvetoCubicAbs(new_x, new_y, p0_x, p0_y, p01_x, p01_y);
  600. var pts = [seg.item.x, seg.item.y, p12_x, p12_y, p2_x, p2_y];
  601. svgedit.path.replacePathSeg(seg.type, index, pts);
  602. break;
  603. }
  604. svgedit.path.insertItemBefore(this.elem, newseg, index);
  605. };
  606. svgedit.path.Path.prototype.deleteSeg = function(index) {
  607. var seg = this.segs[index];
  608. var list = this.elem.pathSegList;
  609. seg.show(false);
  610. var next = seg.next;
  611. var pt;
  612. if (seg.mate) {
  613. // Make the next point be the "M" point
  614. pt = [next.item.x, next.item.y];
  615. svgedit.path.replacePathSeg(2, next.index, pt);
  616. // Reposition last node
  617. svgedit.path.replacePathSeg(4, seg.index, pt);
  618. list.removeItem(seg.mate.index);
  619. } else if (!seg.prev) {
  620. // First node of open path, make next point the M
  621. var item = seg.item;
  622. pt = [next.item.x, next.item.y];
  623. svgedit.path.replacePathSeg(2, seg.next.index, pt);
  624. list.removeItem(index);
  625. } else {
  626. list.removeItem(index);
  627. }
  628. };
  629. svgedit.path.Path.prototype.subpathIsClosed = function(index) {
  630. var closed = false;
  631. // Check if subpath is already open
  632. svgedit.path.path.eachSeg(function(i) {
  633. if (i <= index) {return true;}
  634. if (this.type === 2) {
  635. // Found M first, so open
  636. return false;
  637. }
  638. if (this.type === 1) {
  639. // Found Z first, so closed
  640. closed = true;
  641. return false;
  642. }
  643. });
  644. return closed;
  645. };
  646. svgedit.path.Path.prototype.removePtFromSelection = function(index) {
  647. var pos = this.selected_pts.indexOf(index);
  648. if (pos == -1) {
  649. return;
  650. }
  651. this.segs[index].select(false);
  652. this.selected_pts.splice(pos, 1);
  653. };
  654. svgedit.path.Path.prototype.clearSelection = function() {
  655. this.eachSeg(function() {
  656. // 'this' is the segment here
  657. this.select(false);
  658. });
  659. this.selected_pts = [];
  660. };
  661. svgedit.path.Path.prototype.storeD = function() {
  662. this.last_d = this.elem.getAttribute('d');
  663. };
  664. svgedit.path.Path.prototype.show = function(y) {
  665. // Shows this path's segment grips
  666. this.eachSeg(function() {
  667. // 'this' is the segment here
  668. this.show(y);
  669. });
  670. if (y) {
  671. this.selectPt(this.first_seg.index);
  672. }
  673. return this;
  674. };
  675. // Move selected points
  676. svgedit.path.Path.prototype.movePts = function(d_x, d_y) {
  677. var i = this.selected_pts.length;
  678. while(i--) {
  679. var seg = this.segs[this.selected_pts[i]];
  680. seg.move(d_x, d_y);
  681. }
  682. };
  683. svgedit.path.Path.prototype.moveCtrl = function(d_x, d_y) {
  684. var seg = this.segs[this.selected_pts[0]];
  685. seg.moveCtrl(this.dragctrl, d_x, d_y);
  686. if (link_control_pts) {
  687. seg.setLinked(this.dragctrl);
  688. }
  689. };
  690. svgedit.path.Path.prototype.setSegType = function(new_type) {
  691. this.storeD();
  692. var i = this.selected_pts.length;
  693. var text;
  694. while(i--) {
  695. var sel_pt = this.selected_pts[i];
  696. // Selected seg
  697. var cur = this.segs[sel_pt];
  698. var prev = cur.prev;
  699. if (!prev) {continue;}
  700. if (!new_type) { // double-click, so just toggle
  701. text = 'Toggle Path Segment Type';
  702. // Toggle segment to curve/straight line
  703. var old_type = cur.type;
  704. new_type = (old_type == 6) ? 4 : 6;
  705. }
  706. new_type = Number(new_type);
  707. var cur_x = cur.item.x;
  708. var cur_y = cur.item.y;
  709. var prev_x = prev.item.x;
  710. var prev_y = prev.item.y;
  711. var points;
  712. switch ( new_type ) {
  713. case 6:
  714. if (cur.olditem) {
  715. var old = cur.olditem;
  716. points = [cur_x, cur_y, old.x1, old.y1, old.x2, old.y2];
  717. } else {
  718. var diff_x = cur_x - prev_x;
  719. var diff_y = cur_y - prev_y;
  720. // get control points from straight line segment
  721. /*
  722. var ct1_x = (prev_x + (diff_y/2));
  723. var ct1_y = (prev_y - (diff_x/2));
  724. var ct2_x = (cur_x + (diff_y/2));
  725. var ct2_y = (cur_y - (diff_x/2));
  726. */
  727. //create control points on the line to preserve the shape (WRS)
  728. var ct1_x = (prev_x + (diff_x/3));
  729. var ct1_y = (prev_y + (diff_y/3));
  730. var ct2_x = (cur_x - (diff_x/3));
  731. var ct2_y = (cur_y - (diff_y/3));
  732. points = [cur_x, cur_y, ct1_x, ct1_y, ct2_x, ct2_y];
  733. }
  734. break;
  735. case 4:
  736. points = [cur_x, cur_y];
  737. // Store original prevve segment nums
  738. cur.olditem = cur.item;
  739. break;
  740. }
  741. cur.setType(new_type, points);
  742. }
  743. svgedit.path.path.endChanges(text);
  744. };
  745. svgedit.path.Path.prototype.selectPt = function(pt, ctrl_num) {
  746. this.clearSelection();
  747. if (pt == null) {
  748. this.eachSeg(function(i) {
  749. // 'this' is the segment here.
  750. if (this.prev) {
  751. pt = i;
  752. }
  753. });
  754. }
  755. this.addPtsToSelection(pt);
  756. if (ctrl_num) {
  757. this.dragctrl = ctrl_num;
  758. if (link_control_pts) {
  759. this.segs[pt].setLinked(ctrl_num);
  760. }
  761. }
  762. };
  763. // Update position of all points
  764. svgedit.path.Path.prototype.update = function() {
  765. var elem = this.elem;
  766. if (svgedit.utilities.getRotationAngle(elem)) {
  767. this.matrix = svgedit.math.getMatrix(elem);
  768. this.imatrix = this.matrix.inverse();
  769. } else {
  770. this.matrix = null;
  771. this.imatrix = null;
  772. }
  773. this.eachSeg(function(i) {
  774. this.item = elem.pathSegList.getItem(i);
  775. this.update();
  776. });
  777. return this;
  778. };
  779. svgedit.path.getPath_ = function(elem) {
  780. var p = pathData[elem.id];
  781. if (!p) {
  782. p = pathData[elem.id] = new svgedit.path.Path(elem);
  783. }
  784. return p;
  785. };
  786. svgedit.path.removePath_ = function(id) {
  787. if (id in pathData) {delete pathData[id];}
  788. };
  789. var newcx, newcy, oldcx, oldcy, angle;
  790. var getRotVals = function(x, y) {
  791. var dx = x - oldcx;
  792. var dy = y - oldcy;
  793. // rotate the point around the old center
  794. var r = Math.sqrt(dx*dx + dy*dy);
  795. var theta = Math.atan2(dy, dx) + angle;
  796. dx = r * Math.cos(theta) + oldcx;
  797. dy = r * Math.sin(theta) + oldcy;
  798. // dx,dy should now hold the actual coordinates of each
  799. // point after being rotated
  800. // now we want to rotate them around the new center in the reverse direction
  801. dx -= newcx;
  802. dy -= newcy;
  803. r = Math.sqrt(dx*dx + dy*dy);
  804. theta = Math.atan2(dy, dx) - angle;
  805. return {'x': r * Math.cos(theta) + newcx,
  806. 'y': r * Math.sin(theta) + newcy};
  807. };
  808. // If the path was rotated, we must now pay the piper:
  809. // Every path point must be rotated into the rotated coordinate system of
  810. // its old center, then determine the new center, then rotate it back
  811. // This is because we want the path to remember its rotation
  812. // TODO: This is still using ye olde transform methods, can probably
  813. // be optimized or even taken care of by recalculateDimensions
  814. svgedit.path.recalcRotatedPath = function() {
  815. var current_path = svgedit.path.path.elem;
  816. angle = svgedit.utilities.getRotationAngle(current_path, true);
  817. if (!angle) {return;}
  818. // selectedBBoxes[0] = svgedit.path.path.oldbbox;
  819. var box = svgedit.utilities.getBBox(current_path),
  820. oldbox = svgedit.path.path.oldbbox; //selectedBBoxes[0],
  821. oldcx = oldbox.x + oldbox.width/2;
  822. oldcy = oldbox.y + oldbox.height/2;
  823. newcx = box.x + box.width/2;
  824. newcy = box.y + box.height/2;
  825. // un-rotate the new center to the proper position
  826. var dx = newcx - oldcx,
  827. dy = newcy - oldcy,
  828. r = Math.sqrt(dx*dx + dy*dy),
  829. theta = Math.atan2(dy, dx) + angle;
  830. newcx = r * Math.cos(theta) + oldcx;
  831. newcy = r * Math.sin(theta) + oldcy;
  832. var list = current_path.pathSegList,
  833. i = list.numberOfItems;
  834. while (i) {
  835. i -= 1;
  836. var seg = list.getItem(i),
  837. type = seg.pathSegType;
  838. if (type == 1) {continue;}
  839. var rvals = getRotVals(seg.x, seg.y),
  840. points = [rvals.x, rvals.y];
  841. if (seg.x1 != null && seg.x2 != null) {
  842. var c_vals1 = getRotVals(seg.x1, seg.y1);
  843. var c_vals2 = getRotVals(seg.x2, seg.y2);
  844. points.splice(points.length, 0, c_vals1.x , c_vals1.y, c_vals2.x, c_vals2.y);
  845. }
  846. svgedit.path.replacePathSeg(type, i, points);
  847. } // loop for each point
  848. box = svgedit.utilities.getBBox(current_path);
  849. // selectedBBoxes[0].x = box.x; selectedBBoxes[0].y = box.y;
  850. // selectedBBoxes[0].width = box.width; selectedBBoxes[0].height = box.height;
  851. // now we must set the new transform to be rotated around the new center
  852. var R_nc = svgroot.createSVGTransform(),
  853. tlist = svgedit.transformlist.getTransformList(current_path);
  854. R_nc.setRotate((angle * 180.0 / Math.PI), newcx, newcy);
  855. tlist.replaceItem(R_nc,0);
  856. };
  857. // ====================================
  858. // Public API starts here
  859. svgedit.path.clearData = function() {
  860. pathData = {};
  861. };
  862. }());