feat: 도면 버튼 — 작업실 STL 카드·STL 편집기에서 3면도 즉시 생성
- 작업실 파일 카드(STL)에 📐 버튼: /api/cad/render?view=three 를 fetch→blob으로 생성해 새 탭에 표시(Authorization 헤더 필요라 단순 링크 불가). 생성 중 표시 후 결과로 교체. 카드 하단 좌측 오버레이 버튼. - STL 편집기 파일 행에 📐 버튼(로드 없이 선택만으로 도면 가능). - 3면도 실루엣 판정 수정: 옛 '모서리 색과의 차이' 방식은 원통처럼 뷰가 사각형을 못 채우는 부품에서 bbox 모서리에 옛 배경(cream)이 사각형으로 남았다 — 흰 시트 기준 임계로 판정하고 크롭 내부 잔여 배경도 흰색으로 통일(2026-09-25 turntable 실측). 참고: PIL floodfill은 fill 값과 배경이 thresh 이내면 실행 자체를 안 함. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -44,12 +44,20 @@ W, D, H = float(dims['w']), float(dims['d']), float(dims['h'])
|
||||
def silhouette(p):
|
||||
im = Image.open(p).convert('RGB')
|
||||
a = np.asarray(im).astype(int)
|
||||
bg = a[2, 2]
|
||||
m = (np.abs(a - bg).sum(axis=2) > 20)
|
||||
# Cornfield 배경(cream 255,255,229)과 물체(gold)의 명도 차이가 크므로 "흰 시트에서 충분히
|
||||
# 다른 픽셀 = 실루엣"으로 잡는다. 옛 방식(모서리(2,2) 색과의 차이)은 원통 같은 부품에서
|
||||
# bbox 사각형 모서리에 옛 배경색이 그대로 남았다(2026-09-25 turntable 실측).
|
||||
m = (np.abs(a - 255).sum(axis=2) > 60)
|
||||
ys, xs = np.nonzero(m)
|
||||
if len(xs) < 5:
|
||||
return None
|
||||
return im.crop((int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1))
|
||||
c = im.crop((int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1))
|
||||
# 크롭 안에 남은 옛 배경(물체에 둘러싸인 영역 포함)은 흰색으로 통일 — 시트 위에 사각형이 그대로 보이는 것을 막는다.
|
||||
ca = np.asarray(c)
|
||||
cm = (np.abs(ca.astype(int) - 255).sum(axis=2) > 60)
|
||||
out = np.full(ca.shape, 255, np.uint8)
|
||||
out[cm] = ca[cm]
|
||||
return Image.fromarray(out)
|
||||
|
||||
crops = {}
|
||||
for key in ('front', 'top', 'right'):
|
||||
|
||||
@@ -284,6 +284,9 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
숨어있다 호버 시 보이되, 체크된 상태면 계속 보임(선택 중임을 표시). */
|
||||
.rb-file-stl-sel{position:absolute;top:4px;left:4px;z-index:2;width:16px;height:16px;cursor:pointer;opacity:0;transition:.15s;accent-color:#f59e0b;}
|
||||
.rb-file-card:hover .rb-file-stl-sel,.rb-file-stl-sel:checked{opacity:1;}
|
||||
.rb-file-draw{position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55);border:none;border-radius:5px;color:#fff;font-size:11px;padding:2px 6px;cursor:pointer;opacity:0;transition:.15s;}
|
||||
.rb-file-card:hover .rb-file-draw{opacity:1;}
|
||||
.rb-file-draw:hover{background:#f97316;}
|
||||
.rb-file-card.stl-selected{outline:2px solid #f59e0b;outline-offset:-1px;}
|
||||
|
||||
/* 지연 로딩 모듈을 받는 동안 탭 패널을 흐리게 하고 클릭을 막는다 */
|
||||
|
||||
@@ -106,9 +106,12 @@ function renderFileCard(file){
|
||||
// STL만 슬라이서로 보낼 다중선택 체크박스(09-22) — 여러 부품 STL을 한 판에 얹어 같이 슬라이싱.
|
||||
const checked=stlSelection.has(file.relPath);
|
||||
const selBox=isStl?`<input type="checkbox" class="rb-file-stl-sel" title="슬라이서로 보내기 선택" ${checked?'checked':''} onclick="event.stopPropagation()" onchange="toggleStlSelect('${escJs(file.relPath)}',this.checked)">`:'';
|
||||
// STL 전용 도면 버튼(09-25) — OpenSCAD 3면도(외곽 치수선) 새 탭 생성.
|
||||
const drawBtn=isStl?`<button class="rb-file-draw" onclick="openDrawingForFile('${escJs(file.relPath)}')" title="3면도 도면 생성">📐</button>`:'';
|
||||
return `<div class="rb-file-card${checked?' stl-selected':''}" data-rel="${escAttr(file.relPath)}">
|
||||
${selBox}
|
||||
<button class="rb-file-del" onclick="deleteFile('${escJs(file.relPath)}')" title="삭제">✕</button>
|
||||
${drawBtn}
|
||||
<a ${openAttrs}>
|
||||
<div class="rb-file-thumb">${thumb}</div>
|
||||
<div class="rb-file-info">
|
||||
@@ -137,6 +140,35 @@ function openCadEditorForFile(relPath){
|
||||
window.open('/html/cad-editor-app.html?path='+encodeURIComponent(workspacePath),'cad-editor',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
||||
}
|
||||
|
||||
// STL 카드의 "📐" — OpenSCAD 3면도(외곽 치수선 도면)를 생성해 새 탭으로 보여준다(09-25).
|
||||
// /api/cad/render는 Authorization 헤더가 필요하므로 fetch→blob→objectURL로 연다(단순 링크 안 됨).
|
||||
// 팝업은 동기으로 먼저 열어 팝업차단을 피하고, 생성 중 표시 후 결과로 교체.
|
||||
function openDrawingForFile(relPath){
|
||||
const workspacePath=workshopFilePath(relPath);
|
||||
if(!workspacePath)return;
|
||||
const w=1100,h=940;
|
||||
const left=Math.max(0,(screen.width-w)/2), top=Math.max(0,(screen.height-h)/2);
|
||||
const win=window.open('','cad-drawing',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
||||
if(!win){alert('팝업이 차단됐습니다. 팝업 허용 후 다시 시도하세요.');return;}
|
||||
win.document.write('<!doctype html><html><head><meta charset="utf-8"><title>도면 생성 중…</title></head><body style="margin:0;font-family:sans-serif;background:#1a1a1a;color:#eee;display:flex;align-items:center;justify-content:center;height:100vh">📐 도면 생성 중… (OpenSCAD 3면도, 몇 초 걸림)</body></html>');
|
||||
(async()=>{
|
||||
try{
|
||||
const r=await fetch('/api/cad/render?path='+encodeURIComponent(workspacePath)+'&view=three',{headers:authH()});
|
||||
if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d.error||('HTTP '+r.status));}
|
||||
const url=URL.createObjectURL(await r.blob());
|
||||
win.document.title='3면도';
|
||||
win.document.body.style.cssText='margin:0;background:#2a2a2a;display:flex;align-items:center;justify-content:center;height:100vh';
|
||||
win.document.body.innerHTML='';
|
||||
const img=win.document.createElement('img');
|
||||
img.src=url;img.style.cssText='max-width:100%;max-height:100%;object-fit:contain;';
|
||||
win.document.body.appendChild(img);
|
||||
}catch(e){
|
||||
win.document.body.style.cssText='margin:0;font-family:sans-serif;background:#1a1a1a;color:#ef4444;display:flex;align-items:center;justify-content:center;height:100vh';
|
||||
win.document.body.textContent='도면 생성 실패: '+e.message;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// ── 슬라이서 연동(09-22) ────────────────────────────────────────────────────
|
||||
// STL 카드 체크박스로 여러 부품(예: 스캐너 rig의 base_plate+head_plate+turntable 등)을
|
||||
// 골라 K2 슬라이서 팝업(slicer-app.html)에 한 번에 올린다. 실제 로드는 slicer-view.js의
|
||||
|
||||
@@ -441,6 +441,22 @@
|
||||
} catch (e) { setErr('저장 실패: ' + e.message); }
|
||||
}
|
||||
|
||||
// ── 도면(3면도) ───────────────────────────────────────────────────────
|
||||
// 선택된 STL의 제작용 3면도(외곽 치수선)를 서버(OpenSCAD)에서 생성해 새 탭으로 연다.
|
||||
// /api/cad/render는 Authorization 헤더가 필요하므로 fetch→blob→objectURL.
|
||||
async function openDrawing() {
|
||||
const p = panelEl.querySelector('#ce-file').value;
|
||||
if (!p) { setErr('파일을 먼저 선택하세요.'); return; }
|
||||
setErr(''); setInfo('📐 도면 생성 중… (OpenSCAD 3면도, 몇 초 걸림)');
|
||||
try {
|
||||
const r = await fetch('/api/cad/render?path=' + encodeURIComponent(p) + '&view=three', { headers: authH() });
|
||||
if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.error || ('HTTP ' + r.status)); }
|
||||
const url = URL.createObjectURL(await r.blob());
|
||||
window.open(url, '_blank');
|
||||
setInfo('📐 3면도를 새 탭으로 열었습니다.');
|
||||
} catch (e) { setErr('도면 생성 실패: ' + e.message); }
|
||||
}
|
||||
|
||||
// ── UI 구축 ───────────────────────────────────────────────────────────
|
||||
function setErr(msg) {
|
||||
const el = barEl.querySelector('#ce-err');
|
||||
@@ -465,6 +481,7 @@
|
||||
<div class="ce-row">
|
||||
<select id="ce-file" class="ce-grow"></select>
|
||||
<button class="ce-mini" onclick="window.__cadRefresh()">↺</button>
|
||||
<button class="ce-mini" onclick="window.__cadDrawing()">📐</button>
|
||||
<button class="ce-mini primary" onclick="window.__cadLoad()">로드</button>
|
||||
</div>
|
||||
<div id="ce-dims" class="ce-dims"><span class="ce-muted">모델 없음</span></div>
|
||||
@@ -639,6 +656,7 @@
|
||||
|
||||
// 글로벌 노출(인라인 onclick).
|
||||
window.__cadRefresh = loadFileList;
|
||||
window.__cadDrawing = openDrawing;
|
||||
window.__cadLoad = loadSelected;
|
||||
window.__cadFlip = flipAxis;
|
||||
window.__cadMirror = mirrorAxis;
|
||||
|
||||
Reference in New Issue
Block a user