feat: 작업실 앱 파일 탭에 K2 슬라이서 연동
- slicer-view.js: ?load=<경로1,경로2,...> URL 파라미터 지원 추가 — cad-editor-view.js의 ?path= 패턴과 동일한 취지로, 페이지 진입 시 지정한 워크스페이스 STL들을 /api/cad/file로 받아 자동으로 슬라이서 판에 배치한다 - workshop-app.html: 파일 탭 STL 카드에 다중선택 체크박스 추가 + "선택 STL 슬라이서로" 툴바 버튼 — 체크한 부품 STL들을 K2 슬라이서 팝업(slicer-app.html)에 한 번에 올려 같은 판에서 배치/슬라이싱 기존 slicer-app.html/PrusaSlicer 파이프라인은 그대로 재사용, 새 백엔드 라우트 없이 routes-cad.ts의 /api/cad/file만 재사용. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -184,6 +184,11 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
.rb-file-card:hover .rb-file-del{opacity:1;}
|
||||
.rb-file-del:hover{background:#ef4444;}
|
||||
.rb-file-card a{display:block;color:inherit;text-decoration:none;}
|
||||
/* STL 카드 전용 — 슬라이서로 보낼 파일 다중선택 체크박스(좌상단). 평소엔 델버튼처럼
|
||||
숨어있다 호버 시 보이되, 체크된 상태면 계속 보임(선택 중임을 표시). */
|
||||
.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-card.stl-selected{outline:2px solid #f59e0b;outline-offset:-1px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -255,6 +260,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
<select id="upload-folder-select"></select>
|
||||
<button class="hdr-btn" onclick="promptNewFolder()">+ 새 폴더</button>
|
||||
<label class="rb-upload-btn">📎 파일 업로드<input type="file" id="file-input" multiple style="display:none" onchange="handleFileInputChange(event)"></label>
|
||||
<button class="hdr-btn" id="stl-slice-btn" onclick="openSlicerForSelected()" disabled>🔪 선택 STL 슬라이서로 (<span id="stl-sel-count">0</span>)</button>
|
||||
</div>
|
||||
<div class="rb-files-dropzone" id="files-dropzone">여기로 파일을 끌어다 놓거나 위 버튼으로 업로드 (사진/CAD/STL 등, 최대 100MB)</div>
|
||||
<div id="files-list-container"></div>
|
||||
@@ -777,6 +783,7 @@ function deleteTask(phaseId,taskId){
|
||||
// 목록을 받아 그때그때 다시 그린다.
|
||||
const FILE_ICONS={photo:'🖼️',video:'🎬',document:'📄',other:'📦'};
|
||||
let filesState={files:[],folders:[]};
|
||||
let stlSelection=new Set(); // 슬라이서로 보낼 STL relPath 다중선택(09-22, K2 슬라이서 연동)
|
||||
|
||||
async function loadFiles(){
|
||||
const proj=currentProject();
|
||||
@@ -792,6 +799,9 @@ async function loadFiles(){
|
||||
container.innerHTML='<div class="rb-files-empty">파일 목록을 불러오지 못했습니다.</div>';
|
||||
return;
|
||||
}
|
||||
// 삭제된 파일이 남아있지 않게 선택 상태 정리.
|
||||
const liveRelPaths=new Set(filesState.files.map(f=>f.relPath));
|
||||
for(const rp of [...stlSelection])if(!liveRelPaths.has(rp))stlSelection.delete(rp);
|
||||
renderFiles();
|
||||
}
|
||||
|
||||
@@ -836,6 +846,10 @@ function renderFiles(){
|
||||
${grid}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const btn=document.getElementById('stl-slice-btn');
|
||||
const countEl=document.getElementById('stl-sel-count');
|
||||
if(countEl)countEl.textContent=String(stlSelection.size);
|
||||
if(btn)btn.disabled=stlSelection.size===0;
|
||||
}
|
||||
|
||||
const STL_RE=/\.stl$/i;
|
||||
@@ -847,7 +861,11 @@ function renderFileCard(file){
|
||||
: `<span class="rb-file-icon">${icon}</span>`;
|
||||
// STL은 새탭 다운로드 대신 Three.js CAD 편집기 팝업으로 연다(작업실↔K2 CAD 플러그인 연동).
|
||||
const openAttrs=isStl?`href="javascript:void(0)" onclick="openCadEditorForFile('${escAttr(file.relPath)}')"`:`href="${escAttr(file.url)}" target="_blank" rel="noopener"`;
|
||||
return `<div class="rb-file-card" data-rel="${escAttr(file.relPath)}">
|
||||
// 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('${escAttr(file.relPath)}',this.checked)">`:'';
|
||||
return `<div class="rb-file-card${checked?' stl-selected':''}" data-rel="${escAttr(file.relPath)}">
|
||||
${selBox}
|
||||
<button class="rb-file-del" onclick="deleteFile('${escAttr(file.relPath)}')" title="삭제">✕</button>
|
||||
<a ${openAttrs}>
|
||||
<div class="rb-file-thumb">${thumb}</div>
|
||||
@@ -859,18 +877,47 @@ function renderFileCard(file){
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// K2 프린터 앱의 "🔧 STL 편집기"(Three.js, cad-editor-app.html)를 그대로 재사용 — STL 목록은
|
||||
// 워크스페이스 전체를 훑으므로(findStlFiles) 작업실 파일도 이미 그 목록에 잡힌다. ?path= 로
|
||||
// 작업실 프로젝트 relPath → 워크스페이스 전체 기준 상대경로. K2 CAD/슬라이서 라우트
|
||||
// (/api/cad/file, /api/cad/list 등)는 findStlFiles()로 워크스페이스 전체를 훑으므로
|
||||
// 이 접두어(workshop/<projectId>/)만 붙이면 작업실 파일도 그대로 잡힌다.
|
||||
function workshopFilePath(relPath){
|
||||
const proj=currentProject();
|
||||
return proj?('workshop/'+proj.id+'/'+relPath):null;
|
||||
}
|
||||
|
||||
// K2 프린터 앱의 "🔧 STL 편집기"(Three.js, cad-editor-app.html)를 그대로 재사용 — ?path= 로
|
||||
// 어느 파일을 열지 지정하면 편집기가 자동으로 로드한다(cad-editor-view.js 09-22 추가).
|
||||
function openCadEditorForFile(relPath){
|
||||
const proj=currentProject();
|
||||
if(!proj)return;
|
||||
const workspacePath='workshop/'+proj.id+'/'+relPath;
|
||||
const workspacePath=workshopFilePath(relPath);
|
||||
if(!workspacePath)return;
|
||||
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?path='+encodeURIComponent(workspacePath),'cad-editor',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
||||
}
|
||||
|
||||
// ── 슬라이서 연동(09-22) ────────────────────────────────────────────────────
|
||||
// STL 카드 체크박스로 여러 부품(예: 스캐너 rig의 base_plate+head_plate+turntable 등)을
|
||||
// 골라 K2 슬라이서 팝업(slicer-app.html)에 한 번에 올린다. 실제 로드는 slicer-view.js의
|
||||
// ?load=<워크스페이스 상대경로,...> 지원(09-22 추가)이 처리.
|
||||
function toggleStlSelect(relPath,checked){
|
||||
if(checked)stlSelection.add(relPath);else stlSelection.delete(relPath);
|
||||
const card=document.querySelector(`.rb-file-card[data-rel="${CSS.escape(relPath)}"]`);
|
||||
if(card)card.classList.toggle('stl-selected',checked);
|
||||
const btn=document.getElementById('stl-slice-btn');
|
||||
const countEl=document.getElementById('stl-sel-count');
|
||||
if(countEl)countEl.textContent=String(stlSelection.size);
|
||||
if(btn)btn.disabled=stlSelection.size===0;
|
||||
}
|
||||
|
||||
function openSlicerForSelected(){
|
||||
if(!stlSelection.size)return;
|
||||
const paths=[...stlSelection].map(workshopFilePath).filter(Boolean);
|
||||
if(!paths.length)return;
|
||||
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?load='+encodeURIComponent(paths.join(',')),'k2-slicer',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
||||
}
|
||||
|
||||
function fmtFileSize(n){
|
||||
if(!n&&n!==0)return '';
|
||||
if(n<1024)return n+'B';
|
||||
|
||||
@@ -1140,6 +1140,39 @@
|
||||
ro.observe(vpEl);
|
||||
// 초기 렌더 확정 — 다음 프레임에 한 번 더 resize(레이아웃 완료 보장).
|
||||
requestAnimationFrame(() => { resize(); requestAnimationFrame(resize); });
|
||||
await loadFromUrlParams();
|
||||
}
|
||||
|
||||
// 다른 앱(작업실 파일 탭 등)에서 워크스페이스 STL을 바로 슬라이서 판에 얹어주도록
|
||||
// ?load=<워크스페이스 상대경로>[,경로2,...] 지원(09-22, cad-editor-view.js의 ?path=
|
||||
// 패턴과 동일한 취지). /api/cad/file?path= 로 다운로드해 loadModel()에 그대로 넣는다 —
|
||||
// 파일 input change 핸들러(1013줄 근처)와 배치 로직을 공유.
|
||||
async function loadFromUrlParams() {
|
||||
const loadParam = new URLSearchParams(location.search).get('load');
|
||||
if (!loadParam) return;
|
||||
const paths = loadParam.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
if (!paths.length) return;
|
||||
setInfo(`📂 서버에서 ${paths.length}개 파일 불러오는 중…`);
|
||||
let placed = 0;
|
||||
for (const p of paths) {
|
||||
try {
|
||||
const r = await fetch('/api/cad/file?path=' + encodeURIComponent(p), { headers: authH() });
|
||||
if (!r.ok) { setErr(`불러오기 실패(${r.status}): ${p}`); continue; }
|
||||
const buf = await r.arrayBuffer();
|
||||
const name = p.split('/').pop() || p;
|
||||
const lr = loadModel(buf, name);
|
||||
if (!lr.ok) { setErr(lr.detail + ' (' + name + ')'); continue; }
|
||||
const px = clamp(BED / 2 + (placed - (paths.length - 1) / 2) * 35, 15, BED - 15);
|
||||
const pz = clamp(BED / 2 + (placed % 2) * 35 - 17, 15, BED - 15);
|
||||
addInstance(lr.idx, px, pz, 0, 1);
|
||||
placed++;
|
||||
} catch (e) { setErr(`불러오기 실패: ${p} (${e.message})`); }
|
||||
}
|
||||
if (placed) {
|
||||
selectInstance(instances.length - 1);
|
||||
fitCameraToPlate();
|
||||
setInfo(`📂 서버에서 ${placed}개 모델 로드 · 총 <b>${instances.length}</b>개 인스턴스 · <b>${models.length}</b>종`);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
Reference in New Issue
Block a user