> [Site index](https://perfectvector.com/llms.txt) — available pages and descriptions.

# SVG 해치 채우기: 펜 플로터용 실제 경로 만들기

> 닫힌 SVG 영역에서 실제 열린 해치 경로를 생성하세요. 원본 Paper.js 예제로 구멍, 간격, 각도, 저장 파일 점검을 재현합니다.

Source: https://perfectvector.com/ko/blog/hatch-fill-svg-pen-paths

Language: ko
Author: Irene Kim
Published: 2026-10-11T08:00:00+09:00
Updated: 2026-10-11T08:00:00+09:00

펜 플로터용 SVG를 해치로 채우려면 의도한 닫힌 영역 안에 열린 선을 생성하고 경계에서 나눈 뒤 남은 선분을 도형으로 내보내세요. 기계 작업을 준비하기 전에 저장 파일에서 구멍과 도형 바깥을 가로지르는 경로를 확인합니다.

색 채움이나 줄무늬 미리보기 자체가 그리기 경로를 제공하지는 않습니다. 이 튜토리얼은 직사각형 구멍이 있는 오각형 엠블럼을 직접 만들고, Paper.js로 열린 선과 해당 영역의 교집합을 구한 다음 저장 SVG를 다시 열어 확인합니다. 디지털 중심선 도형을 확인하는 점검이며 실제 플로팅 성공을 입증하지 않습니다.

## 1. 복원할 선이 아니라 채울 영역 선택하기

구멍이 도형의 일부인 닫힌 영역이 필요합니다. 채워진 도형 위에 흰 사각형을 놓으면 구멍처럼 보여도 아래 영역은 그대로일 수 있습니다. GRBL-Plotter의 [해치 채우기 문서](https://grbl-plotter.de/index.php?id=hatch-fill)는 겹친 객체와 한 경로 안의 구멍을 비교해 이 차이를 보여 줍니다. 여기서는 GRBL-Plotter를 실행하지 않았습니다.

굵은 래스터 선의 가운데에서 스트로크 하나를 복원하려면 [단일 선 SVG 가이드](https://perfectvector.com/ko/blog/drawing-to-single-line-svg)를 사용하세요. 해치는 영역 전체에 여러 스트로크를 만들며 원래 펜 중심선을 복원하지 않습니다.

채워진 그림의 사본을 보관하세요. 예제는 의도적으로 윤곽 통과를 더하지 않고 해치 경로를 만듭니다. 바깥 경계나 구멍 경계도 그리고 싶다면 별도로 의도한 경로로 준비합니다.

## 2. 클리핑한 외관과 잘라 낸 경로 구분하기

SVG [패턴](https://www.w3.org/TR/SVG2/pservers.html#Patterns)은 반복 타일을 칠합니다. [클리핑 경로](https://www.w3.org/TR/css-masking-1/#ClippingPaths)는 아래 도형을 바꾸지 않고 렌더링 영역을 제한합니다. 둘 다 유용한 화면 이미지를 만들 수 있지만 애플리케이션에는 그 외관을 펜 경로로 바꾸는 방법이 여전히 필요합니다.

클리핑만 적용한 대조군에는 각각 200사용자 단위인 수평선 열 개가 저장됩니다. 클리핑은 바깥 부분과 직사각형 구멍을 숨깁니다. 저장 선 길이 합은 여전히 2,000단위입니다.

잘라 낸 넓은 간격 출력에는 합계 1,320단위인 별도 열린 경로 14개가 있습니다. 클리핑 경로, 패턴, 마스크, 내장 이미지, 재사용 심볼이 없습니다. 해치 네 행은 구멍 주변에서 두 경로로 나뉩니다. 클리핑과 잘라 내기 미리보기가 비슷해 보여도 저장 SVG에서 차이를 확인할 수 있습니다.

![다시 연 실제 SVG에서 클리핑 전용 해치 미리보기와 두 간격 및 30도 각도의 명시적인 열린 해치 경로를 비교한 그림](https://perfectvector.com/blog/hatch-fill-svg-pen-paths/hatch-routes.webp)

*자체 제작 디지털 시험 파일과 실제 Paper.js 출력입니다. 세 경로 파일 모두 구멍이 열린 채 유지됩니다. 점검은 중심선에 관한 것이며 플로터나 실제 펜 시험은 하지 않았습니다.*

반복 채색이 의도한 출력이라면 [SVG 패턴 단위 가이드](https://perfectvector.com/ko/blog/svg-pattern-units)를 참고하세요. 여기서 필요한 출력은 명시적인 열린 도형입니다.

## 3. 완전한 Paper.js 예제 실행하기

브라우저, 텍스트 편집기, [공식 릴리스](https://github.com/paperjs/paper.js/releases/tag/v0.12.18)의 Paper.js 0.12.18 전체 빌드를 사용하세요. `paper-full.min.js`를 `library` 폴더에 놓습니다. 다음 HTML을 `generate.html`로, 이어지는 JavaScript를 `hatch.js`로 그 폴더 옆에 저장하세요. 원본 엠블럼은 코드에 포함돼 있습니다.

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Generate original hatch routes</title>
<canvas id="work" width="200" height="140" hidden></canvas>
<p id="status">Running</p>
<pre id="export-receipt"></pre>
<h2>0 degrees, 10-unit spacing</h2>
<img id="preview-coarse" width="400" alt="Coarse hatch">
<pre id="svg-coarse"></pre>
<h2>0 degrees, 5-unit spacing</h2>
<img id="preview-dense" width="400" alt="Dense hatch">
<pre id="svg-dense"></pre>
<h2>30 degrees, 10-unit spacing</h2>
<img id="preview-angled" width="400" alt="Angled hatch">
<pre id="svg-angled"></pre>
<script src="library/paper-full.min.js"></script>
<script src="hatch.js"></script>
</html>
```

```javascript
paper.setup(document.getElementById('work'));

// Original area: a five-sided boundary with a rectangular hole.
const areaData = 'M20 20 L140 20 L180 60 L180 120 L20 120 Z ' +
  'M60 50 L60 90 L110 90 L110 50 Z';
const configurations = [
  { id: 'coarse', angle: 0, spacing: 10 },
  { id: 'dense', angle: 0, spacing: 5 },
  { id: 'angled', angle: 30, spacing: 10 }
];

function makeHatch(angle, spacing) {
  const area = new paper.CompoundPath({
    pathData: areaData,
    fillRule: 'nonzero',
    insert: false
  });
  const radians = angle * Math.PI / 180;
  const direction = new paper.Point(Math.cos(radians), Math.sin(radians));
  const normal = new paper.Point(-direction.y, direction.x);
  const center = new paper.Point(100, 70);
  const delivery = new paper.Group({ insert: false });

  for (let offset = -200 + spacing / 2; offset < 200; offset += spacing) {
    const origin = center.add(normal.multiply(offset));
    const line = new paper.Path.Line({
      from: origin.subtract(direction.multiply(300)),
      to: origin.add(direction.multiply(300)),
      insert: false
    });
    const kept = line.intersect(area, { trace: false, insert: false });
    const parts = kept instanceof paper.CompoundPath ? [...kept.children] : [kept];
    const routes = parts.filter(path => path.length > 1e-7);
    for (const path of routes) {
      if (path.closed || path.segments.length !== 2) {
        throw new Error('Expected one open straight segment');
      }
      if (path.firstSegment.point.dot(direction) > path.lastSegment.point.dot(direction)) {
        path.reverse();
      }
    }
    routes.sort((a, b) => a.firstSegment.point.dot(direction) - b.firstSegment.point.dot(direction));
    for (const path of routes) {
      path.fillColor = null;
      path.strokeColor = '#2f6977';
      path.strokeWidth = 1;
      path.strokeCap = 'butt';
      delivery.addChild(path);
    }
    if (kept instanceof paper.CompoundPath || routes.length === 0) {
      kept.remove();
    }
    line.remove();
  }
  area.remove();
  const geometry = delivery.exportSVG({ asString: true, precision: 6 });
  const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="100mm" height="70mm" ' +
    'viewBox="0 0 200 140">' + geometry + '</svg>';
  const routeCount = delivery.children.length;
  const drawnLength = delivery.children.reduce((sum, path) => sum + path.length, 0);
  let transitionLength = 0;
  for (let index = 1; index < routeCount; index += 1) {
    transitionLength += delivery.children[index - 1].lastSegment.point
      .getDistance(delivery.children[index].firstSegment.point);
  }
  delivery.remove();
  return { svg, routeCount, drawnLength, transitionLength };
}

const receipt = { paper: paper.version, units: 'viewBox user units; 0.5 mm per unit', rows: [] };
for (const configuration of configurations) {
  const result = makeHatch(configuration.angle, configuration.spacing);
  document.getElementById('svg-' + configuration.id).textContent = result.svg;
  const preview = document.getElementById('preview-' + configuration.id);
  preview.src = URL.createObjectURL(new Blob([result.svg], { type: 'image/svg+xml' }));
  receipt.rows.push({ ...configuration, route_count: result.routeCount,
    drawn_length: result.drawnLength, transition_length: result.transitionLength });
}
document.getElementById('export-receipt').textContent = JSON.stringify(receipt, null, 2);
document.getElementById('status').textContent = 'Generated. Save each exact SVG shown below, then reopen those saved files for independent checks.';
```

브라우저에서 `generate.html`을 여세요. Paper.js 0.12.18과 Chromium 155로 실행했습니다. 각 결과는 미리보기와 정확한 SVG 마크업을 모두 표시합니다. 완전한 SVG 문자열 세 개를 `hatch-coarse.svg`, `hatch-dense.svg`, `hatch-angled.svg`에 복사한 뒤 저장 파일을 독립적으로 여세요.

핵심 호출은 `line.intersect(area, { trace: false, insert: false })`입니다. Paper.js 문서는 [`trace: false`](https://paperjs.org/reference/pathitem/#intersect-path)를 첫 경로를 교차점에서 나누고 두 번째 경로 영역 안의 곡선을 유지하는 동작으로 설명합니다. 열린 해치 선분이 유지됩니다. 기본 영역 추적 모드는 다른 불리언 문제에 답합니다. [Paper.js 채움 도형 가이드](https://perfectvector.com/ko/blog/paperjs-svg-booleans)가 그 과정을 다룹니다.

함수는 `fillColor`를 null로 설정하고 각 경로에 스트로크를 적용합니다. 내보내기 전에 두 점으로 된 열린 결과인지도 확인합니다. 닫힌 영역은 작성 보조 요소로 남고 전달 그룹에는 포함되지 않습니다.

## 4. 간격, 각도, 실물 배율 함께 확인하기

세 출력 모두 `width="100mm"`, `height="70mm"`, `viewBox="0 0 200 140"`을 선언합니다. SVG에서 사용자 단위 하나가 0.5 mm에 매핑됩니다. 따라서 10단위 해치 간격은 5 mm, 5단위는 2.5 mm입니다.

의도적으로 넓게 정한 예제 간격은 개별 경로를 쉽게 확인하게 합니다. 잉크로 완전히 덮기 위한 권장값이 아닙니다. 화면 스트로크는 사용자 단위 하나의 너비지만 그 외관이 재료 위 실제 펜 자국을 측정하지는 않습니다.

오프셋 반복문은 해치 시작 위치를 간격의 절반에 고정합니다. 그 위치를 바꾸면 선이 만나는 경계 특징이 달라질 수 있습니다. 양의 30°에서 이 SVG 좌표계의 방향은 오른쪽 아래로 향합니다. 간격은 화면상 세로 간격이 아니라 평행선에 수직으로 측정합니다.

| 저장 파일              | 각도  | 간격           | 열린 경로 | 그려지는 중심선 길이                |
| ------------------ | --- | ------------ | ----- | -------------------------- |
| `hatch-coarse.svg` | 0°  | 10단위 / 5 mm  | 14    | 1,320단위 / 660 mm           |
| `hatch-dense.svg`  | 0°  | 5단위 / 2.5 mm | 28    | 2,640단위 / 1,320 mm         |
| `hatch-angled.svg` | 30° | 10단위 / 5 mm  | 20    | 약 1,318.564단위 / 659.282 mm |

예상 선·다각형 교차를 별도로 계산한 뒤 사용자 단위 0.000002의 허용 오차로 저장 좌표를 비교했습니다. 내보낸 모든 경로가 예상 끝점과 일치했습니다. 각 경로는 열려 있었으며 표본으로 읽은 중심선 점은 그 허용 오차를 넘어 구멍이나 바깥을 지나지 않았습니다. 저장 SVG 세 개 모두 선언한 실물 크기 매핑을 유지했습니다.

다른 도형에서는 이 시험 파일의 개수가 그대로 적용된다고 가정하지 말고 실제 출력 경로를 확인하세요. 곡선, 매우 작은 틈, 자기 교차, 변환된 그룹, 임의의 가져온 SVG 문서는 이 예제 시험 범위 밖입니다.

## 5. 경로 사이 틈과 최적화된 이동 계획 구분하기

넓은 간격 파일에는 열린 경로 14개와 직렬화 순서에서 연속 경로 사이 틈 13개가 있습니다. 펜을 내린 상태로 틈을 연결하면 원하지 않는 자국이 추가됩니다. 그리기 전에 플로팅 과정이 전환을 의도대로 처리해야 합니다.

스크립트는 단순한 단방향 순서를 유지합니다. 해치 오프셋을 차례로 방문하고 각 선분을 같은 방향으로 따릅니다. 각 경로 끝에서 다음 시작까지의 직선 거리를 더하면 넓은 간격 파일은 약 1,562.997사용자 단위입니다. 조밀한 파일에는 틈 27개가 있고 연결 거리 합은 약 3,281.578단위입니다.

이 합은 처음 접근과 마지막 복귀를 제외합니다. 순서에서 나온 기하학적 결과이며 측정한 기계 이동, 시간 추정, 최적화 계획이 아닙니다. 플로팅 애플리케이션이 경로를 뒤집거나 순서를 바꿀 수 있습니다. 간격을 절반으로 줄이자 이 시험의 그려지는 길이와 경로 수는 두 배가 됐지만 보편적으로 작업 시간도 두 배가 된다고 입증되지는 않습니다.

플로터를 제어할 애플리케이션에서 파일을 다시 열어 크기를 확인하고 경로 미리보기와 펜 들기 동작을 살펴보세요. 자신의 구성에서 펜, 재료, 간격, 경계 여유, 이동 설정을 시험합니다. 플로터 작업을 실행하거나 기계 코드를 생성하거나 Cricut Draw/Attach를 시험하지 않았습니다.

## 흔한 도형 오류 수정하기

| 증상                 | 확인하고 바꿀 내용                                                                        |
| ------------------ | --------------------------------------------------------------------------------- |
| 해치가 의도한 구멍을 지남     | 채움 영역을 확인합니다. 교집합을 구하기 전에 흰 덮개를 복합 도형의 실제 구멍으로 바꿉니다.                              |
| 미리보기는 맞지만 전체 선이 남음 | 클리핑이나 패턴 의존성을 찾습니다. 렌더링 실루엣에 의존하지 말고 저장 경로 좌표를 확인합니다.                             |
| 선이 닫힌 도형이 됨        | 첫 피연산자가 열린 선이고 `trace: false`가 있는지 확인합니다. 예제는 닫히거나 여러 선분인 결과를 거부합니다.              |
| 잉크가 경계를 넘음         | 중심선과 스트로크의 실제 점유 범위를 구분합니다. 예제는 펜촉 반지름만큼 영역을 줄이지 않습니다. 적절한 여유를 준비하고 실제 도구를 시험합니다. |
| 채움이 성기거나 표면이 너무 젖음 | 조건을 통제한 펜·재료 샘플로 조정합니다. 디지털 간격만으로 잉크 범위나 건조 특성을 알 수는 없습니다.                        |
| 파일의 이동이 예상치 못하게 뜀  | 대상 소프트웨어에서 순서와 펜을 든 전환을 점검합니다. 최적화 전 원래 경로를 보관하고 구멍과 경계를 다시 확인합니다.                |

원본이 평면 래스터 실루엣뿐이라면 PerfectVector의 [클립아트 SVG 변환 과정](https://perfectvector.com/ko/clipart-to-svg)으로 먼저 닫힌 그림을 복원할 수 있습니다. 윤곽과 각 구멍을 확인한 뒤 확정한 도형에서 해치 경로를 생성하세요. 기존의 깔끔한 벡터 그림은 바로 해치 준비로 넘어갈 수 있습니다.

실루엣 벡터화와 해치 경로 생성은 별도 단계입니다. 이 글의 원본 엠블럼은 벡터 도형으로 직접 작성했으며 PerfectVector 변환 샘플이 아닙니다.

## 자주 묻는 질문

**SVG 채움이 자동으로 펜 해치가 되나요?**
아닙니다. 채움은 칠해진 외관을 설명합니다. 플로팅에는 해치 경로나 그 영역에서 그리기 스트로크를 만드는 다른 지원 방식이 필요합니다.

**해치 미리보기에 여전히 구멍을 지나는 선이 있는 이유는 무엇인가요?**
클리핑은 저장 도형을 자르지 않고도 전체 선의 일부를 숨길 수 있습니다. 명시적인 경로를 만들기 전에 저장 좌표를 확인하고 구멍을 실제 영역의 일부로 만드세요.

**해치 간격은 얼마를 사용해야 하나요?**
의도한 배율에서 펜과 재료 시험으로 선택하세요. 이 디지털 예제는 경로를 보이게 하려고 5 mm와 2.5 mm 간격을 사용합니다. 완전한 잉크 채움이나 보편적인 펜 설정을 정하지는 않습니다.

**하나의 연속 경로인가요?**
아닙니다. 넓은 간격 결과에는 별도의 열린 경로 14개가 있습니다. 구멍 때문에 네 행이 나뉘므로 그리기 과정에서 불필요한 자국 없이 경로 사이 틈을 처리해야 합니다.

## 출처

1. [Paper.js PathItem 교집합](https://paperjs.org/reference/pathitem/#intersect-path) — trace false의 열린 경로 분할 동작을 설명합니다.
2. [SVG 패턴](https://www.w3.org/TR/SVG2/pservers.html#Patterns) — 반복 채색을 명시적인 경로 도형과 별도로 정의합니다.
3. [CSS 마스킹과 클리핑 경로](https://www.w3.org/TR/css-masking-1/#ClippingPaths) — 클리핑이 아래 도형을 자르지 않고 렌더링만 바꾸는 이유를 설명합니다.
4. [GRBL-Plotter 해치 채우기](https://grbl-plotter.de/index.php?id=hatch-fill) — 덮어 놓은 객체와 경로 하나 안의 구멍 차이를 보여 줍니다.
5. [Paper.js 0.12.18 릴리스](https://github.com/paperjs/paper.js/releases/tag/v0.12.18) — 실행 가능한 예제에 사용한 라이브러리 버전을 식별합니다.

래스터 전용 실루엣에 편집 가능한 경계가 필요하다면 [클립아트 SVG 후보를 준비](https://perfectvector.com/ko/clipart-to-svg)하고 구멍을 확인한 뒤 해치 경로를 따로 생성·검증하고 실제 작업을 시험하세요.

## Explore

- [Read the HTML article](https://perfectvector.com/ko/blog/hatch-fill-svg-pen-paths)
- [SVG 접는 선과 자르는 선: 도형 구조 읽기](https://perfectvector.com/ko/blog/svg-score-lines-vs-cut-lines.md)
- [유리 에칭 스텐실: 제거할 부분 선택하기](https://perfectvector.com/ko/blog/glass-etching-stencil-polarity.md)
- [Product facts](https://perfectvector.com/product-facts.md)
- [FAQ](https://perfectvector.com/faq.md)
- [Pricing and credits](https://perfectvector.com/pricing.md)
