PerfectVector
โดย Irene Kim

เติมแฮตช์ SVG: สร้างพาธจริงสำหรับพล็อตเตอร์ปากกา

สร้างพาธแฮตช์เปิดจากพื้นที่ SVG ปิด ทำซ้ำการตรวจช่อง ระยะห่าง มุม และไฟล์บันทึกด้วยตัวอย่าง Paper.js ต้นฉบับ

อ่าน 5 นาที
ในหน้านี้

หากต้องการเติมแฮตช์ SVG สำหรับพล็อตเตอร์ปากกา ให้สร้างเส้นเปิดในพื้นที่ปิดที่ต้องการ แบ่งที่ขอบ แล้วส่งออกท่อนที่เหลือเป็นรูปทรง ตรวจไฟล์ที่บันทึกว่ามีทางข้ามช่องหรืออยู่นอกรูปหรือไม่ ก่อนเตรียมงานเครื่อง

สีเติมหรือภาพตัวอย่างลายเส้นไม่ได้ให้ทางวาดเหล่านั้นโดยตัวมันเอง บทเรียนนี้สร้างตราห้าเหลี่ยมต้นฉบับที่มีช่องสี่เหลี่ยม ใช้ Paper.js ตัดเส้นเปิดกับพื้นที่ แล้วเปิด SVG ที่บันทึกเพื่อตรวจ ยืนยันรูปทรงเส้นกลางดิจิทัล ไม่ใช่การพล็อตจริงที่สำเร็จ

1. เลือกพื้นที่ที่จะเติม ไม่ใช่เส้นที่จะกู้

ต้องมีบริเวณปิดที่ช่องเป็นส่วนของรูปทรง สี่เหลี่ยมขาววางเหนือรูปทึบอาจดูเป็นรู แต่พื้นที่ล่างยังอยู่ครบ เอกสารแฮตช์ฟิลล์ของ GRBL-Plotter แสดงความต่างระหว่างวัตถุซ้อนกับช่องในพาธเดียว เราไม่ได้รัน GRBL-Plotter ที่นี่

หากต้องกู้เส้นเดียวตรงกลางเส้นราสเตอร์หนา ใช้คู่มือ SVG เส้นเดียว แฮตช์สร้างหลายเส้นพาดพื้นที่ ไม่ได้กู้เส้นกลางปากกาเดิม

เก็บสำเนาภาพเติมสี ตัวอย่างตั้งใจสร้างทางแฮตช์โดยไม่เพิ่มการลากขอบ หากอยากวาดขอบนอกหรือขอบช่องด้วย ให้เตรียมเป็นทางแยกอย่างตั้งใจ

2. แยกภาพที่คลิปออกจากพาธที่ตัดจริง

แพตเทิร์น SVG ระบายแผ่นซ้ำ พาธคลิป จำกัดบริเวณแสดงโดยไม่เปลี่ยนรูปทรงต้นฉบับ ทั้งสองช่วยทำภาพบนจอ แต่แอปยังต้องมีวิธีเปลี่ยนรูปลักษณ์นั้นเป็นทางปากกา

ชุดควบคุมคลิปอย่างเดียวเก็บเส้นแนวนอนสิบเส้น เส้นละ 200 หน่วยผู้ใช้ คลิปซ่อนส่วนนอกและช่องสี่เหลี่ยม ความยาวเส้นที่เก็บยังรวม 2,000 หน่วย

ไฟล์ตัดจริงระยะห่างมากมีพาธเปิดแยก 14 เส้น รวม 1,320 หน่วย ไม่มีพาธคลิป แพตเทิร์น มาสก์ ราสเตอร์ฝัง หรือสัญลักษณ์นำกลับมาใช้ แฮตช์สี่แถวแบ่งเป็นสองพาธรอบช่อง ความต่างตรวจใน SVG ที่บันทึกได้แม้ภาพตัวอย่างเหมือนกัน

SVG ที่เปิดซ้ำจริงเปรียบเทียบภาพแฮตช์แบบคลิปอย่างเดียวกับพาธแฮตช์เปิดชัดเจนที่สองระยะห่างและมุม 30 องศา
ชุดทดสอบดิจิทัลต้นฉบับและไฟล์ส่งออก Paper.js จริง ช่องยังเปิดในทั้งสามไฟล์เส้นทาง การตรวจเกี่ยวกับเส้นกลาง ไม่ได้ทดสอบพล็อตเตอร์หรือปากกาจริง

คู่มือหน่วยแพตเทิร์น SVG อธิบายสีซ้ำเมื่อเป็นผลลัพธ์ที่ต้องการ แต่ที่นี่ต้องการรูปทรงเปิดชัดเจน

3. รันตัวอย่าง Paper.js ครบชุด

ใช้เบราว์เซอร์ โปรแกรมข้อความ และ Paper.js 0.12.18 แบบเต็มจากรุ่นอย่างเป็นทางการ ใส่ paper-full.min.js ในโฟลเดอร์ library บันทึก HTML นี้เป็น generate.html และ JavaScript ถัดไปเป็น hatch.js ข้างโฟลเดอร์นั้น โค้ดมีตราต้นฉบับแล้ว

<!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>
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 ว่าแบ่งพาธแรกที่จุดตัดและเก็บส่วนโค้งที่อยู่ภายในพื้นที่พาธที่สอง จึงคงท่อนแฮตช์เปิด โหมดลากพื้นที่เริ่มต้นตอบคำถามบูลีนอีกแบบ คู่มือรูปเติมสี Paper.js อธิบายงานนั้น

ฟังก์ชันตั้ง fillColor เป็น null และให้เส้นกับแต่ละทาง ยังตรวจผลเปิดสองจุดก่อนส่งออก พื้นที่ปิดคงเป็นตัวช่วยสร้าง ไม่รวมในกลุ่มส่งมอบ

4. ตรวจระยะ มุม และสเกลจริงร่วมกัน

ทั้งสามไฟล์ระบุ width="100mm", height="70mm" และ viewBox="0 0 200 140" ทำให้หนึ่งหน่วยผู้ใช้เป็น 0.5 mm ใน SVG ดังนั้นระยะแฮตช์ 10 หน่วยคือ 5 mm ส่วน 5 หน่วยคือ 2.5 mm

ระยะตัวอย่างที่ตั้งใจให้กว้างทำให้ตรวจแต่ละทางง่าย ไม่ใช่คำแนะนำให้หมึกคลุมทึบ เส้นหน้าจอกว้างหนึ่งหน่วยผู้ใช้ แต่รูปลักษณ์ไม่ได้วัดรอยปากกาจริงบนวัสดุ

ลูป offset กำหนดเฟสแฮตช์ที่ครึ่งระยะ เปลี่ยนเฟสอาจทำให้เส้นพบขอบคนละจุด ที่บวก 30° ทิศไปขวาลงในพิกัด SVG นี้ ระยะห่างวัดตั้งฉากกับเส้นขนาน ไม่ใช่ช่องแนวตั้งที่เห็นบนจอ

ไฟล์บันทึกมุมระยะห่างทางเปิดความยาวเส้นกลางที่วาด
hatch-coarse.svg0°10 หน่วย / 5 mm141,320 หน่วย / 660 mm
hatch-dense.svg0°5 หน่วย / 2.5 mm282,640 หน่วย / 1,320 mm
hatch-angled.svg30°10 หน่วย / 5 mm20ประมาณ 1,318.564 หน่วย / 659.282 mm

เราคำนวณจุดตัดเส้นกับรูปหลายเหลี่ยมที่คาดแยก แล้วเทียบพิกัดบันทึกด้วยค่าคลาดเคลื่อน 0.000002 หน่วยผู้ใช้ ทุกทางตรงจุดปลายที่คาด ทุกทางเปิด และไม่มีจุดตัวอย่างเส้นกลางข้ามช่องหรือด้านนอกเกินค่าคลาดเคลื่อนนั้น SVG ทั้งสามรักษาการแมปขนาดจริงที่ประกาศ

สำหรับรูปอื่น ให้ตรวจทางที่ส่งออกจริงแทนการคิดว่าจำนวนตัวอย่างนี้ใช้ต่อได้ เส้นโค้ง ช่องจิ๋ว การตัดกันเอง กลุ่มแปลงรูป และเอกสาร SVG นำเข้าใด ๆ อยู่นอกขอบเขตการทดสอบนี้

5. แยกช่องระหว่างทางจากแผนเดินที่ปรับเหมาะแล้ว

ไฟล์ระยะกว้างมี 14 ทางเปิดและ 13 ช่องระหว่างทางติดกันตามลำดับที่บันทึก การลากเชื่อมขณะปากกาลงจะเพิ่มรอยไม่ต้องการ กระบวนการพล็อตต้องจัดการการเปลี่ยนทางให้ถูกก่อนวาด

สคริปต์ใช้ลำดับทางเดียวเรียบง่าย ไล่ offset ตามลำดับและตามแต่ละท่อนไปทิศเดียวกัน ผลรวมระยะตรงจากปลายแต่ละทางถึงต้นทางถัดไปประมาณ 1,562.997 หน่วยผู้ใช้ในไฟล์ระยะกว้าง ไฟล์หนาแน่นมี 27 ช่องและผลรวมเชื่อมประมาณ 3,281.578 หน่วย

ผลรวมไม่รวมเข้าเริ่มต้นและกลับสุดท้าย เป็นผลทางเรขาคณิตจากลำดับนี้ ไม่ใช่ระยะเครื่องวัดจริง ประมาณเวลา หรือแผนปรับเหมาะ แอปพล็อตอาจกลับทิศหรือเรียงใหม่ การลดระยะครึ่งหนึ่งทำให้ความยาววาดและจำนวนทางตัวอย่างนี้เป็นสองเท่า ไม่ได้ยืนยันว่าเวลางานทุกแบบจะสองเท่า

เปิดไฟล์ในแอปควบคุมพล็อตเตอร์ ยืนยันขนาด แล้วดูภาพเส้นทางและการยกปากกา ทดสอบปากกา วัสดุ ระยะห่าง ระยะจากขอบ และค่าการเคลื่อนในเครื่องของคุณ เราไม่ได้รันพล็อตเตอร์ สร้างโค้ดเครื่อง หรือทดสอบ Cricut Draw/Attach

แก้ข้อผิดพลาดรูปทรงที่พบบ่อย

อาการสิ่งที่ตรวจและเปลี่ยน
แฮตช์ข้ามช่องที่ต้องการตรวจพื้นที่เติม แทนรูปขาวทับด้วยช่องจริงในรูปทรงประกอบก่อนตัดกัน
ภาพตัวอย่างถูกแต่ยังมีเส้นเต็มหา clip หรือ pattern ที่พึ่งอยู่ ตรวจพิกัดทางที่บันทึกแทนพึ่งรูปเงาที่แสดง
เส้นกลายเป็นรูปปิดยืนยันตัวตั้งแรกเป็นเส้นเปิดและมี trace: false ตัวอย่างปฏิเสธผลปิดหรือหลายท่อน
หมึกเลยขอบแยกเส้นกลางจากพื้นที่กว้างของเส้น ตัวอย่างไม่ได้หดพื้นที่ตามรัศมีหัวปากกา เตรียมระยะเว้นและทดสอบเครื่องมือจริง
เติมห่างหรือพื้นผิวเปียกเกินปรับจากตัวอย่างปากกาและวัสดุที่ควบคุมได้ ระยะดิจิทัลอย่างเดียวไม่กำหนดการคลุมหรือแห้ง
ไฟล์กระโดดผิดคาดดูลำดับและการยกปากกาในโปรแกรมปลายทาง เก็บทางเดิมก่อนใช้ตัวปรับเหมาะ แล้วตรวจช่องและขอบใหม่

หากต้นทางมีแค่รูปเงาราสเตอร์สีเรียบ กระบวนการคลิปอาร์ตเป็น SVG ของ PerfectVector ช่วยกู้งานปิดก่อนได้ ตรวจขอบและทุกช่อง แล้วสร้างทางแฮตช์จากรูปทรงที่รับแล้ว เวกเตอร์สะอาดที่มีอยู่เข้าเตรียมแฮตช์ได้เลย

แปลงรูปเงาเป็นเวกเตอร์กับสร้างทางแฮตช์เป็นคนละขั้น ตราต้นฉบับบทความนี้สร้างเป็นเวกเตอร์ ไม่ใช่ตัวอย่างแปลงด้วย PerfectVector

คำถามที่พบบ่อย

สีเติม SVG กลายเป็นแฮตช์ปากกาอัตโนมัติหรือไม่? ไม่ สีเติมอธิบายรูปลักษณ์ที่ระบาย กระบวนการพล็อตต้องมีทางแฮตช์หรือวิธีที่รองรับในการสร้างเส้นวาดจากพื้นที่

ทำไมภาพแฮตช์ยังมีเส้นผ่านช่อง? คลิปซ่อนบางส่วนของเส้นเต็มได้โดยไม่ตัดรูปทรงที่เก็บ ตรวจพิกัดบันทึกและให้ช่องเป็นส่วนของพื้นที่จริงก่อนสร้างทางชัดเจน

ควรใช้ระยะแฮตช์เท่าไร? เลือกผ่านการทดสอบปากกาและวัสดุที่สเกลใช้งาน ตัวอย่างดิจิทัลนี้ใช้ 5 mm และ 2.5 mm เพื่อให้เห็นทาง ไม่ได้กำหนดการคลุมทึบหรือค่าปากกาสากล

นี่คือพาธต่อเนื่องเส้นเดียวหรือไม่? ไม่ ผลระยะกว้างมีทางเปิดแยก 14 เส้น ช่องแบ่งสี่แถว และกระบวนการวาดต้องจัดการช่องระหว่างทางโดยไม่เพิ่มรอยไม่ต้องการ

แหล่งข้อมูล

  1. Paper.js PathItem intersection — อธิบายการแบ่งพาธเปิดเมื่อ trace false
  2. แพตเทิร์น SVG — นิยามสีซ้ำแยกจากรูปทรงทางชัดเจน
  3. CSS masking และ clipping paths — อธิบายว่าคลิปเปลี่ยนการแสดงโดยไม่ตัดรูปต้นฉบับอย่างไร
  4. GRBL-Plotter hatch fill — แสดงความต่างวัตถุวางทับกับช่องในพาธเดียว
  5. Paper.js รุ่น 0.12.18 — ระบุรุ่นไลบรารีที่ใช้ในตัวอย่างรันได้

หากรูปเงาราสเตอร์ต้องการขอบแก้ไขได้ ให้เตรียมคลิปอาร์ต SVG ตัวเลือก ตรวจช่อง และสร้างพร้อมยืนยันทางแฮตช์แยกก่อนทดสอบงานจริง

เพิ่มเติมจากบล็อก

PerfectVector

เตรียมเส้นพาธ SVG ที่เรียบร้อยสำหรับ
การตัดและการผลิต

ไม่ต้องใช้บัตรเครดิต