path.js 25 KB

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