Skip to content
GiveSeo edited this page Dec 26, 2025 · 20 revisions

Plotto Spine Code

0. 개요

본 문서는 로봇 드로잉 시스템 PlottoSpine.js 파일을 설명합니다. 이 문서는 Plotto 로봇 팔 코드의 구성 요소와 작동 방식을 설명합니다. 해당 코드는 2자유도(2-DOF) 로봇 팔을 제어하여 SVG로 정의된 경로를 따라 펜으로 그림을 그리는 기능을 제공합니다. 코드의 주요 상수, 클래스 속성, 함수, 그리고 전체적인 동작 흐름을 단계적으로 살펴보겠습니다.


1. 상수 정의

자세히 보기

코드 상단에는 로봇 팔의 물리적인 치수와 초기 상태를 나타내는 상수들이 정의되어 있습니다. 자세한 설명은 다음과 같습니다.

  • TOP_JOINT

    로봇 팔 상부 관절의 이미지 좌표입니다.

    const TOP_JOINT_X = 220;
    const TOP_JOINT_Y = 547;
  • UPPER_JOINT_BASE

    로봇 팔 상부 암(upper arm)이 기준(base)에 부착된 지점의 이미지 좌표입니다. 상부 암의 시작점이라고 볼 수 있습니다.

    const UPPER_JOINT_BASE_X = 747;
    const UPPER_JOINT_BASE_Y = 226;
  • UPPER_JOINT_ELBOW

    : 전완의 시작점(팔꿈치)의 이미지 좌표입니다. 이상적으로는 UPPER_JOINT_ELBOW와 동일한 위치여야 하지만, 측정 오차로 약간 차이가 있을 수 있습니다.

    const UPPER_JOINT_ELBOW_X = 195;
    const UPPER_JOINT_ELBOW_Y = 383;
  • FORE_JOINT_ELBOW

    전완의 시작점(팔꿈치)의 이미지 좌표입니다. 이상적으로는 UPPER_JOINT_ELBOW와 동일한 위치여야 하지만, 측정 오차로 약간 차이가 있을 수 있습니다.

    const FORE_JOINT_ELBOW_X = 195;
    const FORE_JOINT_ELBOW_Y = 385;
  • FORE_PEN

    로봇 팔 끝에 부착된 펜(Pen) 끝점의 이미지 좌표입니다. 전완의 끝점(펜이 위치한 지점)을 나타냅니다.

    const FORE_PEN_X = 778;
    const FORE_PEN_Y = 612;
  • imageScale

    이미지 좌표를 실제 로봇의 길이 단위로 변환하기 위한 스케일 계수입니다.

    예) const imageScale = 0.5; : 이미지 길이를 절반 스케일 후 실제 길이로 사용합니다.

이 값들은 로봇 팔의 기구학적 특성을 설정하고, 이미지 또는 설계도에서 측정된 좌표를 사용하여 로봇 팔의 길이와 각도를 계산하는 데 활용됩니다. 다음으로 주요 Plotto의 클래스를 살펴보겠습니다.


2. 클래스 정의

fkPenXY_deg(j1Deg, j2Deg)

이 함수는 관절 각도(j1Deg, j2Deg)를 받아 펜 끝 좌표(x3,y3)를 계산해 줍니다.

실행 동작

각도 변환 : 우선 도 단위인 j1Deg를 라디안에 맞게 변환하며, 부호를 뒤집습니다(로봇 좌표게(시계 -, 반시계 +)와 반대이기 때문). 이 변환한 수를 theta1에 저장합니다.

이후 theta1에 upperRestAngle을 더한 각도를 theta1_ft에 저장합니다. 이는 이미지 상의 로봇팔 각도를 더한 실제 관절 각도입니다.

j2Deg + this.JOINT2_OFFSET을 더한 실제 각도 값을 라디안으로 변환하고 부호를 뒤집습니다. 이를 theta2에 저장합니다.

팔꿈치 좌표(x2,y2) 계산 :
다음으로 좌표 계산을 진행합니다. 정기구학 공식과 동일합니다.

x2 = baseX + link1 * cos(theta1_fk);
y2 = baseY + link1 * sin(theta1_fk);

(baseX, baseY)는 로봇 joint1 모터의 위치, link1은 상부 암 길이입니다.

펜 끝 좌표(x3,y3) 계산 :

x3 = x2 + link2 * cos(theta1_fk + theta2);
y3 = y2 + link2 * sin(theta1_fk + theta2);

팔꿈치에서부터 theta2만큼 더 회전하므로, 팔꿈치에서의 절대 각도는 theta1_ft + theta2가 됩니다. 이 방향으로 link2만큼 더 나아간 지점이 펜 끝 좌표 입니다.

결과로 {x : x3, y : y3}를 반환합니다.

inverseKinematics2DOF(targetX, targetY, prevJ1Deg, prevJ2Deg)

이 함수는 목표 펜 좌표(targetX, targetY)를 주면, 해당 지점에 펜일 위치시키기 위한 두 관절 각도(joint1, joint2)를 계산합니다. 2DOF 평면 로봇 팔 표준 역기구학 공식을 구현하고 있으며, 기존 각도에 가까운 각도를 반환합니다.

실행 동작

목표 점 까지 벡터 계산 및 작업 공간 체크 : 목표점까지 상대 좌표로 거리(d)를 계산하고, | L1 - L2 | (최소 거리) ~ |L1 + L2| (최대 거리) 안에 있는지 검사합니다.

역 기구학 각도 계산 : 목표점이 도달 가능 하다면, 삼각법을 이용해 팔꿈치 관절 각도(theta2)를 구합니다.

let cos2 = (d * d - L1 * L1 - L2 * L2) / (2 * L1 * L2); // 삼각법으로 theta2 구하기

const theta2Abs = Math.acos(cos2); 
const theta2List = [theta2Abs, -theta2Abs]; // theta2 후보(theta2는 안으로 굽힌 팔, 바깥으로 굽힌 팔 두가지임.)

구한 팔꿈치 각도는 절대값으로 두가지 경우가 있는데, 팔꿈치가 안쪽으로 굽힌 경우와, 바깥쪽으로 굽힌 경우입니다.

각 후보에 대해 theta1 구하기 : 역기구학을 통해 어깨관절의 각도 theta1를 얻습니다.

k1 = L1 + L2 * cos(theta2_fk)
k2 = L2 * sin(theta2_fk)

이 값들은 역기구학 공식에서 중간 단계로, 두 링크에 의해 형성된 삼각형에서 어깨 관절과 목표점 사이 각도를 구하는 데 사용하는 변수입니다.

const theta1_fk = Math.atan2(dy, dx) - Math.atan2(k2, k1);

를 통해 각각 theta2에 대한 절대 각도 theta1_fk를 구하고, offset 및 이미지 각도를 제외한 기준 자세 기준(ㄷ 자세 시 joint1 : 0, joint2 : 0도) 각도 theta1, theta2를 구합니다.

해 결정 : 이후 각각 theta1, theta2에 대해 plotto에서 정의한 최대 최소 각도를 넘는지 확인하고, 제한 각도를 벗어나면 그 해는 사용 불가로 간주합니다.(aVaild 또는 bVaild = false)

이후 만약 둘 다 사용 가능하다면, 인자로 받은 prevJ1Deg와 prevJ2Deg를 이용해 더 가까운 각도를 출력합니다.

buildMotionJsonFromSvg()

buildMotionJsonFromSvg함수는 svgPathPoint를 입력으로 받아, 로봇 팔 각 관절에 대한 세부 움직임 명령 리스트 motionJson을 생성합니다. 결과적으로 로봇팔이 처음 위치에서 시작하여 경로의 모든 점을 방문하고, 펜을 올리거나 내리는 동작까지 포함한 명령이 만들어집니다. 이 떄, 모든 명령은 plotto에서 정의된 MAX_STEPS_PT를 넘지 않는 명령으로 변환합니다.

실행 동작

초기 설정 및 유효성 체크 : 함수가 호출되면 this.jsonBulit를 확인하여, plotto 객체에서 motionJson이 만들어졌는지를 확인합니다. 만든 적이 있다면 다시 만들지 않고 종료합니다. 그런다음 plotto motionJson 리스트를 초기화하고, curStepJ1, curStepJ2(현재 관절 위치를 추적하는 변수)를 0으로 초기화합니다. 또한, ji ~ j2의 각도 최대최소 스탭을 나타내는 (j1MinStep 등) 변수도 스탭 단위로 각도 제한을 저장해둡니다.

보조 함수 moveToTarget(targetJ1, targetJ2, penState)

이 내부 함수는 현재 관절 위치에서 목표 관절 위치(targetJ1, targetJ2)까지 펜 상태 penstate로 이동하는데 필요한 d1,d2 명령을 motionJson에 추가합니다. totalDiff1 = targetJ1 - curStepJ1, totalDiff2 = targetJ2 - curStepJ2, maxDiff = max(|totalDiff1|, |totalDiff2|) 식으로 구해서, 만약 maxDiff가 0이면 펜 상태가 바뀌지 않으면 motionJson에 추가하지 않고, pen 상태가 바뀌는 경우만 motionJson에 추가합니다.

maxDiff가 0이 아니라면, 실제 움직임이 필요한 것이므로, 큰 움직임을 한번에 하지 않고 MAX_STEPS_PT 이하의 작은 단계로 쪼개어 여러 움직임으로 추가합니다.

rem1 = totalDiff1, rem2 = totalDiff2으로 남은 이동 스텝을 저장하고, 반복문을 통해서 MAX_STEPS_PT까지 자릅니다. 이후 $('pen').d = penState;로 현재 펜 상태를 지정하고, 반복문 안에서는 펜 상태를 바꾸지 않으면서, rem1과 rem2가 0이 될때까지 반복합니다. 이 moveToTarget 함수를 통해 모든 d1, d2가 MAX_STEPS_PT 이하로 이루어집니다.

홈 위치 -> 첫 번째 경로 점 이동 로봇팔이 처음에는 홈 위치(Joint1 : 0도, Joint2 : 0도)에서 시작한다 가정하고, 가장 처음으로 진행하는 일은 svgPathPoints[0]을 가져와서, inverseKinematics2DOF()함수를 호출하는 일 입니다. inverseKinematics2DOF()시, 처음 점에서 시작하므로, prevJ1=0, prevJ2=0으로 역기구학 계산을 진행합니다. 이후 얻은 각도 값을 firstIk에 반환하여, firstIK를 STEP 단위로 변환 후, penState = 0(시작할때 이므로)로 moveToTarget() 함수를 호출합니다.

이 호출로 인해 홈 위치에서 첫 점의 스탭까지의 명령이 plotto.motionJson에 추가됩니다. 이후 계산한 각도를 prevJ1Deg, prevJ2Deg에 저장합니다.

경로를 따라 점 이동 : 이제 svgPathPoint의 모든 점을 차례대로 순회하며 처리합니다. for (let idx = 0; idx < totalPoints; idx++) 반복문을 통해 순회합니다. svgPathPoint에서 좌표를 얻어, inverseKinematics2DOF() -> moveToTarget() 를 반복합니다. 이때, curStepJ1, curStepJ2, prevJ1Deg, prevJ2Deg도 업데이트 합니다.

마무리 : 루프가 끝난 후, prevPen !==0(펜이 내려진 상태)라면 펜을 올리는 명령을 추가합니다.this.motionJson.push({ d1: 0, d2: 0, pen: 0 }) 이후 this.plot = plotEncode(this.motionJson);를 통해 motionJson을 plot에 저장합니다(1 byte 형태)

3. 객체 정의

4. 주요 함수

function extractPathPointsFromSvg(svgText, opts = {})

extractPathPointsFromSvg(svgText, opts={}) 함수는 SVG 이미지 소스 (svgText)를 입력으로 받아, 그 안에 포함된 모든 도형(graphic element)들의 경로를 따라 일정 간격의 점들을 추출하고 이를 좌표 리스트로 반환하는 기능을 합니다.

매개 변수

이 함수는 두개의 인수를 받습니다. svgText : SVG 파일의 원본 텍스트 입니다. SVG를 문자열 형태로 제공하면, 함수가 이를 파싱하여 DOM 구조로 변환합니다. opts : 경로 추출과 샘플링 옵션을 담은 객체입니다. 기본값은 {}이며, 다음과 같은 속성을 지정할 수 있습니다.

opts 옵션
옵션 이름 타입 / 기본값 설명
sampleStep number / null 샘플링 간격를 직접 지정합니다. 이 값이 주어지면 각 경로를 고정된 간격으로 샘플링하며, samplesPerPath설정은 무시됩니다.
samplesPerPath number / 200 각 도형의 경로를 몇 등분으로 나눌지에 대한 대략적인 기준입니다. 경로 전체를 이 값으로 나누어 평균 간격을 계산합니다.
maxStepClamp number / 2 샘플 간격의 상한값을 지정합니다. 너무 긴 경로의 경우 간격이 지나치게 커지는 것을 방지하기 위한 값으로, 간격이 이보다 크다면 이 값으로 고정됩니다.
maxSamplesPerPath number / 3000 각 경로당 생성될 수 있는 최대 샘플 점 갯수입니다.
bridgeScale number / 1.0 path간 이동 경로의 밀도를 결정하는 스케일 변수입니다.

opts를 통해 경로 샘플링 간격과 밀도를 세밀하게 조정할 수 있습니다.

실행 동작

SVG 파싱 및 초기 설정

DOM 객체 생성 : 함수는 주어진 SVG 텍스트를 파싱하여 DOM 객체를 생성합니다. 이 때, DOM 파서를이용하여 실제 SVG DOM 객체로(doc)변환합니다. 임시 SVG 엘리먼트 생성 : 변수 초기화 : 최종으로 반환할 배열인 points를 초기화합니다.(point에는 {x, y, pen} 객체가 순서대로 쌓입니다.) 또한, 이전 도형의 마지막을 저장하는 lastGlobalPt도 초기화합니다.(첫 도형 후에 값을 채웁니다.), 이 값은 path 간 이동 경로를 계산하기 위해 사용됩니다.

변환 행렬 유틸리티

SVG 요소들은 transfrom 속성을 통해 평행이동, 회전, 스케일, 기울이기(skew)등의 변환을 가지며, <g> 그룹을 이용해서 상위 그룹들의 변환이 누적 적용 될 수 있습니다. spine 코드에선 이러한 변환을 적용하기 위해 변환 affine 행렬을 이용해서 좌표계 경로를 구하며, 이 행렬 유틸리티 함수에 대해 소개하고자 합니다.

I()

항등 행렬을 반환합니다(a=1, d=1이고 나머지 0인 단위행렬)

T(tx, ty)

이동 행렬을 반환합니다. x방향 tx, y 방향 ty만큼 평행이동 시키는 행렬입니다.

S(sx, sy)

스케일 행렬을 반환합니다. x축 방향 sx배 y축 방향 sy배 확대/축소 합니다.

R(deg)

회전 행렬을 반환합니다. 주어진 각도 만큼 원점을 중심으로 회전하는 행렬입니다.

KX(deg)

X축 기준 기울이기 행렬을 반환합니다. 주어진 각도 만큼 x축 방향으로 기울이는 변환입니다.

KY(deg)

Y축 기준 기울이기 행렬을 반환합니다. 주어진 각도 만큼 y축 방향으로 기울이는 변환입니다.

multiplyMatrices(m1, m2)

두개의 행렬 m1,m2를 받아 순서대로 합성한 결과 행렬을 반환합니다.

parseTransform(transformStr)

"translate(...)", "rotate(...)"와 같은 SVG transform 속성 문자열을 읽어들이고, 그 변환들의 합성 결과를 하나의 행렬로 변환하는 함수입니다. 정규식으로 "이름" 패턴을 찾아서 각 변환명령을 순서대로 해석하며, 앞서 정의한 행렬 함수들을 이용합니다.

각 변환 행렬 m을 이전까지의 누적 행렬 M에 곱셈하며 합성해 나갑니다. 이렇게 하면 transform 문자열에 나열된 변환이 순차 적용된 최종 행렬 M을 얻게 됩니다.

getAccumulatedTransformInclusive(el)

<g>그룹에 속한 svg는 자신의 transform을 포함하여 부모를 따라 최상위 svg까지 거슬러 올라가며 모든 변환을 합성해야 합니다. 이 함수는 요소 자체 -> 부모의 변환 .. 순으로 모두 곱한 전체 변환 행렬을 반환합니다.

getAccumulatedTransformParentsOnly(el)

<use> 요소를 구할 때, 자기 자신의 transform을 적용하기 전 상위 요소의 transform을 적용해야 합니다. 이 함수는 요소 자체의 변환을 제외하고 부모들의 누적 변환을 구하여 반환합니다.

shouldRender(el)

실제로 그려지지 않는 요소를 제외하는 필터 함수입니다. 예를 들어, <defs>, display="none"와 같은 요소는 그려지지 않는 요소인데, 이와 같은 것을 제외합니다.

그래픽 요소

SVG 내부의 각 도형 요소들을 수집하고, 각 요소 별로 경로 정보를 준비합니다. 이 때, 크게 두가지 요소를 다루는데, 직접적인 그래픽 요소(path와 기본 도형)와 <use>참고 요소 입니다. 기본 그래픽 요소 처리 svgRoot.querySelectorAll("path, circle, rect, ellipse, line, polygon, polyline")를 이용하여 svg 내 모든 기본 그래픽 요소를 선택합니다. 이 때, shouldRender(el)를 통해 렌더링 대상인지 확인하고, 맞다면 allElements 배열에 추가합니다. 이 때, 추가 시에 요소의 정보도 같이 저장합니다.

 	allElements.push({
    element: el, // 요소
    transform: getAccumulatedTransformInclusive(el), // 누적 변환 행렬
    tagName: el.tagName.toLowerCase() // 태그 명
});

<use> 요소 처리 svg에서는 <use xlink:href="#id">와 같은 형태로 사전에 정의된 도형을 복사하여 쓸 수 있습니다. <use> 요소는 자신의 위치(x, y 속성)와 변환(transform 속성)을 가질 수 있으며, 참조 대상 요소의 정의 변환까지 합쳐져서 최종 위치가 결정됩니다. 코드에선 svgRoot.querySelectorAll("use")를 통해 모든 <use>요소를 찾은 뒤, shouldRender(useEl)로 확인하여 처리합니다. 각 <use>에 대해 resolveUseElement(useEl) 함수를 호출하여 실제 참조된 원본 요소와 그 변환 정보를 얻습니다.

resolveUseElement(useEl) 함수

이 함수는 <use> 요소의 원본 도형 + 여러 단계의 변환을 모두 해석해 하나의 실제 도형으로 변환한 뒤 렌더링 대상에 포함시키는 과정을 거친다. <use>의 href 속성에서 참조 대상 추출 : <use> 요소의 href 또는 xlink:href 값을 읽어 #id 형태의 참조 대상 id를 추출하고, svgRoot.querySelector('#id')로 원본 그래픽 요소를 찾습니다.

원본 요소 존재 확인 : 만약 참조된 id에 해당 원본 요소가 존재하지 않으면, 건너뜁니다.

<use>요소의 x, y 속성을 평행이동 행렬로 변환 : <use> 요소의 x,y 속성을 읽어 (x,y)로 이동시키는 평행이동 행렬 T로 변환합니다.

변환 행렬 수집 : <use>로 인해 적용되는 모든 변환을 다음 네가지 종류로 분리하여 수집합니다.

구분 설명
부모 그룹 변환 <use> 요소의 상위 <g> 요소들에 적용된 transform을 누적한 변환 행렬
<use> 자체 변환 <use> 요소에 직접 지정된 transform 속성으로부터 생성된 변환 행렬
<use> 위치 이동 <use> 요소의 x, y 속성으로부터 생성된 평행이동(translate) 행렬
원본 요소 변환 <use>가 참조하는 원본 그래픽 요소(ref)에 적용된 transform 속성으로부터 생성된 변환 행렬

최종 변환 행렬 합성 : 수집된 변환 행렬을 부모 -> <use> -> x,y 이동 -> 원본 요소 변환 순으로 적용하여 하나의 행렬로 합성합니다.

<use> 해석 결과 객체 생성 : 해석된 <use> 요소에 대해 다음 정보를 포함하는 객체를 생성합니다.

렌더링 대상에 추가 : 위에서 해석된 객체를 allElements 배열에 추가하여, 렌더링 처리에 포함합니다.

이 단계까지 왔다면, allElements 배열에는 SVG 내 모든 처리 대상 도형이 등록되어 있습니다.

도형을 경로 데이터(path)로 변환

SVG내에는 path 요소 외에도 <circle>,<rect> 등 다양한 기본 도형이 있습니다. 이 함수에서는 모든 도형을 path 데이터로 변환하여 처리합니다.

circleToPathLocal(cx,cr,y)

원은 path로 변환하기 위해 두개의 호로 표현합니다. svg path의 A(원호) 명령을 이용하여 두개의 호로 원을 구성합니다. <circle>요소의 cx, cy, r 속성을 읽어 중심 좌표와 반지름을 얻습니다. 원의 가장 왼쪽 점 (cx-r, cy)에서 M 명령으로 경로를 시작합니다. 첫 번째 A r r 0 1 1 명령으로 오른쪽 점 (cx + r, cy)까지 반원을 그립니다. 이후 두 번째 A r r 0 1 1 명령으로 다시 시작점 (cx - r, cy)까지 반원을 그립니다. 마지막에 Z 명령으로 경로를 닫아 하나의 완전한 원 path를 생성합니다. 이 방식으로 두개의 호가 연결되어 하나의 원 경로가 됩니다.

ellipseToPathLocal(cx, cy, rx, ry)

타원은 circle과 거의 동일한 방식으로 처리 되며, X·Y 방향 반지름이 서로 다른 원으로 생각할 수 있습니다. <ellipse> 요소의 cx, cy, rx, ry 속성을 읽습니다. 이후 (cx - rx, cy)에서 M 명령으로 시작하며, A rx ry 0 1 1 명령으로 (cx + rx, cy)까지 반원을 그립니다. 다시 A rx ry 0 1 1 명령으로 시작점까지 이어줍니다. 마지막으로 Z 명령으로 경로를 닫아 하나의 타원 path를 생성합니다.

rectToPathLocal(x, y, width, height, rx, ry)

사각형은 rx, ry(모서리 반경)에 따라 두가지 방식으로 처리합니다.

공통 처리 x, y, width, height, rx, ry 속성을 읽습니다. 여기서 rx, ry가 없으면 0으로 처리하고, 하나만 지정된 경우 두 값을 동일하게 맞춥니다. 또한, rx > width / 2 이면 width / 2로 제한하고, ry > height / 2 이면 height / 2로 제한합니다.

모서리가 둥근 사각형(rx > 0 && ry > 0) rx > 0 이고 ry > 0 인 경우, 각 모서리를 타원 1/4 호에 근사한 베지어 곡선으로 처리합니다. 원의 1/4을 큐빅 베지어로 근사하기 위해 K = 0.5522847498307936 상수를 사용하며, 각 모서리에 대해 ox = rx * K, oy = ry * K 값을 계산합니다. 직선(L)과 큐빅 베지어(C) 명령을 조합하여 네 모서리를 부드럽게 연결합니다. 이후 네 모서리를 모두 연결한 후 Z 명령으로 경로를 닫습니다.

이 결과, 원래 rect의 둥근 모서리를 rx, ry 반지름의 타원 호에 근접한 path로 변환합니다.

모서리가 직각인 사각형(rx <=1e-9 && ry <=1e-9) M x,y로 시작한 뒤, 시계 방향으로 네 개의 L 명령을 사용해 꼭짓점을 연결합니다. 마지막에 Z 명령으로 경로를 닫아 단순 사각형 path를 생성합니다.

lineToPathLocal(x1, y1, x2, y2)

<line> 요소는 시작점 (x1, y1)과 끝점 (x2, y2)을 가지는 단순 선분입니다. M x1,y1 → L x2,y2 형태의 path로 변환합니다.

polygonToPathLocal(points)

<polygon> 요소의 points 속성을 파싱하여 좌표 배열을 얻습니다. 첫번째 점을 M 명령으로 설정하며, 이후 각 점마다 L 명령을 추가하여 직선을 그립니다. polygon은 닫힌 도형이므로 마지막에 Z 명령을 추가하여 경로를 닫습니다.

polylineToPathLocal(points)

<polygon> 요소의 points 속성을 파싱하여 좌표 배열을 얻습니다. 첫번째 점을 M 명령으로 설정하며, 이후 각 점마다 L 명령을 추가하여 직선을 그립니다. polygon은 열린 꺾은 선이므로 마지막에 Z 명령을 추가하여 경로를 닫습니다.

경로 길이 계산

이제 모든 요소에 대해 path 데이터가 준비되었으므로, 각 경로의 길이를 계산하는 단계를 거칩니다. 각 경로의 길이를 구하는 이유는, 점 샘플링 간격을 결정하고 최적화 하기 위해서입니다. 코드에서는 모든 요소에 대해 다음 과정을 수행하고, 결과를 prepared라는 새로운 배열에 모읍니다.

임시 path 요소 생성 : 각 요소에 대해 앞서 얻어둔 path 데이터 문자열과, 누적 변환 행렬을 꺼냅니다. 이후 document.createElementNS(..., "path")로 새로운 <path> 요소를 생성하고, 해당 경로 문자열을 d 속성에 지정합니다. 만약 누적 변환 행렬이 존재한다면, 이 path 요소에 행렬을 적용합니다. 행렬 m이 {a, b, c, d, e, f}라면 pathEl.setAttribute("transform", "matrix(a,b,c,d,e,f)") 형태로 지정합니다. 이를 통해 path가 실제 화면 좌표계에서 변환된 모양으로 취해집니다.

이 path 요소를 아까 만든 tempSvg (숨겨진 SVG) 내부에 추가합니다. 이러면 브라우저 엔진이 해당 path의 길이와 모양을 계산할 수 있는 상태가 됩니다.

경로 길이 측정 : pathEl.getTotalLength()를 호출하여 해당 path (변환 적용된)의 전체 길이 (L)를 얻습니다. 길이가 유효하게 계산된 경우 (L > 0), 현재 요소의 정보를 prepared 배열에 추가합니다. prepared.push({ dAttr, transformMatrix, length: L });

이제 prepared에 경로 데이터와 길이가 모이게 되고, 이 정보에 기반하여 점 샘플링을 수행합니다.

점 샘플링

prepared에 담긴 각 경로를 따라 일정한 간격으로 점을 찍어 points 배열에 누적합니다. 샘플링 단위는 opts와 경로의 길이에 의해 결정되고, 각 도형의 bridge 부분도 함께 처리됩니다.

샘플링 루프 시작 :

샘플링 간격 설정 : 옵션으로 sampleStep이 주어졌다면, step = sampleStep으로 설정합니다. sampleStep이 없다면, 이 경로의 길이를 samplePerPath로 나눈 값으로 step을 정합니다. 이 때, 이렇게 계산한 step이 maxStepClamp보다 큰 경우, step = maxStepClamp로 상한을 제한합니다.(너무 간격이 커지는 것을 방지)

최대 점 수 제한 : 경로가 매우 길어서 step 간격으로 찍으면 점 개수가 maxSamplePerPath를 초과할 경우, 간격을 조금 늘려 점 개수를 제한합니다. 이 과정으로 최종 step이 결정됩니다.

경로를 따라 점 찍기 : 이제 루프를 돌며, 경로를 시작부터 끝까지 step 간격으로 따라가면서 좌표를 샘플링합니다. len에 step을 더해가면서 루프가 진행되며, pathEl.getPointAtLength(len)를 통해 len 거리만큼 떨어진 지점의 좌표를 객체로 반환하고, isFirst가 true일시, pen을 0, 아닐 시 pen을 1로 처리하여 localPoints에 저장합니다.이후 isFirst를 false로 변경합니다(이로써, path 첫 점 이후는 모두 pen 1로 처리됩니다.)

또한, 루프 종료 후, 별도로 pathEl.getPointAtLength(totalLength)를 호출하여 경로의 마지막 점을 얻고, 이를 localPoints에 추가합니다.

임시 path 제거 : 사용이 끝난 pathEl은 다시 tempSvg.removeChild(pathEl)로 제거합니다. 이는 경로 겹침 문제를 피하고, 메모리 소비를 줄이기 위해서 입니다.

도형 간 bridge 추가 : 한 도형의 점을 points에 추가하기 전에, 만약 이전 도형의 마지막 위치가 있을 경우(lastGlobalPt가 null이 아닌 경우), 그 점에서 현재 새 경로의 첫 점까지 펜을 들어 이동하는 경로를 추가로 샘플링합니다. bridge 샘플링 밀도는 기본 step에 opts.bridgeScale를 곱해 조절합니다. 예시 : bridgeStep = step * bridgeScale bridge 구간은 펜을 든 이동이므로, pen은 0으로 기록합니다.

점 누적 및 lastGlobalPt 갱신 : 29. 이제 브리지 점(만약 있다면)이 points에 추가된 상태에서, 현재 도형의 실제 경로 점들(localPoints 배열)을 순서대로 points 배열에 이어붙입니다. 이후 lastGlobalPt를 localPoints의 마지막 점으로 업데이트 합니다. 모든 prepared 항목에 대해 위 과정을 마치면, points 배열에는 SVG의 모든 도형 경로를 따라 생성된 점이 순서대로 담기게 됩니다. 마지막으로 tempSvg (임시로 만들었던 보조 SVG 요소)를 document.body.removeChild(tempSvg)로 제거하여, 화면에 보이지 않게 생성했던 요소를 정리합니다. 그리고 points 배열을 return하여 함수 결과를 내보냅니다.

출력 결과

이 함수의 반환값 points는 다음과 같은 javascript 배열입니다.

[
  { x: Number, y: Number, pen: Number },
  { x: Number, y: Number, pen: Number },
  ...
]

이 결과를 통해, 좌표 -> 로봇의 step 증분값 으로 바꾸는 처리를 거처, 로봇이 이용 가능한 명령으로 변경하게 됩니다.

resamplePathByAngle(points, maxDeltaDeg = plotto.MAX_DELTA_DEG)

resamplePathByAngle 함수는 연속된 두 점 사이의 관절 각도 변화가 maxDeltaDeg를 넘지 않도록 중간 점을 삽입하는 함수입니다.

실행 동작

초기화 및 첫 점 추가 : 결과 리스트 result를 생성하고, 첫 점은 그대로 추가합니다. 또한, 첫 점의 역기구학 해도 미리 보관합니다. 이후 각 인접한 점 쌍(prevPoint, currPoint)에 대해 두 점의 역기구학 결과 각도를 구한 뒤, 각각 변화량을 확인합니다. 두 관절의 각도 변화가 maxDeltaDeg 이하라면 그대로 두고, 아니라면 절반으로

Clone this wiki locally