그동안 미추적으로 쌓여 있던 인수인계 문서를 정리해 올린다. 260903 Door 인터락 현장검증 가이드 / 파워캘 3000W 오적용 환산 260907 Arc 출사 파라미터 시각화 / LaserFrequency 단위계통 / NRE 위험구간 점검 / 레시피화면 사용설명서 260915 레시피화면 작업매뉴얼 260916 PTOP 프로그램 사용설명서 / 자동운전 전환 사전점검 현장용 작업 중 임시본(_backup)은 제외했다.
662 lines
44 KiB
HTML
662 lines
44 KiB
HTML
<!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, '"')}">
|
||
<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>
|