import './workspace.css'; import { DEFAULT_SETTINGS, generateDrawing, VIEW_TITLES, formatMm } from './drawing'; import { drawingSvg } from './svg'; import { exportDxf } from './dxf'; import { LAYERS, type DrawingDocument, type DrawingSettings, type DrawingSnapshot, type Layer, type Rect, type ViewId } from './types'; import type { PierModel } from '../src/core/geometry'; import { defaultSheet, parseSheetConfig } from './template'; import { createSheetEditor } from './sheet-editor'; import { shortenedBounds } from './breaks'; import { createQualityPanel } from './quality-panel'; export function createDrawingWorkspace(host: HTMLElement,read:()=>DrawingSnapshot,select:(id:string)=>void,openMaterials?:()=>void): { update:()=>void } { host.className='ga-workspace'; host.innerHTML=`
프로젝트 미지정교각 일반도
교각 형상을 생성하면 일반도가 표시됩니다.
출력 미리보기
`; const node=(id:string)=>host.querySelector(`#${id}`)!; if(openMaterials){ const button=window.document.createElement('button');button.type='button';button.id='ga-materials';button.textContent='재료표';button.addEventListener('click',openMaterials); host.querySelector('.ga-toolbar-actions')!.prepend(button); } const pier=node('ga-pier'),view=node('ga-view'),paper=node('ga-paper'),scale=node('ga-scale'); const bearing=node('ga-bearing'); const canvas=node('ga-canvas'),empty=node('ga-empty'),warnings=node('ga-warnings'),note=node('ga-data-note'); const visible=new Set((Object.keys(LAYERS) as Layer[]).filter(l=>l!=='HIDDEN')); let sheetConfig=defaultSheet(),sheetKey='',lastContextModels:(PierModel|null)[]=[]; let document:DrawingDocument|undefined,lastModel:PierModel|undefined,lastKey='',optionKey='',camera:Rect={x:0,y:0,width:420,height:297}; let press:{x:number;y:number;anchor:DOMPoint;entity?:string}|undefined; const settings=():DrawingSettings=>({paper:paper.value as DrawingSettings['paper'],scale:scale.value==='auto'?'auto':Number(scale.value), dimensions:node('ga-dimensions').checked,centers:node('ga-centers').checked,elevations:node('ga-elevations').checked, shorten:node('ga-shorten').checked,detailBearing:bearing.value,hatches:node('ga-hatches').checked,isoDirection:Number(node('ga-iso').value),sheet:sheetConfig}); const svg=()=>canvas.querySelector('svg'); const setCamera=()=>svg()?.setAttribute('viewBox',`${camera.x} ${camera.y} ${camera.width} ${camera.height}`); const fit=()=>{ if(!document)return; const target=document.views.find(v=>v.id===view.value); if(target){const b=shortenedBounds(target.bounds,target.breaks),width=Math.max(target.slot.width,(b.maxX-b.minX)/target.scale),height=Math.max(target.slot.height,(b.maxY-b.minY)/target.scale);camera={x:target.slot.x+(target.slot.width-width)/2-5,y:target.slot.y+(target.slot.height-height)/2-5,width:width+10,height:height+12};} else camera={x:0,y:0,width:document.paper.width,height:document.paper.height}; setCamera(); }; const redraw=(reset=false)=>{ if(!document){canvas.replaceChildren();empty.hidden=false;return;} empty.hidden=true;canvas.innerHTML=drawingSvg(document,visible,true,view.value==='sheet'?undefined:view.value as ViewId);if(reset)fit();else setCamera(); node('ga-count').textContent=`${document.views.reduce((n,v)=>n+v.entities.filter(e=>visible.has(e.layer)).length,0)} 객체 · 간섭 ${document.quality?.after??'미검사'}건 · mm / EL m`; }; const enable=(on:boolean)=>['ga-dxf','ga-dwg','ga-quality','ga-svg','ga-preview','ga-generate','ga-update','ga-regenerate'].forEach(id=>{node(id).disabled=!on;}); const update=(force=false)=>{ host.style.height=`${Math.max(480,window.innerHeight-host.getBoundingClientRect().top-12)}px`; const snapshot=read(),items=snapshot.piers; const nextSheetKey=`pierzainer.ga.sheet.v1:${snapshot.project.name}|${snapshot.project.bridge}`; if(sheetKey!==nextSheetKey){sheetKey=nextSheetKey;sheetConfig=defaultSheet();try{const saved=localStorage.getItem(sheetKey);if(saved)sheetConfig=parseSheetConfig(JSON.parse(saved));}catch{/* Keep original template if browser storage is unavailable or corrupt. */}} const nextOptionKey=items.map(p=>`${p.input.name}:${p.input.type}:${!!p.model}`).join('|'); if(optionKey!==nextOptionKey){optionKey=nextOptionKey;pier.replaceChildren(); if(!items.length)pier.add(new Option('교각 없음','')); for(const p of items)pier.add(new Option(`${p.input.name} · ${p.input.type}${p.model?'':' · 형상 없음'}`,p.input.name));} pier.value=snapshot.selected??items[0]?.input.name??''; const selected=items.find(p=>p.input.name===pier.value); node('ga-project').textContent=[snapshot.project.name,snapshot.project.bridge].filter(Boolean).join(' · ')||'프로젝트 미지정'; node('ga-title').textContent=selected?`${selected.input.name} 교각 일반도`:'교각 일반도'; host.dataset.pier=selected?.input.name??''; note.textContent=selected?.dirty?'현재 입력이 3D 형상에 아직 반영되지 않았습니다. 상단에서 형상을 다시 생성하면 도면도 갱신됩니다.':'현재 생성된 3D 형상 데이터를 사용합니다. 모델 변경 시 자동 갱신합니다.'; enable(!!selected?.model);warnings.replaceChildren(); if(!selected?.model){document=undefined;lastModel=undefined;empty.textContent=selected?'선택한 교각의 형상이 없습니다. 형상 생성 오류를 먼저 확인하세요.':'교각 형상을 생성하면 일반도가 표시됩니다.';redraw();return;} if(lastModel!==selected.model){const previous=bearing.value;bearing.replaceChildren();for(const b of selected.model.blocks)bearing.add(new Option(`${b.row} ${b.no}`,`${b.row}:${b.no}`));if(Array.from(bearing.options).some(o=>o.value===previous))bearing.value=previous;} const key=JSON.stringify([settings(),snapshot.project.name,snapshot.project.bridge]); try{ const models=items.map(p=>p.model),contextChanged=models.length!==lastContextModels.length||models.some((m,i)=>m!==lastContextModels[i]); if(force||lastModel!==selected.model||lastKey!==key||contextChanged){document=generateDrawing(selected.model,selected.input,snapshot.project,settings(),{models:models.filter((m):m is PierModel=>!!m)});lastModel=selected.model;lastContextModels=models;lastKey=key;redraw(true);} for(const message of document?.warnings??[]){const li=window.document.createElement('li');li.textContent=message;warnings.append(li);} }catch(error){document=undefined;lastModel=undefined;enable(false);empty.textContent=(error as Error).message;redraw();} }; const download=(text:string,extension:string,mime:string)=>{ if(!document)return; const url=URL.createObjectURL(new Blob([text],{type:mime})),link=window.document.createElement('a'); link.href=url;link.download=`${document.bridge||document.project||'교량'}_${document.pier}_일반도.${extension}`.replace(/[\\/:*?"<>|]/g,'_');link.click();window.setTimeout(()=>URL.revokeObjectURL(url),1000); }; pier.addEventListener('change',()=>select(pier.value));view.addEventListener('change',()=>redraw(true)); [paper,scale,bearing,node('ga-iso'),node('ga-hatches'),node('ga-shorten'),node('ga-dimensions'),node('ga-centers'),node('ga-elevations')].forEach(e=>e.addEventListener('change',()=>update(true))); const editor=createSheetEditor(host,config=>{sheetConfig=config;try{localStorage.setItem(sheetKey,JSON.stringify(config));}catch{node('ga-object-info').textContent='브라우저 저장 공간이 부족합니다. 도곽/노트 JSON 저장을 사용하세요.';}update(true);}); node('ga-sheet-editor').addEventListener('click',()=>editor.open(sheetConfig)); ['ga-generate','ga-update','ga-regenerate'].forEach(id=>node(id).addEventListener('click',()=>update(true))); node('ga-fit').addEventListener('click',fit); host.querySelectorAll('[data-layer]').forEach(checkbox=>checkbox.addEventListener('change',()=>{const layer=checkbox.dataset.layer as Layer;if(checkbox.checked)visible.add(layer);else visible.delete(layer);redraw();})); const point=(event:{clientX:number;clientY:number}):DOMPoint=>new DOMPoint(event.clientX,event.clientY).matrixTransform(svg()!.getScreenCTM()!.inverse()); const zoom=(factor:number,anchor?:DOMPoint)=>{ if(!document||!svg())return; const target=anchor??new DOMPoint(camera.x+camera.width/2,camera.y+camera.height/2),width=Math.min(document.paper.width*20,Math.max(document.paper.width*.025,camera.width*factor)),ratio=width/camera.width; camera={x:target.x-(target.x-camera.x)*ratio,y:target.y-(target.y-camera.y)*ratio,width,height:camera.height*ratio};setCamera(); }; node('ga-zoom-in').addEventListener('click',()=>zoom(.8));node('ga-zoom-out').addEventListener('click',()=>zoom(1.25)); canvas.addEventListener('wheel',event=>{if(svg()){event.preventDefault();zoom(Math.exp(Math.max(-.4,Math.min(.4,event.deltaY*.0015))),point(event));}},{passive:false}); canvas.addEventListener('pointerdown',event=>{if(!svg()||event.button>1)return;event.preventDefault();press={x:event.clientX,y:event.clientY,anchor:point(event),entity:(event.target as Element).closest('[data-entity]')?.dataset.entity};canvas.setPointerCapture(event.pointerId);canvas.classList.add('is-panning');}); canvas.addEventListener('pointermove',event=>{if(!press||!svg())return;const current=point(event);camera.x+=press.anchor.x-current.x;camera.y+=press.anchor.y-current.y;setCamera();}); const release=(event:PointerEvent)=>{if(!press)return;const clicked=Math.hypot(event.clientX-press.x,event.clientY-press.y)<4;if(clicked&&press.entity&&document){const [viewId,index]=press.entity.split(':');const e=document.views.find(v=>v.id===viewId)?.entities[Number(index)];if(e)node('ga-object-info').textContent=`${e.source.pier} / ${e.source.member} ${e.source.index+1} · ${e.layer}${e.kind==='dimension'?` · ${formatMm(e.value)} mm`:''}`;}press=undefined;canvas.classList.remove('is-panning');}; canvas.addEventListener('pointerup',release);canvas.addEventListener('pointercancel',()=>{press=undefined;canvas.classList.remove('is-panning');}); node('ga-dxf').addEventListener('click',()=>{if(document)download(exportDxf(document,visible),'dxf','application/dxf;charset=utf-8');}); const qualityPanel=createQualityPanel(host,()=>document,id=>{view.value=id;redraw(true);}); node('ga-quality').addEventListener('click',qualityPanel); node('ga-dwg').addEventListener('click',async()=>{ const snapshot=document;if(!snapshot)return;const button=node('ga-dwg');button.disabled=true;button.textContent='DWG 검증 중…'; try{ const response=await fetch('/__pierzainer/general-arrangement/dwg',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dxf:exportDxf(snapshot,new Set(visible))})}); if(!response.ok){const error=await response.json().catch(()=>({error:'DWG 출력 서버를 찾을 수 없습니다. 개발 서버를 다시 실행하세요.'}));throw new Error(error.error);} snapshot.dwgValidation=JSON.parse(decodeURIComponent(response.headers.get('X-Pierzainer-Validation')??'%7B%7D')); const url=URL.createObjectURL(await response.blob()),a=window.document.createElement('a');a.href=url;a.download=`${snapshot.bridge||snapshot.project}_${snapshot.pier}_일반도.dwg`.replace(/[\\/:*?"<>|]/g,'_');a.click();setTimeout(()=>URL.revokeObjectURL(url),1000); node('ga-object-info').textContent='DWG 2000 변환 · 내부 객체/치수 비교 통과(CAD 직접 열기 미검증). 열리지 않으면 DXF 내보내기를 사용하고 CAD 오류 문구를 확인하세요.'; }catch(error){node('ga-object-info').textContent=`DWG 출력 실패: ${(error as Error).message}`;}finally{button.disabled=false;button.textContent='DWG 내보내기';} }); node('ga-svg').addEventListener('click',()=>{if(document)download(drawingSvg(document,visible),'svg','image/svg+xml;charset=utf-8');}); const preview=node('ga-print-dialog'); node('ga-preview').addEventListener('click',()=>{if(!document)return;node('ga-print-sheet').innerHTML=drawingSvg(document,visible);node('ga-print-paper').textContent=`${document.paper.name} 가로 · 각 뷰 축척 적용`;preview.showModal();}); node('ga-print-close').addEventListener('click',()=>preview.close()); node('ga-print').addEventListener('click',()=>{ if(!document)return;const frame=window.document.createElement('iframe');frame.style.cssText='position:fixed;width:1px;height:1px;left:-9999px;border:0'; frame.srcdoc=`교각 일반도${drawingSvg(document,visible)}`; frame.onload=()=>{frame.contentWindow?.addEventListener('afterprint',()=>frame.remove(),{once:true});frame.contentWindow?.print();};host.append(frame); }); window.addEventListener('resize',()=>{if(!host.hidden)host.style.height=`${Math.max(480,window.innerHeight-host.getBoundingClientRect().top-12)}px`;}); return{update:()=>update()}; }