- 신규 팝업 cad-editor-app.html + cad-editor-view.js: Three.js 3D 뷰포트(원점 그리드+축), 워크스페이스 STL 로드/측정, 변환(이동·회전·배율·뒤집기·거울·중앙정렬), 구멍(표면 클릭 픽), 박스 컷, 두 STL 합/차/교집합, 새 파일로 저장. - 좌표계: 모델 원본 프레임 유지(슬라이서처럼 베드 중앙정렬 안 함). STL Z-up↔three Y-up 매핑 = rotateX(-90°), 역매핑 STL=(threeX,-threeZ,threeY). 변환은 클라이언트 bake, boolean은 bake→서버 OpenSCAD→결과 바이트 재로드(변환 리셋). - stl-cad-core: applyBooleanOp(op∈hole/cut/union/difference/intersection) 추가, addHoleToStl를 그 위의 얇은 래퍼로 리팩터(채팅도구 하위호환 유지). - routes-cad: GET /api/cad/file(원본 다운로드, dotfiles:allow), POST /api/cad/save(bake된 STL 저장), POST /api/cad/apply(바디=primary 바이트, ?op=¶ms, ?second=) 추가. 기존 list/info/render/ add-hole은 건드리지 않음. - k2-app: "🔧 STL 확인" 버튼 → "🔧 STL 편집" 팝업(openCadEditorPopup). 옛 PNG 모달 마크업·JS·CSS 제거. Co-Authored-By: Claude Code <noreply@anthropic.com>
167 lines
10 KiB
HTML
167 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>🖨️ K2 콤보</title>
|
|
<link rel="icon" type="image/png" sizes="64x64" href="/homeclaw_logo.png">
|
|
<link rel="stylesheet" href="../css/styles.css">
|
|
<script>try{var _t=localStorage.getItem('homeclaw_theme')||'dark';document.documentElement.setAttribute('data-theme',_t);}catch(e){}</script>
|
|
<style>
|
|
:root{--brand:#f97316;}
|
|
html,body{height:100%;margin:0;padding:0;}
|
|
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;display:flex;flex-direction:column;overflow:hidden;height:100dvh;}
|
|
|
|
.hdr{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--panel);border-bottom:1px solid var(--line);flex-shrink:0;}
|
|
.hdr h1{font-size:13px;font-weight:700;margin:0;}
|
|
.hdr-sp{flex:1;}
|
|
.hdr-btn{background:none;border:1.5px solid var(--line);border-radius:6px;padding:3px 9px;font-size:11px;color:var(--muted);cursor:pointer;font-family:inherit;transition:.15s;text-decoration:none;display:inline-flex;align-items:center;}
|
|
.hdr-btn:hover{border-color:var(--brand);color:var(--brand);}
|
|
|
|
.body-wrap{flex:1;min-height:0;display:flex;flex-direction:column;}
|
|
.cam-bar{flex-shrink:0;background:#000;display:flex;justify-content:center;position:relative;max-height:32vh;}
|
|
.cam-bar video{max-width:100%;max-height:32vh;display:block;background:#000;}
|
|
.cam-status{position:absolute;top:6px;right:10px;font-size:11px;color:#9ca3af;background:rgba(0,0,0,.5);padding:2px 8px;border-radius:6px;}
|
|
.cam-empty{color:var(--muted);font-size:12px;padding:16px;text-align:center;}
|
|
|
|
.frame-wrap{flex:1;min-height:0;position:relative;}
|
|
.frame-wrap iframe{width:100%;height:100%;border:none;display:block;}
|
|
.err{display:none;position:absolute;inset:0;align-items:center;justify-content:center;flex-direction:column;gap:10px;background:var(--bg);color:var(--muted);font-size:13px;text-align:center;padding:20px;}
|
|
.err.show{display:flex;}
|
|
.err a{color:var(--brand);}
|
|
|
|
/* ── 슬라이싱/CAD 공통 모달 클래스 ── */
|
|
.slice-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:50;align-items:center;justify-content:center;padding:16px;}
|
|
.slice-overlay.show{display:flex;}
|
|
.slice-modal{width:100%;max-width:520px;max-height:92vh;overflow-y:auto;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;}
|
|
.slice-modal h2{font-size:14px;margin:0 0 12px;}
|
|
.slice-field{margin-bottom:10px;}
|
|
.slice-field label{display:block;font-size:11px;color:var(--muted);margin-bottom:3px;}
|
|
.slice-field input[type=file],.slice-field input[type=number],.slice-field input[type=text],.slice-field select{width:100%;box-sizing:border-box;background:var(--panel-2);border:1px solid var(--line);border-radius:6px;padding:6px 8px;font-size:12px;color:var(--text);font-family:inherit;outline:none;}
|
|
.slice-field input:focus,.slice-field select:focus{border-color:var(--brand);}
|
|
.slice-row{display:flex;gap:8px;}
|
|
.slice-row .slice-field{flex:1;}
|
|
.slice-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text);margin-bottom:8px;}
|
|
.slice-btn{background:var(--brand);border:none;border-radius:8px;padding:9px 14px;font-size:13px;color:#fff;cursor:pointer;font-family:inherit;font-weight:700;}
|
|
.slice-btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
.slice-close{float:right;background:none;border:none;font-size:16px;color:var(--muted);cursor:pointer;}
|
|
.slice-result{margin-top:12px;padding:10px;background:var(--panel-2);border-radius:8px;font-size:12px;line-height:1.7;display:none;}
|
|
.slice-result.show{display:block;}
|
|
.slice-err{margin-top:10px;color:#ef4444;font-size:12px;display:none;}
|
|
.slice-err.show{display:block;}
|
|
|
|
/* ── 3D 슬라이서·STL 편집기 모두 전용 팝업창으로 분리(slicer-app.html / cad-editor-app.html). k2-app엔 오버레이 없음 ── */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="hdr">
|
|
<a href="/" style="text-decoration:none;font-size:13px;color:var(--muted)">← 홈</a>
|
|
<h1>🖨️ K2 콤보</h1>
|
|
<span class="hdr-sp"></span>
|
|
<button class="hdr-btn" onclick="openSlicerPopup()">🔪 슬라이싱</button>
|
|
<button class="hdr-btn" onclick="openCadEditorPopup()">🔧 STL 편집</button>
|
|
<button class="hdr-btn" onclick="reconnectCam()">📷 카메라 재연결</button>
|
|
<button class="hdr-btn" onclick="reloadFrame()">↺ 새로고침</button>
|
|
<a class="hdr-btn" href="https://print.applecherry.net/" target="_blank">↗ 새 탭</a>
|
|
<button class="hdr-btn" onclick="toggleTheme()">🌙</button>
|
|
</header>
|
|
|
|
<div class="body-wrap">
|
|
<div class="cam-bar" id="cam-bar">
|
|
<video id="k2-cam" autoplay muted playsinline></video>
|
|
<span class="cam-status" id="cam-status">연결 중...</span>
|
|
</div>
|
|
<div class="frame-wrap">
|
|
<iframe id="k2-frame" src="https://print.applecherry.net/" title="Fluidd"></iframe>
|
|
<div class="err" id="k2-err">
|
|
<div>Fluidd를 불러오지 못했습니다 (print.applecherry.net).</div>
|
|
<div><a href="https://print.applecherry.net/" target="_blank">새 탭에서 직접 열기 ↗</a></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ── 슬라이서 팝업 — Cura 스타일 3D GUI는 전용창(slicer-app.html)으로 분리 ──
|
|
// k2-app(카메라+Fluidd 모니터)과 슬라이서(편집 도구) 역할 분리. 인증은 같은 오리진
|
|
// smallclaw_session 쿠키가 팝업에 자동 상속되어 별도 토큰 전달 불필요.
|
|
function openSlicerPopup(){
|
|
const w=1320,h=860;
|
|
const left=Math.max(0,(screen.width-w)/2), top=Math.max(0,(screen.height-h)/2);
|
|
window.open('/html/slicer-app.html','k2-slicer',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
|
}
|
|
// ── STL 편집기 팝업 — Three.js 3D 편집기(cad-editor-app.html)로 분리 ──
|
|
// 옛 "STL 확인"은 서버 렌더 PNG 정적 미리보기 + 구멍뚫기였으나, 3D 상호작용 편집기로 전환.
|
|
// 인증은 same-origin smallclaw_session 쿠키가 팝업에 자동 상속(slicer 팝업과 동일).
|
|
function openCadEditorPopup(){
|
|
const w=1320,h=860;
|
|
const left=Math.max(0,(screen.width-w)/2), top=Math.max(0,(screen.height-h)/2);
|
|
window.open('/html/cad-editor-app.html','cad-editor',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
|
}
|
|
function toggleTheme(){const d=document.documentElement;const n=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',n);try{localStorage.setItem('homeclaw_theme',n);}catch{}}
|
|
function reloadFrame(){const f=document.getElementById('k2-frame');f.src=f.src;}
|
|
let loaded=false;
|
|
document.getElementById('k2-frame').addEventListener('load',()=>{loaded=true;});
|
|
setTimeout(()=>{ if(!loaded) document.getElementById('k2-err').classList.add('show'); },8000);
|
|
|
|
// ── K2 콤보 내장 카메라(자체 WebRTC 시그널링, 프린터 펌웨어가 직접 서빙) ──
|
|
// 프린터 자체는 :8000/call/webrtc_local 로 SDP offer를 POST하면 answer를 돌려주는
|
|
// 평문 HTTP 시그널링(Moonraker webcam 목록엔 안 잡힘 — 크리얼리티 자체 프로토콜).
|
|
// 홈클로는 https://ai.applecherry.net 로 서빙되므로 브라우저에서 직접 http://로
|
|
// fetch(POST)하면 mixed-content로 막힘("주의 요함" + 카메라 검은 화면, 2026-09-18
|
|
// 발견) — 그래서 게이트웨이의 /api/k2/webrtc-signal(같은 오리진, HTTPS)을 거쳐
|
|
// 서버가 대신 그 요청을 해준다.
|
|
function authH(){const t=(function(){try{return sessionStorage.getItem('smallclaw_token')||localStorage.getItem('smallclaw_token')||'';}catch{return '';}})();return t?{'Authorization':'Bearer '+t}:{};}
|
|
let camPC=null;
|
|
function setStatus(t){const s=document.getElementById('cam-status'); if(s) s.textContent=t;}
|
|
function connectCam(){
|
|
if(camPC){ try{camPC.close();}catch(e){} }
|
|
setStatus('연결 중...');
|
|
// STUN을 빼면(host candidate만) 이 프린터의 ICE 응답이 실제로는 안 옴(DTLS
|
|
// failed, bytesReceived 0) — 크리얼리티 데모 코드 그대로 STUN 유지해야 정상
|
|
// 동작함(2026-09-18 확인, 원인 불명이나 재현됨).
|
|
const pc=new RTCPeerConnection({iceServers:[{urls:'stun:stun.l.google.com:19302'}]});
|
|
camPC=pc;
|
|
pc.ontrack=(ev)=>{
|
|
const v=document.getElementById('k2-cam');
|
|
v.srcObject=ev.streams[0];
|
|
setStatus('실시간');
|
|
};
|
|
pc.oniceconnectionstatechange=()=>{
|
|
if(pc===camPC && (pc.iceConnectionState==='failed'||pc.iceConnectionState==='disconnected')){
|
|
setStatus('끊김');
|
|
}
|
|
};
|
|
// 원래는 ev.candidate===null(ICE gathering 완전 종료)까지 기다린 뒤에만 오퍼를
|
|
// 보냈는데, 이 네트워크에서 그 완전종료 신호가 20~30초씩 걸림(가상/VPN 인터페이스들을
|
|
// 하나씩 훑고 타임아웃되는 걸로 보임) — 정작 이 프린터가 필요로 하는 STUN srflx
|
|
// 후보 자체는 보통 1~2초 안에 잡힘. 그래서 완전종료를 기다리지 않고 짧은 타임아웃
|
|
// 뒤에 그때까지 모인 후보로 바로 보낸다(2026-09-19, 20초+ 연결지연 확인 후 수정).
|
|
let offerSent=false;
|
|
function sendOfferOnce(){
|
|
if(offerSent || pc!==camPC) return;
|
|
offerSent=true;
|
|
fetch('/api/k2/webrtc-signal',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'text/plain', ...authH()},
|
|
body:btoa(JSON.stringify({type:'offer',sdp:pc.localDescription.sdp})),
|
|
}).then(r=>r.text()).then(txt=>{
|
|
const res=JSON.parse(atob(txt));
|
|
if(res.type==='answer' && pc===camPC){
|
|
pc.setRemoteDescription(new RTCSessionDescription(res));
|
|
}
|
|
}).catch(()=>{ if(pc===camPC) setStatus('연결 실패'); });
|
|
}
|
|
pc.onicecandidate=(ev)=>{
|
|
if(ev.candidate===null) sendOfferOnce(); // 빨리 끝나면 그대로 씀
|
|
};
|
|
pc.addTransceiver('video',{direction:'sendrecv'});
|
|
pc.createOffer().then(d=>pc.setLocalDescription(d)).then(()=>{
|
|
setTimeout(sendOfferOnce, 1500); // 안 끝나면 1.5초 뒤 그때까지 모인 후보로 진행
|
|
}).catch(()=>{ if(pc===camPC) setStatus('연결 실패'); });
|
|
}
|
|
function reconnectCam(){ connectCam(); }
|
|
connectCam();
|
|
</script>
|
|
</body>
|
|
</html>
|