프로젝트 틀만 남기고 초기화

This commit is contained in:
김지영 committed 2026-10-20 12:05:57 +09:00
1 parent fac29cb010
commit aadccee687
32 files changed
+44 -331892

No files matched your search

-96
View File
@@ -1,96 +0,0 @@
import json,pathlib,collections,math
ROOT=pathlib.Path(__file__).resolve().parents[1]
raw=(ROOT/'public/drawings/광터지하차도_일반도(BLOCK7).dxf').read_text(encoding='utf8',errors='replace').splitlines()
pairs=[(int(raw[i].strip()),raw[i+1].strip()) for i in range(0,len(raw)-1,2)]
entities=[]; current=None; section=''; block=None; blocks={}
for code,value in pairs:
if code==0:
if current:
if section=='ENTITIES': entities.append(current)
elif section=='BLOCKS' and block and current['type'] not in ('BLOCK','ENDBLK'): blocks.setdefault(block,[]).append(current)
current={'type':value,'data':[]}
if value=='ENDBLK': block=None
elif current:
current['data'].append((code,value))
if current['type']=='SECTION' and code==2: section=value
if current['type']=='BLOCK' and code==2: block=value
def get(e,c,d=None): return next((v for k,v in e['data'] if k==c),d)
def point(e,c=10): return [float(get(e,c,0)),float(get(e,c+10,0))]
def layer(e): return get(e,8,'')
out=ROOT/'docs/extracted';out.mkdir(exist_ok=True)
texts=[{'text':get(e,1,''),'p':point(e),'layer':layer(e)} for e in entities if e['type'] in ('TEXT','MTEXT')]
lines=[{'a':point(e),'b':point(e,11),'layer':layer(e),'handle':get(e,5)} for e in entities if e['type']=='LINE']
for name,data in [('texts',texts),('lines',lines),('blocks',blocks),('inserts',[{'name':get(e,2),'p':point(e),'sx':get(e,41,'1'),'sy':get(e,42,'1'),'rotation':get(e,50,'0'),'layer':layer(e)} for e in entities if e['type']=='INSERT'])]:
(out/(name+'.json')).write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf8')
print('Entities',collections.Counter(e['type'] for e in entities))
for t in texts:
if any(s in t['text'] for s in ('BLOCK7','106.675','107.307','101.636','101.069','102.436','101.869','299.010','318.990','선형','측구','20.100')): print(t)
for name,top in [('A',-74271),('B',-91211)]:
selected=[l for l in lines if l['layer'] in ('CS-CONC-MAJR','CS-CONC-MINR') and all(1118000<p[0]<1139000 and top-7500<p[1]<top+100 for p in (l['a'],l['b']))]
print(name,len(selected),collections.Counter(l['layer'] for l in selected))
(out/f'section-{name}.json').write_text(json.dumps(selected,ensure_ascii=False,indent=2),encoding='utf8')
# 도면 근거를 보존하는 모델 데이터와 원도면 검토 SVG 생성
sections=[]
for name,top,el,sta in [('A',-74271.37855593952,106.675,299.000),('B',-91211.37855593952,107.307,319.000)]:
selected=[l for l in lines if l['layer'] in ('CS-CONC-MAJR','CS-CONC-MINR') and all(1118000<p[0]<1139000 and top-7500<p[1]<top+100 for p in (l['a'],l['b']))]
sections.append({'name':name,'topCad':top,'topEL':el,'station':sta,'lines':selected})
def group(i):
if i<5 or i==100: return '底版'
if 5<=i<=15: return '좌측 측구·포장 경계'
if 16<=i<=26: return '우측 측구·포장 경계'
if 27<=i<=86: return '포장 반복선·내부선'
if 87<=i<=89: return '중앙 분리부'
if 90<=i<=94: return '좌측 벽체'
if 95<=i<=99: return '우측 벽체'
if 101<=i<=107: return '하부·외측 보조 윤곽'
return '바닥판 상면·세부선'
def norm(p,s): return [round((p[0]-1128486.218066491)/1000,7),round(s['topEL']+(p[1]-s['topCad'])/1000,7)]
parts=[]
for i,(a,b) in enumerate(zip(sections[0]['lines'],sections[1]['lines'])):
assert a['layer']==b['layer']
assert all(abs(a[k][0]-b[k][0])<0.01 for k in ['a','b'])
parts.append({'id':'line-'+a['handle'],'name':group(i).replace('底版','바닥판 하단')+' · '+str(i+1),'group':group(i).replace('底版','바닥판 하단'),'layer':a['layer'],'handles':[a['handle'],b['handle']],'a':[norm(a['a'],sections[0]),norm(a['b'],sections[0])],'b':[norm(b['a'],sections[1]),norm(b['b'],sections[1])],'repeat':27<=i<=86,'kind':'line','source':'일반도 14 / 단면 A-A·B-B / '+a['layer']})
# 삽입 블록의 반전과 실제 LINE/ARC 윤곽을 보존한다. 원호는 5도 간격으로 표시한다.
ductName='A$C2017021411375356'
for side,x,sx in [('좌측',1119736.218066468,1),('우측',1137236.218066513,-1)]:
for j,e in enumerate(blocks[ductName]):
if e['type']=='LINE': pts=[point(e),point(e,11)]
elif e['type']=='ARC':
center=point(e); r=float(get(e,40)); start=float(get(e,50)); end=float(get(e,51))
if end<=start:end+=360
pts=[[center[0]+r*math.cos(math.radians(t)),center[1]+r*math.sin(math.radians(t))] for t in [start+(end-start)*k/36 for k in range(37)]]
else: continue
profiles=[]
for s,y in zip(sections,[-77447.89255584464,-95669.3155558447]):profiles.append([norm([x+sx*p[0],y+p[1]],s) for p in pts])
parts.append({'id':f'duct-{side}-{j}','name':f'{side} 공동구·상부 상세 · {j+1}','group':f'{side} 공동구·상부 상세','layer':layer(e),'handles':[get(e,5)],'a':profiles[0],'b':profiles[1],'repeat':False,'kind':e['type'].lower(),'source':'일반도 14 / '+ductName+' / 원본 삽입 블록'})
model={'source':'광터지하차도_일반도(BLOCK7).dxf','units':'m','length':19.98,'sectionStations':[299,319],'longitudinalStations':[299.01,318.99],'sectionTops':[106.675,107.307],'sectionBottoms':[101.636,101.069],'centerEL':[None,None],'alignment':None,'status':'확인 필요','connection':{'mode':'straight','status':'확인 필요','authorization':'2026-10-20 사용자 검토용 직선 연결 허용','description':'종단 경계 299.010~318.990에 원본 단면 299.000/319.000을 대응시킨 검토용 연결. 실제 평면 위치와 중간 형상은 미검증.'},'parts':parts}
modelDir=ROOT/'src/model';modelDir.mkdir(exist_ok=True)
(modelDir/'block7.json').write_text(json.dumps(model,ensure_ascii=False,indent=2),encoding='utf8')
import html
previewDir=ROOT/'public/analysis';previewDir.mkdir(exist_ok=True)
views=[('단면A',1113000,-84300,1142000,-69500),('단면B',1113000,-102300,1142000,-85500),('평면검토',1020000,-96000,1105000,-64000),('종단검토',1040000,-113000,1085000,-95000),('측구상세',1165000,-85000,1185000,-77000)]
for title,x0,y0,x1,y1 in views:
content=[]
for l in lines:
if all(x0<p[0]<x1 and y0<p[1]<y1 for p in (l['a'],l['b'])):
color='#326c5d' if l['layer'] in ('CS-CONC-MAJR','CS-CONC-MINR') else '#8d969e'
content.append(f'<line x1="{l["a"][0]}" y1="{-l["a"][1]}" x2="{l["b"][0]}" y2="{-l["b"][1]}" stroke="{color}" stroke-width="15"><title>{l["handle"]} / {l["layer"]}</title></line>')
for t in texts:
x,y=t['p']
if x0<x<x1 and y0<y<y1:content.append(f'<text x="{x}" y="{-y}" font-size="200" fill="#28334b">{html.escape(t["text"])}</text>')
# 공동구 삽입 블록의 실제 선 포함
for e in entities:
if e['type']=='INSERT' and get(e,2)==ductName:
x,y=point(e);sx=float(get(e,41,1));sy=float(get(e,42,1))
if x0<x<x1 and y0<y<y1:
for q in blocks[ductName]:
if q['type']=='LINE':
a,b=point(q),point(q,11);content.append(f'<line x1="{x+sx*a[0]}" y1="{-y-sy*a[1]}" x2="{x+sx*b[0]}" y2="{-y-sy*b[1]}" stroke="#a97635" stroke-width="15"/>')
svg=f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x0} {-y1} {x1-x0} {y1-y0}"><rect x="{x0}" y="{-y1}" width="{x1-x0}" height="{y1-y0}" fill="#fff"/>'+''.join(content)+'</svg>'
(previewDir/(title+'.svg')).write_text(svg,encoding='utf8')
print('모델 추출:',len(parts),'객체. 단면 원본 선 114쌍, 공동구 20쌍 × 좌우.')
-34
View File
@@ -1,34 +0,0 @@
<!doctype html><meta charset="utf-8"><title>뷰어 자동 검증</title><pre id="result">검증 중</pre><iframe src="/" style="width:1280px;height:1000px;border:0"></iframe><script type="module">
const iframe = document.querySelector('iframe'), result = document.querySelector('#result');
const pause = () => new Promise(resolve => setTimeout(resolve, 120));
const checks = [];
const check = (condition, name) => { checks.push({ name, pass: !!condition }); if (!condition) throw Error(name); };
try {
for (let i = 0; i < 80 && !iframe.contentDocument?.querySelector('canvas'); i++) await pause();
const doc = iframe.contentDocument;
const click = async text => { const button = [...doc.querySelectorAll('button')].find(e => e.textContent === text); check(button, `버튼 존재: ${text}`); button.click(); await pause(); };
const canvas = doc.querySelector('canvas'); check(canvas?.width > 0, '3D 캔버스 생성');
check(doc.body.textContent.includes('154'), '원본 154 객체 표시');
for (const name of ['평면','정면','측면','A-A','B-B','전체']) await click(name);
await click('모두 숨김'); check(doc.querySelector('.panel-heading').textContent.includes('0 / 154'), '전체 숨김 반영');
await click('모두 표시'); check(doc.querySelector('.panel-heading').textContent.includes('94 / 154'), '반복선 제외한 전체 표시');
const details = doc.querySelector('details'); details.open = true;
const row = doc.querySelector('.part-row button'); row.click(); await pause();
check(doc.querySelector('.selection').textContent.includes('원본 선 번호'), '선택하면 원본 근거 표시');
await click('선택한 윤곽 숨기기'); check(doc.querySelector('.panel-heading').textContent.includes('93 / 154'), '개별 숨김 반영');
let captured; iframe.contentWindow.URL.createObjectURL = blob => { captured = blob; return 'blob:viewer-test'; }; doc.addEventListener('click', e => { if (e.target.closest('a[download]')) e.preventDefault(); }, true);
await click('3D OBJ ↓'); const obj = await captured.text(); check(obj.split('\n').filter(s => s.startsWith('o ')).length === 153, 'OBJ 내보내기 153개 객체'); check(!obj.includes('NaN'), 'OBJ 유효한 좌표'); check(obj.split('\n').filter(s => s.startsWith('f ')).length === 466, 'OBJ 연결 삼각면 466개');
const before = canvas.toDataURL(); canvas.dispatchEvent(new iframe.contentWindow.KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); await pause(); check(canvas.toDataURL() !== before, '키보드 회전 반영');
const beforeZoom = canvas.toDataURL(); canvas.dispatchEvent(new iframe.contentWindow.WheelEvent('wheel', { deltaY: -200, bubbles: true, cancelable: true })); await pause(); check(canvas.toDataURL() !== beforeZoom, '휠 확대 반영');
const input = doc.querySelector('.search'), setter = Object.getOwnPropertyDescriptor(iframe.contentWindow.HTMLInputElement.prototype, 'value').set;
setter.call(input, '없는부재'); input.dispatchEvent(new iframe.contentWindow.Event('input', { bubbles: true })); await pause();
check(doc.querySelector('.empty'), '윤곽 검색 빈 결과 표시');
await click('도면'); check(doc.querySelector('.drawing-preview'), '도면 발췌 표시');
await click('측구상세'); await pause(); check(doc.querySelector('.drawing-preview').getAttribute('src').includes('측구상세'), '상세도 전환');
await click('검증'); check(doc.querySelectorAll('.checks .pending').length === 3, '미검증 항목 구분');
await click('렌더링 보기'); check(doc.querySelector('.canvas-tag').textContent.includes('도면 검토용'), '시각화 구분');
result.textContent = JSON.stringify({ status: 'PASS', checks }, null, 2);
} catch(e) { result.textContent = JSON.stringify({ status:'FAIL', error:e.message, checks }, null, 2); }
</script>
-7
View File
@@ -1,7 +0,0 @@
<!doctype html><meta charset="utf-8"><pre id="result">검증 중</pre><iframe src="/" style="width:390px;height:1450px;border:0"></iframe><script type="module">
const iframe=document.querySelector('iframe');
await new Promise(resolve=>iframe.addEventListener('load',resolve));
await new Promise(resolve=>setTimeout(resolve,1000));
const doc=iframe.contentDocument;
document.querySelector('#result').textContent=JSON.stringify({viewport:doc.documentElement.clientWidth,content:doc.documentElement.scrollWidth,body:doc.body.scrollWidth,pass:doc.documentElement.scrollWidth===doc.documentElement.clientWidth});
</script>
-4
View File
@@ -1,4 +0,0 @@
<!doctype html><meta charset="utf-8"><title>검토용 렌더링 미리보기</title><style>body{margin:0}iframe{border:0;width:100vw;height:100vh;display:block}</style><iframe src="/"></iframe><script type="module">
const iframe=document.querySelector('iframe');
for(let i=0;i<100;i++) { await new Promise(r=>setTimeout(r,100)); const button=[...(iframe.contentDocument?.querySelectorAll('button')||[])].find(b=>b.textContent==='렌더링 보기'); if(button){button.click();break;} }
</script>
-9
View File
@@ -1,9 +0,0 @@
import pathlib,json,html,re
root=pathlib.Path(__file__).resolve().parents[1]
for name in ['browser','mobile']:
file=root/f'docs/{name}-check-output.html'
source=file.read_text(encoding='utf-8-sig')
text=html.unescape(re.search(r'<pre id="result">(.*?)</pre>',source,re.S).group(1))
result=json.loads(text)
(root/f'docs/{name}-check-result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf8')
print(name,result.get('status',result.get('pass')),len(result.get('checks',[])))
-27
View File
@@ -1,27 +0,0 @@
import assert from 'node:assert/strict'
import { writeFileSync, mkdirSync } from 'node:fs'
import { model, exportOBJ, partFaces, profileVertices } from '../src/model/geometry.js'
const faces = model.parts.flatMap(partFaces)
assert.equal(faces.length, 468)
assert.equal(model.connection.status, '확인 필요')
for (const part of model.parts) {
const a = profileVertices(part, 0), b = profileVertices(part, 1)
assert.equal(a.length,b.length)
for(let i=0;i<a.length;i++) {
assert.ok(Math.abs(b[i][2]-a[i][2]-19.98)<1e-10)
assert.equal(a[i][0],part.a[i][0]); assert.equal(b[i][0],part.b[i][0])
assert.ok(Math.abs(a[i][1]+100-part.a[i][1])<1e-10)
assert.ok(Math.abs(b[i][1]+100-part.b[i][1])<1e-10)
}
}
const obj=exportOBJ()
const rows=obj.split('\n'), vertices=rows.filter(r=>r.startsWith('v '))
assert.equal(rows.filter(r=>r.startsWith('o ')).length,154)
assert.equal(rows.filter(r=>r.startsWith('f ')).length,468)
for(const row of rows.filter(r=>r.startsWith('f ')||r.startsWith('l '))) {
for(const n of row.split(' ').slice(1)) assert.ok(Number(n)>=1&&Number(n)<=vertices.length)
}
assert.equal(exportOBJ(new Set(model.parts.map(p=>p.id))).split('\n').filter(r=>r.startsWith('v ')).length,0)
mkdirSync('public/models',{recursive:true})
writeFileSync('public/models/BLOCK7_검토용모델.obj',obj)
console.log('직선 연결 19.980m, 468개 삼각면, 154개 객체, OBJ 참조·숨김 반영 검증 통과')
-30
View File
@@ -1,30 +0,0 @@
import json,math,pathlib
root=pathlib.Path(__file__).resolve().parents[1]
model=json.loads((root/'src/model/block7.json').read_text(encoding='utf8'))
parts=model['parts']; assert len(parts)==154
assert len({p['id'] for p in parts})==154
results=[]
for s,top,el in [('A',-74271.37855593952,106.675),('B',-91211.37855593952,107.307)]:
raw=json.loads((root/f'docs/extracted/section-{s}.json').read_text(encoding='utf8'))
assert len(raw)==114
assert sum(l['layer']=='CS-CONC-MAJR' for l in raw)==108
assert sum(l['layer']=='CS-CONC-MINR' for l in raw)==6
for original,part in zip(raw,parts[:114]):
assert original['handle']==part['handles'][0 if s=='A' else 1]
for key,point in zip(['a','b'],part[s.lower()]):
expected=[(original[key][0]-1128486.218066491)/1000,el+(original[key][1]-top)/1000]
assert all(abs(a-b)<0.000000051 for a,b in zip(point,expected))
results.append(f'{s}: 114개 선 HANDLE·레이어·양 끝점 원본 대조 통과 (좌표 반올림 오차 0.000051mm 이하)')
for p in parts:
assert len(p['a'])==len(p['b'])
assert all(math.isfinite(v) for s in ['a','b'] for point in p[s] for v in point)
assert sum(p['kind']=='arc' for p in parts)==4
assert abs(model['longitudinalStations'][1]-model['longitudinalStations'][0]-model['length'])<1e-10
assert model['sectionStations'][1]-model['sectionStations'][0]==20
for i,el in enumerate(model['sectionTops']):
assert abs(max(pt[1] for p in parts[:114] for pt in p['a' if i==0 else 'b'])-el)<1e-7
assert abs((parts[1]['a'][1][1]-parts[1]['a'][0][1]))<1e-7
results+=['154개 ID 중복 없음 / 모든 좌표 유한수 / 대응 꼭짓점 수 일치','원호 4개, 좌우 삽입 블록 40쌍 반영','벽체 상단 EL 및 종단 측점 차 19.980m 검산 통과','원본 단면 STA 간격 20.000m 보존; 사용자 허용에 따라 배치 간격만 19.980m로 대응 (verify_geometry.mjs에서 별도 검증)']
print('\n'.join(results))
(root/'docs/좌표검증결과.txt').write_text('\n'.join(results)+'\n',encoding='utf8')