SVG Editor 분석-3 Path 포인트 정의하기
SVG Editor 에서는 SVG 에디터 답게 SVG의 패스포인트를 직접 수정할 수 있다. path 의 경우 객체에 마우스를 두번 클릭하면 자동적으로 path 포인트를 따낸다. 하지만 rect나 polyline , ellipse같은 경우 상단 메뉴에서 convert to path 로 변경하면 type 을 path로 변경하면서 중요 포인트 (각) 를 path 포인트로 짚어낸다.
태그 자체를 path 로 변경하고 d 를 attribute 로 추가하여 d의 path 포인트를 따내니 정말 훌륭하다.
새삼 이세상에는 진짜 외계인을 옆에 두고 고문하면서 개발하는 개발자들이 많다는걸 느낀다.
각설하고, path 로 변경되면서 각 path 포인트를 직접 수정할 수 있도록 하는데 그 과정의 로직을 정리해본다.
convert to path 버튼을 눌러 path로 변경된 이후 다시 클릭하면 mousedown 이벤트가 발생한다.
var mouseUp = function(evt) { if (evt.button === 2) {return;} var tempJustSelected = justSelected; justSelected = null; if (!started) {return;} var pt = svgedit.math.transformPoint(evt.pageX, evt.pageY, root_sctm), mouse_x = pt.x * current_zoom, mouse_y = pt.y * current_zoom, x = mouse_x / current_zoom, y = mouse_y / current_zoom, element = svgedit.utilities.getElem(getId()), keep = false; var real_x = x; var real_y = y; // TODO: Make true when in multi-unit mode var useUnit = false; // (curConfig.baseUnit !== 'px'); started = false; var attrs, t; switch (current_mode) { // intentionally fall-through to select here case "resize": case "multiselect": if (rubberBox != null) { rubberBox.setAttribute("display", "none"); curBBoxes = []; } current_mode = "select"; case "select": if (selectedElements[0] != null) { // if we only have one selected element if (selectedElements[1] == null) { // set our current stroke/fill properties to the element's var selected = selectedElements[0]; switch ( selected.tagName ) { case "g": case "use": case "image": case "foreignObject": break; default: cur_properties.fill = selected.getAttribute("fill"); cur_properties.fill_opacity = selected.getAttribute("fill-opacity"); cur_properties.stroke = selected.getAttribute("stroke"); cur_properties.stroke_opacity = selected.getAttribute("stroke-opacity"); cur_properties.stroke_width = selected.getAttribute("stroke-width"); cur_properties.stroke_dasharray = selected.getAttribute("stroke-dasharray"); cur_properties.stroke_linejoin = selected.getAttribute("stroke-linejoin"); cur_properties.stroke_linecap = selected.getAttribute("stroke-linecap"); } if (selected.tagName == "text") { cur_text.font_size = selected.getAttribute("font-size"); cur_text.font_family = selected.getAttribute("font-family"); } selectorManager.requestSelector(selected).showGrips(true); // This shouldn't be necessary as it was done on mouseDown... // call("selected", [selected]); } // always recalculate dimensions to strip off stray identity transforms recalculateAllSelectedDimensions(); // if it was being dragged/resized if (real_x != r_start_x || real_y != r_start_y) { var i, len = selectedElements.length; for (i = 0; i < len; ++i) { if (selectedElements[i] == null) {break;} if (!selectedElements[i].firstChild) { // Not needed for groups (incorrectly resizes elems), possibly not needed at all? selectorManager.requestSelector(selectedElements[i]).resize(); } } } // no change in position/size, so maybe we should move to pathedit else { t = evt.target; if (selectedElements[0].nodeName === "path" && selectedElements[1] == null) { // 아래 pathActions.select로 넘어간다. pathActions.select(selectedElements[0]); } // if it was a path // else, if it was selected and this is a shift-click, remove it from selection else if (evt.shiftKey) { if (tempJustSelected != t) { canvas.removeFromSelection([t]); } } } // no change in mouse position // Remove non-scaling stroke if (svgedit.browser.supportsNonScalingStroke()) { var elem = selectedElements[0]; if (elem) { elem.removeAttribute('style'); svgedit.utilities.walkTree(elem, function(elem) { elem.removeAttribute('style'); }); } } } return;
selectedElements 가 사용자가 미리 selete 해놓았던 path 객체가 된다.
해당 path 객체를 가지고 select 를 거친 후 toEditMode 로 target을 가지고 들어간다.
select: function(target) { if (current_path === target) { pathActions.toEditMode(target); current_mode = "pathedit"; } // going into pathedit mode else { current_path = target; } },
그다음 current_path 가 target 즉 최근에 선택한 path 가 가져온 target 과 맞는 요소인지 검사한다. 즉, 미리 선택된 요소가 path 이고 그 path 요소가 지금 선택해서 가지고 들어온 target과 동일한 요소라는게 판명되면 pathActions.toEditMode로 간다.
toEditMode: function(element) { svgedit.path.path = svgedit.path.getPath_(element); current_mode = "pathedit"; clearSelection(); svgedit.path.path.show(true).update(); svgedit.path.path.oldbbox = svgedit.utilities.getBBox(svgedit.path.path.elem); subpath = false; },
처음 path값을 가져오므로 path를 init(초기화) 해야한다. getPath_ 메소드로 이동하는데 element 파라미터는 아직 target 이다.
path.js 에서 path를 init 한다.
파라미터 elem은 넘겨받은 target이다
svgedit.path.getPath_ = function(elem) { var p = pathData[elem.id]; if (!p) { p = pathData[elem.id] = new svgedit.path.Path(elem); } return p; };
svgedit.path.Path = function(elem) { if (!elem || elem.tagName !== 'path') { throw 'svgedit.path.Path constructed without aelement'; } this.elem = elem; this.segs = []; this.selected_pts = []; svgedit.path.path = this; this.init(); }; // Reset path data svgedit.path.Path.prototype.init = function() { // Hide all grips, etc $(svgedit.path.getGripContainer()).find('*').attr('display', 'none'); var segList = this.elem.pathSegList; var len = segList.numberOfItems; this.segs = []; this.selected_pts = []; this.first_seg = null; // Set up segs array var i; for (i = 0; i < len; i++) { var item = segList.getItem(i); var segment = new svgedit.path.Segment(i, item); segment.path = this; this.segs.push(segment); } var segs = this.segs; var start_i = null; for (i = 0; i < len; i++) { var seg = segs[i]; var next_seg = (i+1) >= len ? null : segs[i+1]; var prev_seg = (i-1) < 0 ? null : segs[i-1]; var start_seg; if (seg.type === 2) { if (prev_seg && prev_seg.type !== 1) { // New sub-path, last one is open, // so add a grip to last sub-path's first point start_seg = segs[start_i]; start_seg.next = segs[start_i+1]; start_seg.next.prev = start_seg; start_seg.addGrip(); } // Remember that this is a starter seg start_i = i; } else if (next_seg && next_seg.type === 1) { // This is the last real segment of a closed sub-path // Next is first seg after "M" seg.next = segs[start_i+1]; // First seg after "M"'s prev is this seg.next.prev = seg; seg.mate = segs[start_i]; seg.addGrip(); if (this.first_seg == null) { this.first_seg = seg; } } else if (!next_seg) { if (seg.type !== 1) { // Last seg, doesn't close so add a grip // to last sub-path's first point start_seg = segs[start_i]; start_seg.next = segs[start_i+1]; start_seg.next.prev = start_seg; start_seg.addGrip(); seg.addGrip(); if (!this.first_seg) { // Open path, so set first as real first and add grip this.first_seg = segs[start_i]; } } } else if (seg.type !== 1){ // Regular segment, so add grip and its "next" seg.addGrip(); // Don't set its "next" if it's an "M" if (next_seg && next_seg.type !== 2) { seg.next = next_seg; seg.next.prev = seg; } } } return this; };
init 메소드를 거치면서 seg 를 정의한다. seg 는 path point 이다. seglist 는 path point를 모아놓은 list가 된다. 하단 for 문의 if else if 문을 거치면서 각 path point 를 직접 객체에 포인트를 그리게 된다. segtype 과 next segtype 을 구분하면서 객체의 path point 사이의 line 색상 을 추가하고 마지막으로 초기 default 로 지정될 path point 사이의 path line 을 제외한 나머지 line들을 display none 상태로 처리하여 마무리한다. clearSelection 은 selection point 진한 파란색 포인트를 안보이는 상태로 바꾼다.
addGrip() 부분
잡는 부분 즉, pathpoint를 잡아서 끌어당길 수 있는 포인트를 정의한다.
getPointGrip 와 getControlPoints 는 각각 포인트를 정의한다.
getSegSelector 는 라인을 지정 정의한다. (segLine)
HTML DOM 구조에서는 g태그의 id는 pathpointgrip_container 이다.svgedit.path.Segment.prototype.addGrip = function() { this.ptgrip = svgedit.path.getPointGrip(this, true); this.ctrlpts = svgedit.path.getControlPoints(this, true); this.segsel = svgedit.path.getSegSelector(this, true); };
svgedit.path.getPointGrip = function(seg, update) { var index = seg.index; var pointGrip = svgedit.path.addPointGrip(index); if (update) { var pt = svgedit.path.getGripPt(seg); svgedit.utilities.assignAttributes(pointGrip, { 'cx': pt.x, 'cy': pt.y, 'display': 'inline' }); } return pointGrip; };
svgedit.path.getControlPoints = function(seg) { var item = seg.item; var index = seg.index; if (!('x1' in item) || !('x2' in item)) {return null;} var cpt = {}; var pointGripContainer = svgedit.path.getGripContainer(); // Note that this is intentionally not seg.prev.item var prev = svgedit.path.path.segs[index-1].item; var seg_items = [prev, item]; var i; for (i = 1; i < 3; i++) { var id = index + 'c' + i; var ctrlLine = cpt['c' + i + '_line'] = svgedit.path.getCtrlLine(id); var pt = svgedit.path.getGripPt(seg, {x:item['x' + i], y:item['y' + i]}); var gpt = svgedit.path.getGripPt(seg, {x:seg_items[i-1].x, y:seg_items[i-1].y}); svgedit.utilities.assignAttributes(ctrlLine, { 'x1': pt.x, 'y1': pt.y, 'x2': gpt.x, 'y2': gpt.y, 'display': 'inline' }); cpt['c' + i + '_line'] = ctrlLine; // create it var pointGrip = cpt['c' + i] = svgedit.path.addCtrlGrip(id); svgedit.utilities.assignAttributes(pointGrip, { 'cx': pt.x, 'cy': pt.y, 'display': 'inline' }); cpt['c' + i] = pointGrip; } return cpt; };
svgedit.path.getSegSelector = function(seg, update) { var index = seg.index; var segLine = svgedit.utilities.getElem('segline_' + index); if (!segLine) { var pointGripContainer = svgedit.path.getGripContainer(); // create segline segLine = document.createElementNS(NS.SVG, 'path'); svgedit.utilities.assignAttributes(segLine, { 'id': 'segline_' + index, 'display': 'none', 'fill': 'none', 'stroke': '#0FF', 'stroke-width': 2, 'style':'pointer-events:none', 'd': 'M0,0 0,0' }); pointGripContainer.appendChild(segLine); } if (update) { var prev = seg.prev; if (!prev) { segLine.setAttribute('display', 'none'); return segLine; } var pt = svgedit.path.getGripPt(prev); // Set start point svgedit.path.replacePathSeg(2, 0, [pt.x, pt.y], segLine); var pts = svgedit.path.ptObjToArr(seg.type, seg.item, true); var i; for (i = 0; i < pts.length; i += 2) { pt = svgedit.path.getGripPt(seg, {x:pts[i], y:pts[i+1]}); pts[i] = pt.x; pts[i+1] = pt.y; } svgedit.path.replacePathSeg(seg.type, 1, pts, segLine); } return segLine; };