LGES-OC5-PtoP-Welder/Doc/인수인계/260907_Arc출사_파라미터_시각화.html
NohDoyeon241104 a3238cc2d6 docs: 인수인계 산출물 일괄 커밋 (260903~260916)
그동안 미추적으로 쌓여 있던 인수인계 문서를 정리해 올린다.

260903 Door 인터락 현장검증 가이드 / 파워캘 3000W 오적용 환산

260907 Arc 출사 파라미터 시각화 / LaserFrequency 단위계통 / NRE 위험구간 점검 / 레시피화면 사용설명서

260915 레시피화면 작업매뉴얼

260916 PTOP 프로그램 사용설명서 / 자동운전 전환 사전점검 현장용

작업 중 임시본(_backup)은 제외했다.
2026-09-16 17:10:50 +09:00

662 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ko" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arc 출사 파라미터 시각화 — PTOP (260907)</title>
<style>
:root{
color-scheme: light;
--plane:#f9f9f7; --surface-1:#fcfcfb;
--text-primary:#0b0b0b; --text-secondary:#52514e; --muted:#898781;
--grid:#e1e0d9; --axis:#c3c2b7; --border:rgba(11,11,11,0.10);
--series-1:#2a78d6; /* 마크(용접) 경로 */
--series-2:#eb6834; /* 점프 경로 */
--series-3:#1baf7a; /* 와블 오버레이 */
--st-good:#0ca30c; --st-warn:#fab219; --st-serious:#ec835a; --st-crit:#d03b3b;
/* blue sequential ramp = 파워 크기 */
--p250:#86b6ef; --p350:#5598e7; --p450:#2a78d6; --p550:#1c5cab; --p650:#104281;
}
@media (prefers-color-scheme: dark){
:root:where(:not([data-theme="light"])){
color-scheme: dark;
--plane:#0d0d0d; --surface-1:#1a1a19;
--text-primary:#ffffff; --text-secondary:#c3c2b7; --muted:#898781;
--grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10);
--series-1:#3987e5; --series-2:#d95926; --series-3:#199e70;
--p250:#184f95; --p350:#256abf; --p450:#3987e5; --p550:#6da7ec; --p650:#9ec5f4;
}
}
:root[data-theme="dark"]{
color-scheme: dark;
--plane:#0d0d0d; --surface-1:#1a1a19;
--text-primary:#ffffff; --text-secondary:#c3c2b7; --muted:#898781;
--grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10);
--series-1:#3987e5; --series-2:#d95926; --series-3:#199e70;
--p250:#184f95; --p350:#256abf; --p450:#3987e5; --p550:#6da7ec; --p650:#9ec5f4;
}
*{box-sizing:border-box}
body{
margin:0; background:var(--plane); color:var(--text-primary);
font-family:system-ui,-apple-system,"Segoe UI","Malgun Gothic",sans-serif;
font-size:14px; line-height:1.6;
}
.wrap{max-width:1280px; margin:0 auto; padding:0 24px 80px}
header.banner{
background:linear-gradient(100deg,#104281,#2a78d6);
color:#fff; padding:28px 0 24px; margin-bottom:28px;
}
header.banner .wrap{padding-bottom:0}
header.banner h1{margin:0 0 6px; font-size:23px; letter-spacing:-0.01em}
header.banner p{margin:0; opacity:.88; font-size:13px}
.badges{margin-top:14px; display:flex; gap:8px; flex-wrap:wrap}
.badge{
background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.28);
border-radius:999px; padding:3px 11px; font-size:12px; font-variant-numeric:tabular-nums;
}
h2{font-size:18px; margin:40px 0 6px; letter-spacing:-0.01em}
h2 .num{color:var(--muted); font-variant-numeric:tabular-nums; margin-right:8px}
h3{font-size:14px; margin:24px 0 8px; color:var(--text-secondary)}
p.lede{margin:0 0 16px; color:var(--text-secondary); font-size:13px}
code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12.5px;
background:var(--surface-1); border:1px solid var(--border); border-radius:4px; padding:1px 5px}
.theme-toggle{
position:fixed; top:14px; right:16px; z-index:50; cursor:pointer;
background:var(--surface-1); color:var(--text-primary);
border:1px solid var(--border); border-radius:8px; padding:6px 12px; font-size:12px;
}
/* ── 결론 카드 ─────────────────────────────── */
.cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:14px; margin:16px 0 8px}
.card{background:var(--surface-1); border:1px solid var(--border); border-radius:10px; padding:16px 18px}
.card .st{display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600; margin-bottom:8px}
.card .st .ic{width:16px;height:16px;display:grid;place-items:center;border-radius:50%;color:#fff;font-size:11px;font-weight:700}
.card h4{margin:0 0 6px; font-size:14.5px}
.card p{margin:0 0 8px; font-size:12.5px; color:var(--text-secondary)}
.card .fix{font-size:12.5px; border-top:1px dashed var(--border); padding-top:8px; margin-top:4px}
.ic-crit{background:var(--st-crit)} .ic-warn{background:var(--st-warn); color:#0b0b0b} .ic-ser{background:var(--st-serious)}
/* ── 범례 ─────────────────────────────── */
.legend{display:flex; gap:18px; flex-wrap:wrap; align-items:center;
background:var(--surface-1); border:1px solid var(--border); border-radius:10px;
padding:11px 16px; margin:12px 0 18px; font-size:12.5px}
.legend .item{display:flex; align-items:center; gap:7px; color:var(--text-secondary)}
.legend svg{flex:none}
/* ── 격자 ─────────────────────────────── */
.grid{display:grid; gap:12px}
.g4{grid-template-columns:repeat(4,minmax(0,1fr))}
.g5{grid-template-columns:repeat(5,minmax(0,1fr))}
.g3{grid-template-columns:repeat(3,minmax(0,1fr))}
@media(max-width:1080px){.g4,.g5{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){.g4,.g5,.g3{grid-template-columns:1fr}}
figure.cell{
margin:0; background:var(--surface-1); border:1px solid var(--border);
border-radius:10px; padding:10px 10px 8px; cursor:default; transition:border-color .12s;
}
figure.cell:hover{border-color:var(--series-1)}
figure.cell.ng{border-color:var(--st-crit)}
figure.cell svg{display:block; width:100%; height:auto}
figcaption{margin-top:6px}
figcaption .t{font-size:12.5px; font-weight:600; display:flex; align-items:center; gap:6px}
figcaption .s{font-size:11.5px; color:var(--muted); font-variant-numeric:tabular-nums; margin-top:2px}
.tag{font-size:10.5px; font-weight:700; padding:1px 6px; border-radius:4px; color:#fff; letter-spacing:.02em}
.tag.ng{background:var(--st-crit)} .tag.ok{background:var(--st-good)}
.tag.wr{background:var(--st-warn); color:#0b0b0b} .tag.na{background:var(--muted)}
/* 행 라벨 격자 */
.rowgrid{display:grid; gap:12px; grid-template-columns:132px repeat(4,minmax(0,1fr)); align-items:stretch}
@media(max-width:1080px){.rowgrid{grid-template-columns:1fr}}
.rowlab{display:flex; flex-direction:column; justify-content:center; font-size:12.5px;
font-weight:600; padding:4px 2px; border-right:1px solid var(--border)}
.rowlab span{font-weight:400; font-size:11.5px; color:var(--muted); margin-top:3px}
@media(max-width:1080px){.rowlab{border-right:none; border-bottom:1px solid var(--border); padding-bottom:8px}}
.colhead{display:grid; gap:12px; grid-template-columns:132px repeat(4,minmax(0,1fr));
font-size:12px; color:var(--text-secondary); font-weight:600; margin-bottom:8px}
@media(max-width:1080px){.colhead{display:none}}
/* ── 표 ─────────────────────────────── */
table{width:100%; border-collapse:collapse; font-size:12.5px; background:var(--surface-1);
border:1px solid var(--border); border-radius:10px; overflow:hidden; margin-top:10px}
th,td{padding:8px 11px; text-align:left; border-bottom:1px solid var(--grid); vertical-align:top}
th{background:var(--plane); font-weight:600; color:var(--text-secondary); font-size:12px}
td.num{font-variant-numeric:tabular-nums; white-space:nowrap}
tr:last-child td{border-bottom:none}
details{margin-top:12px} summary{cursor:pointer; font-size:13px; color:var(--text-secondary)}
/* ── 툴팁 ─────────────────────────────── */
#tip{position:fixed; z-index:80; pointer-events:none; opacity:0; transition:opacity .1s;
background:var(--surface-1); color:var(--text-primary); border:1px solid var(--border);
border-radius:8px; padding:9px 12px; font-size:12px; max-width:320px;
box-shadow:0 6px 22px rgba(0,0,0,.16); font-variant-numeric:tabular-nums}
#tip b{display:block; margin-bottom:4px; font-size:12.5px}
#tip .k{color:var(--muted)}
</style>
</head>
<body>
<button class="theme-toggle" id="themeBtn">🌗 다크/라이트</button>
<header class="banner"><div class="wrap">
<h1>Arc 출사 파라미터 시각화 — 원점 방향 이상 발광 분석</h1>
<p>ILSOS_PrismaticRD_PTOP · SeqUploadWeldingList / ScannerParam · 정적 분석 기준</p>
<div class="badges">
<span class="badge">작성 2026-09-07</span>
<span class="badge">근거 로그 Aurora_20260905 / 20260906</span>
<span class="badge">소스 무수정 (분석 전용)</span>
<span class="badge">현장 검증 미실시</span>
</div>
</div></header>
<div class="wrap">
<!-- ══════════════ 0. 결론 ══════════════ -->
<h2><span class="num">0</span>결론 — Arc가 원점 방향으로 발광한 원인 후보 3건</h2>
<p class="lede">
모두 <b>레시피/스캐너 파라미터로 바로 회피 가능</b>합니다. 아래 카드의 조치값을 먼저 적용한 뒤 재현 여부를 보면
어느 후보인지 확정됩니다. 후보 1과 2는 Arc 전용 경로이고, 후보 3은 전 타입 공통입니다.
</p>
<div class="cards">
<div class="card">
<div class="st"><span class="ic ic-crit">✕</span><span style="color:var(--st-crit)">CRITICAL · 최유력</span></div>
<h4>후보 1 — Arc Ramping Interval = 0 (0으로 나눗셈)</h4>
<p><code>ArcRampingInterval</code>은 초기값이 없어 <b>기본 0.0</b>입니다(ScannerParam.cs:331).
여기에 Ramping Start/End Distance를 하나라도 넣으면 <code>theta = (0/R)×(180/π) = 0</code>이 되고,
<code>(int)(Dist/0)</code> → <code>(int)+∞</code> = <b>int.MinValue</b>로 카운터가 붕괴합니다.
Constant 루프가 <b>약 21.5억 회</b> <code>ArcAbs(center, 0°)</code>를 리스트에 밀어넣어
RTC 리스트가 넘치고, 넘친 자리의 미초기화 좌표(0,0 = 원점)가 <b>마크(레이저 ON)로 실행</b>됩니다.</p>
<div class="fix"><b>조치</b> · Arc를 쓰는 모든 레시피에서 <code>Arc Ramping Interval = 0.1 mm</code>
(툴팁 권장값). 램핑이 필요 없으면 <code>Start Distance = End Distance = 0</code>으로 두어
램핑 분기 자체를 타지 않게 하는 편이 더 안전합니다.</div>
</div>
<div class="card">
<div class="st"><span class="ic ic-ser">!</span><span style="color:var(--st-serious)">SERIOUS</span></div>
<h4>후보 2 — 램핑 길이 가드의 단위 버그 → 과회전</h4>
<p>분기 조건(SeqUploadWeldingList.cs:1303)이 호 길이를 <code>R × |Sweep°|</code>로 계산합니다.
실제 호 길이는 <code>R × |Sweep°| × π/180</code>이라 <b>우변이 약 57.3배 과대</b>합니다.
(정확한 값은 <code>Arc.Length</code>에 이미 있는데 쓰지 않습니다.)
그래서 램핑 거리가 실제 호보다 길어도 분기에 진입하고,
rising+falling 반복이 totalCount를 넘겨 <b>의도한 각도를 초과해 레이저 ON 상태로 회전</b>합니다.
끝나면 <code>_LastX/_LastY</code>에 실제와 다른 좌표(PF2)를 기록해,
<b>다음 객체가 점프를 생략하고 엉뚱한 지점에서 마크를 시작</b>합니다.</p>
<div class="fix"><b>조치</b> · <code>Start Dist + End Dist ≤ 호길이 × 0.5</code>.
호길이 = <code>R × |Sweep°| × 0.01745</code>. §4에 계산 예시가 있습니다.</div>
</div>
<div class="card">
<div class="st"><span class="ic ic-warn">!</span><span style="color:#8a6100">WARNING · 병행 확인</span></div>
<h4>후보 3 — LaserOnDelay −1800 µs + JumpDelay 0</h4>
<p><code>LaserOnDelay</code>는 <b>부호 있는 int</b>(ScannerParam.cs:58)이고 단위는 µs입니다
(ScannerRTC6.cs:691, 1bit=0.5µs). 음수는 마크 시작 <b>이전에</b> 레이저를 켜므로
<b>직전 점프 구간 끝에서 발광</b>합니다. 현장 로그 실측값이 −1800 µs이고 JumpDelay가 0이라
미러 정정 대기 없이 발광합니다. JumpSpeed 1000 mm/s 기준 <b>최대 1.8 mm 선행 발광</b>.</p>
<div class="fix"><b>조치</b> · <code>LaserOnDelay</code> −1800 → −200~0 검토,
<code>JumpDelay</code> 0 → 100~300 µs.
<b>단 1.8 mm 규모라 "원점까지"는 이것만으로 설명되지 않습니다.</b> 후보 1·2와 함께 보십시오.</div>
</div>
</div>
<!-- ══════════════ 1. 기준 파라미터 ══════════════ -->
<h2><span class="num">1</span>기준 파라미터 세트 (현장 로그 실측값)</h2>
<p class="lede">
아래 A는 <code>Aurora_20260906.log</code>에서 그대로 읽은 값이고, B·C는 이 문서의 시뮬레이션 예시입니다.
이후 모든 격자는 이 세 세트를 기준으로 그렸습니다.
</p>
<table>
<thead><tr>
<th style="width:26%">파라미터</th><th style="width:24%">A · 현장 실측</th>
<th style="width:24%">B · 권장(램핑 사용)</th><th style="width:26%">C · 재현 조건(NG)</th>
</tr></thead>
<tbody>
<tr><td>LaserOnDelay</td><td class="num">−1800 µs</td><td class="num">−200 µs</td><td class="num">−1800 µs</td></tr>
<tr><td>LaserOffDelay <span style="color:var(--muted)">(uint·음수 불가)</span></td><td class="num">0 µs</td><td class="num">0 µs</td><td class="num">0 µs</td></tr>
<tr><td>JumpDelay</td><td class="num">0 µs</td><td class="num">200 µs</td><td class="num">0 µs</td></tr>
<tr><td>MarkDelay</td><td class="num">200 µs</td><td class="num">200 µs</td><td class="num">200 µs</td></tr>
<tr><td>PolygonDelay <span style="color:var(--muted)">(Arc 분절 간)</span></td><td class="num">0 µs</td><td class="num">50~100 µs</td><td class="num">0 µs</td></tr>
<tr><td>JumpSpeed</td><td class="num">1000 mm/s</td><td class="num">1000 mm/s</td><td class="num">1000 mm/s</td></tr>
<tr><td>MarkSpeed</td><td class="num">100 mm/s</td><td class="num">100 mm/s</td><td class="num">100 mm/s</td></tr>
<tr><td>LaserPower1 (Center)</td><td class="num">467 W</td><td class="num">467 W</td><td class="num">467 W</td></tr>
<tr><td><b>ArcRampingInterval</b></td><td class="num">— (미설정 = 0)</td><td class="num"><b>0.1 mm</b></td><td class="num"><b>0 mm</b></td></tr>
<tr><td>RampingStartDist / EndDist</td><td class="num">0 / 0</td><td class="num">2 / 2 mm</td><td class="num">10 / 10 mm</td></tr>
<tr><td>RampingStartPower / EndPower</td><td class="num">—</td><td class="num">30 / 30 %</td><td class="num">30 / 30 %</td></tr>
<tr><td>Arc 형상 (예시)</td><td class="num">R 10 mm, Sweep 90°</td><td class="num">R 10 mm, Sweep 90°</td><td class="num">R 10 mm, Sweep 90°</td></tr>
<tr><td style="color:var(--muted)">→ 실제 호 길이</td><td class="num" colspan="3">R × Sweep° × π/180 = 10 × 90 × 0.01745 = <b>15.71 mm</b></td></tr>
</tbody>
</table>
<!-- ══════════════ 2. 범례 ══════════════ -->
<h2><span class="num">2</span>격자 읽는 법</h2>
<div class="legend" id="legend"></div>
<p class="lede" style="margin-top:-6px">
색만으로 구분하지 않습니다 — <b>점프는 파선, 마크는 실선, 이상 발광은 굵은 실선 + ✕ 표식</b>입니다.
파워 크기는 같은 파랑 계열의 명도(연함 = 저출력, 진함 = 고출력)로만 표현합니다.
각 셀에 마우스를 올리면 해당 조합의 파라미터가 표시됩니다. 전체 수치는 §6 표로도 제공합니다.
</p>
<!-- ══════════════ 3. 타입별 ══════════════ -->
<h2><span class="num">3</span>격자 A — Draw 타입별 출사 형상 (출사 분기 8종)</h2>
<p class="lede">
<code>FuncLoadDrawObjectScannerParamSelected()</code>가 실제로 처리하는 8개 분기입니다.
Ellipse · Rectangle · Roi는 분기가 없어 여기에 없습니다(별건 — 예외처리 대기 중).
</p>
<div class="grid g4" id="gridA"></div>
<!-- ══════════════ 4. Arc 조합 ══════════════ -->
<h2><span class="num">4</span>격자 B — Arc × 램핑/와블/스노우맨 조합 <span style="color:var(--st-crit)">(핵심)</span></h2>
<p class="lede">
행 = 램핑 상태, 열 = 와블/스노우맨 상태. 모두 <b>R 10 mm · Sweep 90° · CCW</b> 동일 형상입니다.
빨간 셀이 이번 현상의 재현 조건입니다.
</p>
<div class="colhead">
<div></div><div>Wobble OFF</div><div>Wobble ON · 진폭 小</div><div>Wobble ON · 진폭 大</div><div>SnowMan ON</div>
</div>
<div id="gridB"></div>
<!-- ══════════════ 5. 값 크기 ══════════════ -->
<h2><span class="num">5</span>격자 C — 값의 크고 작음이 만드는 차이</h2>
<p class="lede">각 행은 파라미터 하나를 <b>작게 / 권장 / 크게</b> 3단으로 비교합니다.</p>
<div class="colhead" style="grid-template-columns:132px repeat(3,minmax(0,1fr))">
<div></div><div>작게</div><div>권장</div><div>크게</div>
</div>
<div id="gridC"></div>
<!-- ══════════════ 6. 표 ══════════════ -->
<h2><span class="num">6</span>표 뷰 — 격자 B 전체 수치</h2>
<p class="lede">그림 없이도 같은 내용을 읽을 수 있도록 격자 B를 표로 옮겼습니다.</p>
<table id="tblB"><thead><tr>
<th>램핑 상태</th><th>와블 상태</th><th class="num">Interval</th><th class="num">Start+End</th>
<th class="num">theta</th><th class="num">totalCount</th><th class="num">실제 회전각</th><th>판정</th><th>근거</th>
</tr></thead><tbody></tbody></table>
<!-- ══════════════ 7. 체크리스트 ══════════════ -->
<h2><span class="num">7</span>현장 조치 체크리스트</h2>
<table>
<thead><tr><th style="width:34px">#</th><th style="width:30%">항목</th><th>내용</th></tr></thead>
<tbody>
<tr><td class="num">1</td><td>Arc Ramping Interval 확인</td>
<td>Arc를 쓰는 <b>모든</b> 레시피 · 모든 Scanner Param No에 대해 값이 <code>0</code>이 아닌지 확인.
0이면 <b>0.1</b>로 설정. (램핑 미사용이면 2번으로)</td></tr>
<tr><td class="num">2</td><td>램핑 미사용 시</td>
<td><code>Start Distance = 0</code>, <code>End Distance = 0</code> → 단일 <code>ArcAbs</code> 경로(:1369)로 빠져
카운터 붕괴·과회전 모두 회피됩니다. <b>가장 안전한 설정</b>입니다.</td></tr>
<tr><td class="num">3</td><td>램핑 사용 시 길이 검산</td>
<td>호길이 = <code>R × Sweep° × 0.01745</code>. <code>Start + End ≤ 호길이 × 0.5</code>를 만족하는지
Arc마다 확인. 코드의 자동 가드는 57.3배 느슨해서 <b>걸러주지 않습니다</b>.</td></tr>
<tr><td class="num">4</td><td>선행 발광 확인</td>
<td><code>LaserOnDelay</code> −1800 µs 유지 여부 판단. 점프 종단 1.8 mm 발광이 허용되는 공정인지 확인.
줄인다면 −200 µs부터. 동시에 <code>JumpDelay</code>를 100~300 µs로.</td></tr>
<tr><td class="num">5</td><td>Arc 분절 정정</td>
<td><code>PolygonDelay = 0</code>은 Arc 분절 사이 정정 대기가 없다는 뜻입니다.
원호 오버슈트가 보이면 50~100 µs를 넣고 비교.</td></tr>
<tr><td class="num">6</td><td>재현 시험</td>
<td>1·2번만 적용하고 재현되는지 먼저 확인 → 재현되면 후보 3(딜레이) 쪽으로 이동.
한 번에 다 바꾸면 어느 것이 원인인지 확정되지 않습니다.</td></tr>
</tbody>
</table>
<details>
<summary>참조한 소스 위치</summary>
<table style="margin-top:8px">
<thead><tr><th style="width:44%">위치</th><th>내용</th></tr></thead><tbody>
<tr><td><code>SeqUploadWeldingList.cs:1269</code></td><td><code>length = param.ArcRampingInterval</code></td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1273-1278</code></td><td><code>theta = (length/radius)×(180/π)</code>, CW는 부호 반전</td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1302-1303</code></td><td>램핑 분기 가드 — <code>R×|Sweep°|</code> (π/180 누락)</td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1313-1316</code></td><td><code>risingEdgeCount</code>·<code>fallingEdgeCount</code>·<code>totalCount</code></td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1342-1344</code></td><td>Constant 구간 루프 (카운터 붕괴 시 폭주 지점)</td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1366-1367</code></td><td><code>lastSweep</code> 보정 <code>ArcAbs</code></td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1374-1375</code></td><td><code>_LastX/_LastY = obj.PF2</code> (물리 위치와 어긋날 수 있음)</td></tr>
<tr><td><code>SeqUploadWeldingList.cs:1220, 800</code></td><td>"이미 시작점인가" 판정 → 점프 생략 분기</td></tr>
<tr><td><code>ScannerParam.cs:331-341</code></td><td><code>ArcRampingInterval</code> — 초기값 없음(=0), 권장 0.1 mm</td></tr>
<tr><td><code>ScannerParam.cs:56-70</code></td><td><code>LaserOnDelay</code>=int(음수 가능), <code>LaserOffDelay</code>=uint</td></tr>
<tr><td><code>ScannerRTC6.cs:691-699</code></td><td>딜레이 단위 — 레이저 0.5 µs/bit, 스캐너 10 µs/bit</td></tr>
<tr><td><code>ScannerRTC6.cs:262-271</code></td><td><code>ArcAbs(x,y,angle)</code> — 각도 degree, CW가 +</td></tr>
<tr><td><code>ArcCircleTool.cs:905-924</code></td><td><code>SweepAngle</code> degree, <code>Length = R×Sweep×π/180</code> (정확값 존재)</td></tr>
</tbody></table>
</details>
</div>
<div id="tip"></div>
<script>
/* ═══════════ 테마 ═══════════ */
document.getElementById('themeBtn').onclick = () => {
const r = document.documentElement;
r.dataset.theme = r.dataset.theme === 'dark' ? 'light' : 'dark';
};
/* ═══════════ 툴팁 ═══════════ */
const tip = document.getElementById('tip');
document.addEventListener('mousemove', e => {
const c = e.target.closest('[data-tip]');
if (!c) { tip.style.opacity = 0; return; }
tip.innerHTML = c.dataset.tip;
tip.style.opacity = 1;
const r = tip.getBoundingClientRect();
let x = e.clientX + 16, y = e.clientY + 16;
if (x + r.width > innerWidth - 8) x = e.clientX - r.width - 16;
if (y + r.height > innerHeight - 8) y = e.clientY - r.height - 16;
tip.style.left = x + 'px'; tip.style.top = y + 'px';
});
/* ═══════════ 기하 헬퍼 ═══════════ */
const W = 236, H = 168; // 셀 viewBox
const ORIG = [26, 142]; // 필드 원점 (0,0)
const CX = 140, CY = 74, R = 40; // 기본 Arc 중심/반경
const D2R = Math.PI / 180;
const P = (cx, cy, r, deg) => [cx + r * Math.cos(deg * D2R), cy - r * Math.sin(deg * D2R)];
const fmt = a => a.map(v => v.toFixed(2)).join(' ');
function poly(pts) { return pts.map((p, i) => (i ? 'L' : 'M') + fmt(p)).join(' '); }
function arcPts(cx, cy, r, a0, a1, n) {
n = n || Math.max(10, Math.ceil(Math.abs(a1 - a0) / 2));
const out = [];
for (let i = 0; i <= n; i++) out.push(P(cx, cy, r, a0 + (a1 - a0) * i / n));
return out;
}
/* 와블: shape 0=ELLIPSE, 1=HORIZONTAL8, -1=VERTICAL8 */
function wobbleArcPts(cx, cy, r, a0, a1, ampT, ampL, cycles, shape) {
const n = 900, out = [];
for (let i = 0; i <= n; i++) {
const t = i / n, u = t * cycles * 2 * Math.PI, a = a0 + (a1 - a0) * t;
let dR, dA;
if (shape === 0) { dR = ampT * Math.sin(u); dA = ampL * Math.cos(u); }
else if (shape === 1) { dR = ampT * Math.sin(u); dA = ampL * Math.sin(2 * u); }
else { dR = ampT * Math.sin(2 * u); dA = ampL * Math.sin(u); }
const rr = r + dR, aa = a + (dA / Math.max(rr, 1)) * (180 / Math.PI);
out.push(P(cx, cy, rr, aa));
}
return out;
}
function wobbleLinePts(x0, y0, x1, y1, ampT, ampL, cycles, shape) {
const n = 700, out = [], dx = x1 - x0, dy = y1 - y0;
const L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L, nx = -uy, ny = ux;
for (let i = 0; i <= n; i++) {
const t = i / n, u = t * cycles * 2 * Math.PI;
let dT, dL;
if (shape === 0) { dT = ampT * Math.sin(u); dL = ampL * Math.cos(u); }
else if (shape === 1) { dT = ampT * Math.sin(u); dL = ampL * Math.sin(2 * u); }
else { dT = ampT * Math.sin(2 * u); dL = ampL * Math.sin(u); }
out.push([x0 + ux * (t * L + dL) + nx * dT, y0 + uy * (t * L + dL) + ny * dT]);
}
return out;
}
/* ═══════════ SVG 조각 ═══════════ */
const s = (t, at, inner) => `<${t} ${Object.entries(at).map(([k, v]) => `${k}="${v}"`).join(' ')}${inner === undefined ? '/>' : `>${inner}</${t}>`}`;
const MARK = d => s('path', { d, fill: 'none', stroke: 'var(--series-1)', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
const JUMP = d => s('path', { d, fill: 'none', stroke: 'var(--series-2)', 'stroke-width': 1.6, 'stroke-dasharray': '4 3', 'stroke-linecap': 'round' });
const BAD = d => s('path', { d, fill: 'none', stroke: 'var(--st-crit)', 'stroke-width': 3, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
const WOB = d => s('path', { d, fill: 'none', stroke: 'var(--series-3)', 'stroke-width': 1.2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
const PWR = (d, step) => s('path', { d, fill: 'none', stroke: `var(--p${step})`, 'stroke-width': 3.2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
function frame(label) {
let g = s('rect', { x: 8, y: 8, width: W - 16, height: H - 16, rx: 6, fill: 'none', stroke: 'var(--grid)', 'stroke-width': 1 });
// 원점 크로스
g += s('line', { x1: ORIG[0] - 7, y1: ORIG[1], x2: ORIG[0] + 7, y2: ORIG[1], stroke: 'var(--axis)', 'stroke-width': 1.4 });
g += s('line', { x1: ORIG[0], y1: ORIG[1] - 7, x2: ORIG[0], y2: ORIG[1] + 7, stroke: 'var(--axis)', 'stroke-width': 1.4 });
g += s('text', { x: ORIG[0] + 10, y: ORIG[1] + 4, fill: 'var(--muted)', 'font-size': 9 }, label || '원점(0,0)');
return g;
}
function dot(p, color) { return s('circle', { cx: p[0].toFixed(2), cy: p[1].toFixed(2), r: 3, fill: color, stroke: 'var(--surface-1)', 'stroke-width': 2 }); }
function xmark(p) {
const [x, y] = p, k = 4.5;
return s('g', { stroke: 'var(--st-crit)', 'stroke-width': 2.2, 'stroke-linecap': 'round' },
s('line', { x1: x - k, y1: y - k, x2: x + k, y2: y + k }) + s('line', { x1: x - k, y1: y + k, x2: x + k, y2: y - k }));
}
function note(x, y, t, color) { return s('text', { x, y, fill: color || 'var(--muted)', 'font-size': 9.5 }, t); }
/* ═══════════ 셀 빌더 ═══════════ */
function cell(spec) {
const tag = spec.tag ? `<span class="tag ${spec.tag[0]}">${spec.tag[1]}</span>` : '';
return `<figure class="cell${spec.ng ? ' ng' : ''}" data-tip="${spec.tip.replace(/"/g, '&quot;')}">
<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${spec.title}">${frame(spec.originLabel)}${spec.body}</svg>
<figcaption><div class="t">${spec.title}${tag}</div><div class="s">${spec.sub}</div></figcaption>
</figure>`;
}
const TIP = (title, rows) => `<b>${title}</b>` + rows.map(([k, v]) => `<span class="k">${k}</span> ${v}`).join('<br>');
/* ═══════════ 범례 ═══════════ */
document.getElementById('legend').innerHTML = [
['<svg width="34" height="10"><line x1="1" y1="5" x2="33" y2="5" stroke="var(--series-1)" stroke-width="2" stroke-linecap="round"/></svg>', '마크 = 용접 (레이저 ON, 의도됨)'],
['<svg width="34" height="10"><line x1="1" y1="5" x2="33" y2="5" stroke="var(--series-2)" stroke-width="1.6" stroke-dasharray="4 3" stroke-linecap="round"/></svg>', '점프 (레이저 OFF, 의도됨)'],
['<svg width="34" height="10"><line x1="1" y1="5" x2="33" y2="5" stroke="var(--st-crit)" stroke-width="3" stroke-linecap="round"/></svg>', '✕ 이상 발광 (레이저 ON, 의도 안 됨)'],
['<svg width="34" height="10"><path d="M1 5 Q5 1 9 5 T17 5 T25 5 T33 5" fill="none" stroke="var(--series-3)" stroke-width="1.2"/></svg>', '와블 궤적'],
['<svg width="52" height="10"><line x1="1" y1="5" x2="16" y2="5" stroke="var(--p250)" stroke-width="3.2" stroke-linecap="round"/><line x1="19" y1="5" x2="33" y2="5" stroke="var(--p450)" stroke-width="3.2" stroke-linecap="round"/><line x1="36" y1="5" x2="51" y2="5" stroke="var(--p650)" stroke-width="3.2" stroke-linecap="round"/></svg>', '파워 저 → 고'],
].map(([sv, t]) => `<span class="item">${sv}<span>${t}</span></span>`).join('');
/* ═══════════ 격자 A — 타입별 ═══════════ */
const A = [];
const jumpTo = p => JUMP(poly([ORIG, p]));
// 1 Line
{ const a = [80, 110], b = [200, 52];
A.push({ title: 'Line', sub: 'LoadLineScannerParam · :696', tag: ['ok', '지원'],
body: jumpTo(a) + MARK(poly([a, b])) + dot(a, 'var(--series-2)') + dot(b, 'var(--series-1)'),
tip: TIP('Line', [['경로', '점프 → 직선 마크 1회'], ['램핑', '지원 (거리 기반 분할)'], ['와블', '지원'], ['비고', 'PolyLine(LineEdit)도 이 분기']]) }); }
// 2 Dot
{ const a = [150, 84];
A.push({ title: 'Dot', sub: 'LoadDotScannerParam · :986', tag: ['ok', '지원'],
body: jumpTo(a) + s('circle', { cx: a[0], cy: a[1], r: 5.5, fill: 'none', stroke: 'var(--series-1)', 'stroke-width': 2.4 }) + dot(a, 'var(--series-1)') + note(a[0] + 12, a[1] + 4, 'SpotShotTime'),
tip: TIP('Dot', [['경로', '점프 → 정지 발광'], ['시간', 'SpotShotTime'], ['램핑', '해당 없음'], ['와블', '해당 없음']]) }); }
// 3 Spline
{ const pts = [];
for (let i = 0; i <= 120; i++) { const t = i / 120, ang = t * 720, r = 12 + t * 30; pts.push(P(CX, CY + 6, r, ang)); }
A.push({ title: 'Spline', sub: 'LoadSplineScannerParam · :1032', tag: ['ok', '지원'],
body: jumpTo(pts[0]) + MARK(poly(pts)) + dot(pts[0], 'var(--series-2)'),
tip: TIP('Spline', [['경로', '나선(토네이도) 마크'], ['방향', 'DrawWayType 4종'], ['램핑', '미적용'], ['와블', '미적용']]) }); }
// 4 SnowMan
{ const a = [86, 104], b = [190, 62];
const w = wobbleLinePts(a[0], a[1], b[0], b[1], 7, 3, 7, 1);
A.push({ title: 'SnowMan', sub: 'LoadSnowManScannerParam · :1132', tag: ['wr', '_Last 미갱신'],
body: jumpTo(a) + WOB(poly(w)) + MARK(poly([a, b])) + dot(a, 'var(--series-2)') + dot(b, 'var(--series-1)') + note(12, 24, '⚠ _LastX/_LastY 갱신 없음 (:1179)', 'var(--st-serious)'),
tip: TIP('SnowMan', [['경로', '와블 벡터(Head/Body) + 직선'], ['주의', '_LastX/_LastY를 갱신하지 않음'], ['영향', '다음 객체의 점프 생략 판정이 stale 좌표로'], ['현황', '현장 미사용']]) }); }
// 5 Arc
{ const pts = arcPts(CX, CY, R, 200, 340);
A.push({ title: 'Arc', sub: 'LoadArcScannerParam · :1183', tag: ['ok', '지원'],
body: jumpTo(pts[0]) + MARK(poly(pts)) + dot(pts[0], 'var(--series-2)') + dot(pts[pts.length - 1], 'var(--series-1)') + dot([CX, CY], 'var(--muted)') + note(CX - 12, CY - 8, 'center'),
tip: TIP('Arc', [['경로', '현재 위치에서 center 기준 Sweep° 회전'], ['포함', 'Circle (Arc 파생)'], ['램핑', '지원 — 본 문서의 핵심'], ['와블/스노우맨', '모두 지원']]) }); }
// 6 Arc3Point
{ const pts = arcPts(CX, CY + 10, R, 195, 345);
const p3 = [arcPts(CX, CY + 10, R, 270, 270)[0]];
A.push({ title: 'Arc3Point', sub: 'LoadArc3PScannerParam · :1384', tag: ['ok', '지원'],
body: jumpTo(pts[0]) + MARK(poly(pts)) + dot(pts[0], 'var(--series-2)') + dot(p3[0], 'var(--muted)') + dot(pts[pts.length - 1], 'var(--series-1)') + note(10, 24, '3점 통과 원호'),
tip: TIP('Arc3Point', [['경로', '3점을 지나는 원호'], ['키', 'Arc3P123 / Arc3P132'], ['직렬화명', 'Arc3p'], ['램핑', 'Arc와 별도 구현']]) }); }
// 7 ZigZag
{ const a = [76, 112], b = [198, 58], zz = [];
const n = 7;
for (let i = 0; i <= n; i++) {
const t = i / n, x = a[0] + (b[0] - a[0]) * t, y = a[1] + (b[1] - a[1]) * t;
zz.push([x, y + (i % 2 ? -13 : 13)]);
}
A.push({ title: 'ZigZag', sub: 'LoadZigzagScannerParam · :1585', tag: ['ok', '지원'],
body: jumpTo(zz[0]) + MARK(poly(zz)) + dot(zz[0], 'var(--series-2)') + dot(zz[zz.length - 1], 'var(--series-1)'),
tip: TIP('ZigZag', [['경로', '직선 + 원호 반복'], ['파라미터', 'ZigzagWobbel* 계열'], ['램핑', '미적용']]) }); }
// 8 Group
{ const l1 = [[70, 118], [136, 92]], l2 = [[136, 92], [196, 106]];
const ar = arcPts(150, 60, 26, 200, 350);
A.push({ title: 'Group', sub: 'LoadGroupScannerParam · :337', tag: ['ok', '지원'],
body: jumpTo(l1[0]) + MARK(poly(l1)) + MARK(poly(l2)) + JUMP(poly([l2[1], ar[0]])) + MARK(poly(ar)) + dot(l1[0], 'var(--series-2)') + note(10, 24, '부모 ScannerParam 상속 · MarkingCount 반복'),
tip: TIP('Group', [['경로', '서브객체를 순서대로 재귀 처리'], ['조건', '부모 객체의 ScannerParam 사용'], ['반복', 'MarkingCount 회'], ['램핑', 'GroupRamingUse=true 시 전체 길이 기준 램핑']]) }); }
document.getElementById('gridA').innerHTML = A.map(cell).join('');
/* ═══════════ 격자 B — Arc 조합 ═══════════ */
const A0 = 200, A1 = 290; // 90° 스윕 (화면상)
const arcBase = arcPts(CX, CY, R, A0, A1);
const START = arcBase[0], END = arcBase[arcBase.length - 1];
/* 램핑 구간을 파워 계단으로 그린다 */
function rampingBody(startFrac, endFrac, steps) {
const span = A1 - A0, out = [];
const n = steps || 5;
for (let i = 0; i < n; i++) {
const t0 = i / n, t1 = (i + 1) / n;
let lvl;
if (t1 <= startFrac) lvl = 250 + Math.round((t0 / Math.max(startFrac, 1e-6)) * 3) * 100;
else if (t0 >= 1 - endFrac) lvl = 650 - Math.round(((t0 - (1 - endFrac)) / Math.max(endFrac, 1e-6)) * 3) * 100;
else lvl = 650;
lvl = Math.max(250, Math.min(650, lvl));
out.push(PWR(poly(arcPts(CX, CY, R, A0 + span * t0, A0 + span * t1, 14)), lvl));
}
return out.join('');
}
const wobSmall = () => WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 3, 1.5, 14, 0)));
const wobLarge = () => WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 9, 5, 9, 0)));
const snowMan = () => WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 4, 11, 1)));
const rowsB = [
{ key: 'off', lab: '램핑 OFF', sub: 'Start=0 · End=0', interval: '—', se: '0 mm', theta: '—', total: '—', real: '90.0°', verdict: ['ok', 'OK'], why: '단일 ArcAbs (:1369)' },
{ key: 'ok', lab: '램핑 ON · 정상', sub: 'Interval 0.1 · 2+2 mm', interval: '0.1 mm', se: '4 mm', theta: '0.573°', total: '157', real: '90.0°', verdict: ['ok', 'OK'], why: 'rising 20 + const 117 + falling 20' },
{ key: 'over', lab: '램핑 ON · 거리 과대', sub: 'Interval 0.1 · 10+10 mm', interval: '0.1 mm', se: '20 mm', theta: '0.573°', total: '157', real: '≈114.6°', verdict: ['ng', '과회전'], why: 'rising100 + const0 + falling100 → 24.6° 초과' },
{ key: 'zero', lab: '램핑 ON · Interval 0', sub: 'Interval 0 · 10+10 mm', interval: '0 mm', se: '20 mm', theta: '0°', total: 'int.MinValue', real: '붕괴', verdict: ['ng', '리스트 폭주'], why: '0으로 나눗셈 → 21.5억 회 ArcAbs' },
];
const colsB = [
{ key: 'woff', lab: 'Wobble OFF', ov: () => '', p: 'WobbelEnable=false' },
{ key: 'wsm', lab: 'Wobble ON · 진폭 小', ov: wobSmall, p: 'Trans 0.3 / Long 0.15 mm · 150 Hz' },
{ key: 'wlg', lab: 'Wobble ON · 진폭 大', ov: wobLarge, p: 'Trans 1.5 / Long 0.8 mm · 100 Hz' },
{ key: 'snow', lab: 'SnowMan ON', ov: snowMan, p: 'SnowManEnable=true · Tilting 45°' },
];
let htmlB = '';
for (const r of rowsB) {
htmlB += `<div class="rowgrid" style="margin-bottom:12px">
<div class="rowlab">${r.lab}<span>${r.sub}</span></div>`;
for (const c of colsB) {
let body = '', ng = false, tag = r.verdict;
if (r.key === 'off') {
body = JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + c.ov();
} else if (r.key === 'ok') {
body = JUMP(poly([ORIG, START])) + rampingBody(2 / 15.71, 2 / 15.71, 8) + c.ov();
} else if (r.key === 'over') {
ng = true;
const overPts = arcPts(CX, CY, R, A1, A1 + 24.6);
body = JUMP(poly([ORIG, START])) + rampingBody(0.42, 0.42, 8) + c.ov()
+ BAD(poly(overPts)) + xmark(overPts[overPts.length - 1])
+ note(10, 24, '✕ 90° → 114.6° 초과 회전', 'var(--st-crit)')
+ note(10, 36, '_Last는 90° 지점으로 기록 → 다음 객체 오작동', 'var(--st-serious)');
} else {
ng = true;
body = JUMP(poly([ORIG, START]))
+ BAD(poly([END, [ORIG[0], ORIG[1]]]))
+ MARK(poly(arcBase)) + c.ov()
+ xmark(ORIG)
+ note(10, 24, '✕ 리스트 오버플로 → 원점(0,0)으로 마크', 'var(--st-crit)')
+ note(10, 36, '레이저 ON 상태로 원점 복귀 = 현장 현상', 'var(--st-crit)');
}
htmlB += cell({
title: c.lab, sub: `${r.sub}`, tag, ng, body,
tip: TIP(`${r.lab} × ${c.lab}`, [
['Arc', 'R 10 mm · Sweep 90° · CCW · 호길이 15.71 mm'],
['Interval', r.interval], ['Start+End', r.se],
['theta', r.theta], ['totalCount', r.total],
['실제 회전각', r.real], ['와블', c.p], ['판정 근거', r.why]])
});
}
htmlB += `</div>`;
}
document.getElementById('gridB').innerHTML = htmlB;
/* 표 뷰 */
document.querySelector('#tblB tbody').innerHTML = rowsB.flatMap(r => colsB.map(c => `<tr>
<td>${r.lab}</td><td>${c.lab}</td><td class="num">${r.interval}</td><td class="num">${r.se}</td>
<td class="num">${r.theta}</td><td class="num">${r.total}</td><td class="num">${r.real}</td>
<td><span class="tag ${r.verdict[0]}">${r.verdict[1]}</span></td><td>${r.why}</td></tr>`)).join('');
/* ═══════════ 격자 C — 값 크기 ═══════════ */
const rowsC = [
{ lab: 'Arc Ramping Interval', sub: '분할 간격', cells: [
{ t: '0 mm', tag: ['ng', 'NG'], ng: true, sub: 'theta=0 · 카운터 붕괴',
body: JUMP(poly([ORIG, START])) + BAD(poly([END, ORIG])) + MARK(poly(arcBase)) + xmark(ORIG) + note(10, 24, '✕ 0으로 나눗셈', 'var(--st-crit)'),
tip: [['값', '0 mm (기본값)'], ['theta', '0°'], ['결과', '(int)(Dist/0) = int.MinValue → 21.5억 회 루프'], ['조치', '0.1 mm로 설정']] },
{ t: '0.1 mm', tag: ['ok', '권장'], sub: '157분할 · 매끄러움',
body: JUMP(poly([ORIG, START])) + rampingBody(2 / 15.71, 2 / 15.71, 10),
tip: [['값', '0.1 mm (툴팁 권장값)'], ['theta', '0.573°'], ['분할', '157'], ['결과', '파워 계단이 육안으로 안 보임']] },
{ t: '2.0 mm', tag: ['wr', '거침'], sub: '8분할 · 계단 발생',
body: JUMP(poly([ORIG, START])) + rampingBody(0.25, 0.25, 4),
tip: [['값', '2.0 mm'], ['theta', '11.46°'], ['분할', '8'], ['결과', '파워 계단이 비드에 보일 수 있음']] },
]},
{ lab: 'Ramping 거리 합', sub: 'Start + End', cells: [
{ t: '4 mm', tag: ['ok', '권장'], sub: '호길이 15.71의 25%',
body: JUMP(poly([ORIG, START])) + rampingBody(2 / 15.71, 2 / 15.71, 10),
tip: [['Start+End', '4 mm'], ['호길이', '15.71 mm'], ['비율', '25%'], ['결과', 'rising20 + const117 + falling20 = 157 ✓']] },
{ t: '8 mm', tag: ['ok', '상한'], sub: '호길이의 50%',
body: JUMP(poly([ORIG, START])) + rampingBody(0.25, 0.25, 10),
tip: [['Start+End', '8 mm'], ['비율', '51%'], ['결과', '정출력 구간이 거의 사라짐 — 실용 상한']] },
{ t: '20 mm', tag: ['ng', 'NG'], ng: true, sub: '호길이 초과 → 과회전',
body: JUMP(poly([ORIG, START])) + rampingBody(0.42, 0.42, 8) + BAD(poly(arcPts(CX, CY, R, A1, A1 + 24.6))) + xmark(arcPts(CX, CY, R, A1 + 24.6, A1 + 24.6)[0]) + note(10, 24, '✕ 24.6° 초과 회전', 'var(--st-crit)'),
tip: [['Start+End', '20 mm'], ['호길이', '15.71 mm — 초과'], ['가드', 'R×|Sweep°|=900 과 비교해서 통과해버림'], ['결과', '114.6° 회전 + _Last 불일치']] },
]},
{ lab: 'LaserOnDelay', sub: '음수 = 선행 발광', cells: [
{ t: '−1800 µs', tag: ['wr', '현장값'], sub: '점프 끝 1.8 mm 발광',
body: (() => { const p = [ORIG, START]; const pre = [[START[0] - (START[0] - ORIG[0]) * 0.18, START[1] - (START[1] - ORIG[1]) * 0.18], START];
return JUMP(poly(p)) + BAD(poly(pre)) + MARK(poly(arcBase)) + note(10, 24, '✕ 점프 구간 선행 발광', 'var(--st-crit)'); })(),
tip: [['값', '−1800 µs'], ['JumpSpeed', '1000 mm/s'], ['선행 발광', '1.8 mm'], ['비고', 'JumpDelay=0이면 미러 이동 중 발광']] },
{ t: '−200 µs', tag: ['ok', '권장'], sub: '0.2 mm — 시작점 보정용',
body: (() => { const pre = [[START[0] - (START[0] - ORIG[0]) * 0.03, START[1] - (START[1] - ORIG[1]) * 0.03], START];
return JUMP(poly([ORIG, START])) + BAD(poly(pre)) + MARK(poly(arcBase)); })(),
tip: [['값', '−200 µs'], ['선행 발광', '0.2 mm'], ['용도', '시작부 언더필 방지']] },
{ t: '0 µs', tag: ['ok', '기본'], sub: '선행 발광 없음',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)),
tip: [['값', '0 µs'], ['선행 발광', '없음'], ['비고', '시작부 출력 상승 지연이 보이면 음수로 조정']] },
]},
{ lab: 'Wobble 진폭', sub: 'Transversal', cells: [
{ t: '0.3 mm', tag: ['ok', '좁음'], sub: '비드폭 ≈ 스팟 + 0.6',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 3, 1.5, 16, 0))),
tip: [['Transversal', '0.3 mm'], ['Longitudinal', '0.15 mm'], ['Shape', 'ELLIPSE'], ['용도', '갭이 작을 때']] },
{ t: '1.0 mm', tag: ['ok', '표준'], sub: '갭 브리징 표준',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 12, 0))),
tip: [['Transversal', '1.0 mm'], ['Longitudinal', '0.5 mm'], ['Shape', 'ELLIPSE']] },
{ t: '3.0 mm', tag: ['wr', '주의'], sub: '입열 분산 · 관통 저하',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 14, 7, 8, 0))),
tip: [['Transversal', '3.0 mm'], ['주의', '진폭이 커질수록 단위면적 입열 감소'], ['동반 조정', 'MarkSpeed↓ 또는 Power↑']] },
]},
{ lab: 'Wobble 주파수', sub: 'vs MarkSpeed', cells: [
{ t: '50 Hz', tag: ['wr', '성김'], sub: '0.5 loop/mm — 흔적 보임',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 4, 0))),
tip: [['Frequency', '50 Hz'], ['MarkSpeed', '100 mm/s'], ['밀도', '0.5 loop/mm'], ['결과', '와블 궤적이 비드에 그대로 남음']] },
{ t: '150 Hz', tag: ['ok', '권장'], sub: '1.5 loop/mm',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 12, 0))),
tip: [['Frequency', '150 Hz'], ['MarkSpeed', '100 mm/s'], ['밀도', '1.5 loop/mm'], ['결과', '균일한 비드']] },
{ t: '300 Hz', tag: ['ok', '조밀'], sub: '3.0 loop/mm — 스캐너 한계',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 24, 0))),
tip: [['Frequency', '300 Hz (최대)'], ['밀도', '3.0 loop/mm'], ['주의', '고주파에서 실제 진폭이 설정보다 작아질 수 있음']] },
]},
{ lab: 'PolygonDelay', sub: 'Arc 분절 간 정정', cells: [
{ t: '0 µs', tag: ['wr', '현장값'], sub: '정정 대기 없음 · 오버슈트',
body: (() => { const p = arcPts(CX, CY, R, A0, A1, 12).map((q, i) => [q[0] + (i % 2 ? 2.2 : -2.2), q[1] + (i % 2 ? -1.6 : 1.6)]);
return JUMP(poly([ORIG, START])) + MARK(poly(p)) + note(10, 24, '분절 경계에서 궤적 흔들림', 'var(--st-serious)'); })(),
tip: [['값', '0 µs'], ['영향', 'Arc 분절 사이에 정정 대기 없음'], ['증상', '원호 오버슈트 / 각짐']] },
{ t: '50 µs', tag: ['ok', '권장'], sub: '균형',
body: (() => { const p = arcPts(CX, CY, R, A0, A1, 12).map((q, i) => [q[0] + (i % 2 ? 0.6 : -0.6), q[1]]);
return JUMP(poly([ORIG, START])) + MARK(poly(p)); })(),
tip: [['값', '50~100 µs'], ['효과', '분절 경계 정정'], ['비용', '분절 수 × 50 µs 만큼 T/T 증가']] },
{ t: '500 µs', tag: ['wr', '과대'], sub: 'T/T 급증 · 입열 집중',
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + s('g', {}, arcPts(CX, CY, R, A0, A1, 12).map(q => dot(q, 'var(--st-serious)')).join('')) + note(10, 24, '분절마다 정지 → 입열 집중', 'var(--st-serious)'),
tip: [['값', '500 µs'], ['분절 157개 기준', 'T/T +78 ms'], ['위험', '정지 지점마다 과입열 → 언더컷/버닝']] },
]},
];
document.getElementById('gridC').innerHTML = rowsC.map(r => `
<div class="rowgrid" style="grid-template-columns:132px repeat(3,minmax(0,1fr)); margin-bottom:12px">
<div class="rowlab">${r.lab}<span>${r.sub}</span></div>
${r.cells.map(c => cell({ title: c.t, sub: c.sub, tag: c.tag, ng: c.ng, body: c.body,
tip: TIP(`${r.lab} = ${c.t}`, c.tip) })).join('')}
</div>`).join('');
</script>
</body>
</html>