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