HTML5/SVG

SVG Editor 분석-3 Path 포인트 정의하기

Jundol 2015. 6. 15. 12:01



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 a  element';
	}

	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;
};