Files
Pier_v1/도면작성/template.ts
T
2026-11-04 13:34:51 +09:00

73 lines
7.7 KiB
TypeScript

import source from './reference-template';
import type { DrawingDocument, Entity, FrameTemplate, Point, Rect, SheetConfig, ViewId } from './types';
export const REFERENCE_TEMPLATE=source as unknown as FrameTemplate;
export const FIELD_LABELS:Record<string,string>={TD_HSCAL:'도면축척',TD_VERSION:'버전',TD_CNAME:'사업/공사명',TD_CCOMP:'시공자',TD_DSECT:'시설/공구',TD_DWGCLA:'도면분류',TD_STAN:'근거',TD_SUBSTAN:'기준',TD_PPOSE:'용도',TD_DWGCODE:'도면번호',TD_DWGNO:'편철번호',TD_DNAME_MAIN:'도면명',TD_DNAME_TOP:'교각명',TD_DNAME_BOT:'교량/방향',TB_STITL:'상단 부제',TB_MTITIL:'상단 제목',TB_CSCOP:'상단 공종',TB_CNAME:'상단 공사명'};
export function defaultSheet():SheetConfig{return{fields:{TD_CNAME:'{{project}}',TD_DNAME_MAIN:'일반도(1/1)',TD_DNAME_TOP:'{{pier}}',TD_DNAME_BOT:'{{bridge}}',TD_HSCAL:'S=1:{{scale}}@{{paper}}',TD_VERSION:''},notes:REFERENCE_TEMPLATE.notes,references:'종평면도 :\n지질주상도 :\n받침배치도 :\n말뚝 상세도 :',noteHeight:3.5,showFrame:true,showLogo:true,startLabel:'시점',endLabel:'종점'};}
export const referenceRect=(width:number,height:number,x:number,y:number,w:number,h:number):Rect=>({x:x*width/841,y:y*height/594,width:w*width/841,height:h*height/594});
export function templateSlots(width:number,height:number):Record<ViewId,Rect>{
const r=(x:number,y:number,w:number,h:number)=>referenceRect(width,height,x,y,w,h);
return{front:r(58,68,255,216),side:r(335,68,172,216),top:r(58,370,255,157),footing:r(335,370,178,157),section:r(58,68,255,216),
iso:r(538,48,123,137),detail:r(538,225,125,148),schedule:r(538,398,280,146),heights:r(538,398,280,146),keyplan:r(687,42,122,35)};
}
export function wrappedLines(text:string,width:number,font:number):string[]{
const out:string[]=[];
for(const paragraph of text.split('\n')){let line='',length=0;for(const c of paragraph){const n=c.charCodeAt(0)>255?1:.55;if(length+n>width/font&&line){out.push(line);line='';length=0;}line+=c;length+=n;}out.push(line);}
return out;
}
export function templateFrame(doc:DrawingDocument):Entity[]{
const cfg=doc.settings.sheet??defaultSheet(),template=cfg.template??REFERENCE_TEMPLATE,out:Entity[]=[],factor=Math.min(doc.paper.width/template.width,doc.paper.height/template.height);
const dx=(doc.paper.width-template.width*factor)/2,dy=(doc.paper.height-template.height*factor)/2;
const p=(p:Point):Point=>[dx+p[0]*factor,dy+p[1]*factor];
const source={member:'reference-frame',pier:doc.pier,index:0};
const resolve=(s:string)=>s.replace(/\{\{(project|bridge|pier|paper|scale)\}\}/g,(_,key)=>({project:doc.project,bridge:doc.bridge,pier:doc.pier,paper:doc.paper.name,scale:String(doc.views[0].scale)}[key as 'pier']));
if(cfg.showFrame){
for(const e of template.entities){
if(!cfg.showLogo&&e.source.member==='reference-logo')continue;
const base={...e,source:{...e.source,pier:doc.pier}};
if(e.kind==='line')out.push({...base,kind:'line',a:p(e.a),b:p(e.b)});
if(e.kind==='polyline')out.push({...e,points:e.points.map(p),source});
if(e.kind==='hatch')out.push({...e,points:e.points.map(p),holes:e.holes?.map(loop=>loop.map(p)),spacing:e.spacing*factor,source});
if(e.kind==='circle')out.push({...e,center:p(e.center),radius:e.radius*factor,source});
if(e.kind==='text')out.push({...e,point:p(e.point),height:e.height*factor,source});
}
for(const f of template.fields){const text=resolve(cfg.fields[f.tag]??f.value);out.push({kind:'text',point:p(f.point),text,height:f.height*factor,rotation:f.rotation,layer:'FRAME',source:{member:`frame-field:${f.tag}`,index:0,pier:doc.pier}});}
}
const k=doc.paper.width/841,sourceNote={member:'reference-note',pier:doc.pier,index:0};
const pp=(x:number,y:number):Point=>[x*k,doc.paper.height-y*doc.paper.height/594];
const text=(x:number,y:number,t:string,size=3.3)=>out.push({kind:'text',point:pp(x,y),text:t,height:size*k,layer:'FRAME',source:sourceNote});
const line=(x:number,y:number,x1:number,y1:number)=>out.push({kind:'line',a:pp(x,y),b:pp(x1,y1),layer:'FRAME',source:sourceNote});
line(680,82,820,82);line(680,82,680,347);line(820,82,820,347);line(680,347,820,347);
text(680,93,'NOTE',4.2);
wrappedLines(cfg.notes,133,cfg.noteHeight).forEach((s,i)=>text(686,104+i*cfg.noteHeight*1.65,s,cfg.noteHeight));
text(680,275,'LEGEND',4);line(680,281,820,281);
for(const [i,label] of ['코핑','기둥','기초'].entries()){
const x=687+i*44;out.push({kind:'hatch',points:[pp(x,287),pp(x+10,287),pp(x+10,292),pp(x,292)],pattern:i===2?'ANSI31':'SOLID',angle:45,spacing:1.2*k,color:'#bfc6cc',layer:'FRAME',source:sourceNote});text(x+12,292,label,3.1);
}
text(680,310,'REFERENCE',4);
cfg.references.split('\n').forEach((s,i)=>text(686,320+i*5.5,s,3.1));
text(680,355,'NOTE: 참고 DWG 원문 · 현 프로젝트 조건 확인',2.5);
return out;
}
/** Import only finite CAD primitives and bounded text, not executable content. */
export function parseSheetConfig(value:unknown):SheetConfig{
const c=value as SheetConfig;
if(!c||typeof c.notes!=='string'||typeof c.references!=='string'||!c.fields||typeof c.fields!=='object'||c.notes.length>20000||c.references.length>4000)throw new Error('도곽/노트 설정 JSON 형식이 올바르지 않습니다.');
if(!Number.isFinite(c.noteHeight)||c.noteHeight<1.5||c.noteHeight>6)throw new Error('노트 문자 높이는 A1 기준 1.5~6mm입니다.');
if(typeof c.showFrame!=='boolean'||typeof c.showLogo!=='boolean'||typeof c.startLabel!=='string'||typeof c.endLabel!=='string'||c.startLabel.length>200||c.endLabel.length>200)throw new Error('도곽 표시/키플랜 설정이 올바르지 않습니다.');
if(Object.values(c.fields).some(v=>typeof v!=='string'||v.length>500))throw new Error('표제란 값이 올바르지 않습니다.');
if(c.template){
const t=c.template,pt=(p:unknown)=>Array.isArray(p)&&p.length===2&&p.every(v=>typeof v==='number'&&Number.isFinite(v)&&Math.abs(v)<1e6);
if(t.version!==1||!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<100||t.height<100||t.width>3000||t.height>3000||!Array.isArray(t.entities)||t.entities.length>10000||!Array.isArray(t.fields))throw new Error('사용자 도곽 크기/객체 형식이 올바르지 않습니다.');
for(const e of t.entities){if(!e||!e.source||!['FRAME','HATCH','TEXT','STRUCTURE'].includes(e.layer))throw new Error('지원하지 않는 도곽 레이어입니다.');
if(e.cad&&(!e.cad.layer||typeof e.cad.layer!=='string'||e.cad.layer.length>255||!Number.isInteger(e.cad.color)||e.cad.color<0||e.cad.color>256||!Number.isInteger(e.cad.lineweight)||e.cad.lineweight< -3||e.cad.lineweight>211||typeof e.cad.linetype!=='string'))throw new Error('도곽 CAD 스타일 형식이 올바르지 않습니다.');
const okay=e.kind==='line'?pt(e.a)&&pt(e.b):e.kind==='circle'?pt(e.center)&&Number.isFinite(e.radius)&&e.radius>0:e.kind==='text'?pt(e.point)&&typeof e.text==='string'&&Number.isFinite(e.height)&&e.height>0:e.kind==='polyline'||e.kind==='hatch'?Array.isArray(e.points)&&e.points.length<10000&&e.points.every(pt):false;
if(!okay)throw new Error('도곽에 잘못된 CAD 객체가 있습니다.');
if(e.kind==='hatch'&&(!['SOLID','ANSI31'].includes(e.pattern)||!Number.isFinite(e.spacing)||e.spacing<=0||!Number.isFinite(e.angle)))throw new Error('해치 형식이 올바르지 않습니다.');
if(e.kind==='hatch'&&e.holes&&(!Array.isArray(e.holes)||e.holes.some(loop=>!Array.isArray(loop)||!loop.every(pt))))throw new Error('해치 내부 경계가 올바르지 않습니다.');
}
for(const f of t.fields)if(typeof f.tag!=='string'||!/^[A-Z][A-Z0-9_]{0,60}$/i.test(f.tag)||['__proto__','constructor','prototype'].includes(f.tag)||typeof f.value!=='string'||!pt(f.point)||!Number.isFinite(f.height)||f.height<=0||!Number.isFinite(f.rotation))throw new Error('도곽 속성 형식이 올바르지 않습니다.');
}
return{...defaultSheet(),...c};
}