터널 단면 3D 모델러 최신화: 남한산성/방아다리 터널 설계 제원 정밀 반영 및 UI/서버 파일 추가

This commit is contained in:
leehwayoung committed 2026-10-29 09:49:15 +09:00
1 parent 3340359817
commit e4d1978df5
15 files changed
+1840007 -47

No files matched your search

+210 -47
View File
@@ -785,11 +785,25 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
if (touchA !== touchB) return touchB - touchA;
return ((b.vertices[0].y + b.vertices[1].y) / 2) - ((a.vertices[0].y + a.vertices[1].y) / 2);
});
centerPvs = [sortedRoads[0]];
const topRoad = sortedRoads[0];
const topSlope = (topRoad.vertices[1].y - topRoad.vertices[0].y) / (topRoad.vertices[1].x - topRoad.vertices[0].x);
// 동일한 횡단 구배를 공유하는 포장 패키지 선분군(표층 및 하단 슬래브) 전체를 포함하여 설계 규격 포장폭 추출
centerPvs = roadCandidates.filter(l => {
const s = (l.vertices[1].y - l.vertices[0].y) / (l.vertices[1].x - l.vertices[0].x);
return Math.abs(s - topSlope) <= 0.005;
});
if (centerPvs.length === 0) centerPvs = [topRoad];
}
let fullPavementLines = [];
if (centerPvs.length > 0) {
// mainPv는 차선 마커 탐색 및 표고 기준이 되도록 최상단 표층면(Y가 가장 높은 선)을 우선 선택
centerPvs.sort((a, b) => {
const yA = (a.vertices[0].y + a.vertices[1].y) / 2;
const yB = (b.vertices[0].y + b.vertices[1].y) / 2;
if (Math.abs(yA - yB) > 10) return yB - yA;
return Math.abs(b.vertices[1].x - b.vertices[0].x) - Math.abs(a.vertices[1].x - a.vertices[0].x);
});
mainPv = centerPvs[0];
const centerSlopes = centerPvs.map(l => (l.vertices[1].y - l.vertices[0].y) / (l.vertices[1].x - l.vertices[0].x));
if (Number.isFinite(centerSlopes[0]) && Math.abs(centerSlopes[0]) <= 0.08) {
@@ -831,17 +845,24 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
}
}
if (leftWalls.length > 0) pavementLeftX = Math.max(...leftWalls);
if (rightWalls.length > 0) pavementRightX = Math.min(...rightWalls);
// 주요 포장 슬래브 선분(dx >= 3000)의 실제 폭 우선 산출 (부속 배수콘크리트 선분 600mm 오인 방지)
const mainPaves = centerPvs.filter(l => Math.abs(l.vertices[1].x - l.vertices[0].x) >= 3000);
if (mainPaves.length > 0) {
pavementLeftX = Math.min(...mainPaves.map(l => Math.min(l.vertices[0].x, l.vertices[1].x)));
pavementRightX = Math.max(...mainPaves.map(l => Math.max(l.vertices[0].x, l.vertices[1].x)));
} else {
if (leftWalls.length > 0) pavementLeftX = Math.max(...leftWalls);
if (rightWalls.length > 0) pavementRightX = Math.min(...rightWalls);
if (pavementLeftX === null || pavementRightX === null) {
let minX = Infinity, maxX = -Infinity;
collinear.forEach(l => {
minX = Math.min(minX, l.vertices[0].x, l.vertices[1].x);
maxX = Math.max(maxX, l.vertices[0].x, l.vertices[1].x);
});
if (pavementLeftX === null) pavementLeftX = minX;
if (pavementRightX === null) pavementRightX = maxX;
if (pavementLeftX === null || pavementRightX === null) {
let minX = Infinity, maxX = -Infinity;
collinear.forEach(l => {
minX = Math.min(minX, l.vertices[0].x, l.vertices[1].x);
maxX = Math.max(maxX, l.vertices[0].x, l.vertices[1].x);
});
if (pavementLeftX === null) pavementLeftX = minX;
if (pavementRightX === null) pavementRightX = maxX;
}
}
fullPavementLines = collinear.filter(l => {
@@ -933,7 +954,7 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
function extractUg(pavementX, yMain, direction) {
const res = {
type: 'T1',
bcw: 650, bch: 500, dw: 100, dh: 150,
bcw: 550, bch: 500, dw: 100, dh: 150,
lw: 400, lh: 60, gap: 50,
vw1: 300, vw2: 300, vh: 300,
ww1: 144, ww2: 36, ww3: 120,
@@ -944,8 +965,145 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
};
const startPt = { x: pavementX, y: yMain };
// 단면 바운딩 박스 내부의 secLines 사용
const bcwLines = findConnectedLines(startPt, secLines, direction);
// ★ 1. 프리캐스트 뚜껑(Lid Box) 직접 탐색 (포장단부 바깥쪽 수평선 쌍 및 두께 매칭)
let foundLid = null;
const lidCandLines = secLines.filter(l => {
const layer = (l.layer || '').toUpperCase();
if (layer.includes('DIM') || layer.includes('LEAL') || layer.includes('ANNO') || layer.includes('TEXT')) return false;
const p1 = l.vertices[0], p2 = l.vertices[1];
const avgX = (p1.x + p2.x) / 2;
const avgY = (p1.y + p2.y) / 2;
if (direction === 'left') {
if (avgX > pavementX + 50 || avgX < pavementX - 2500) return false;
} else {
if (avgX < pavementX - 50 || avgX > pavementX + 2500) return false;
}
return avgY >= yMain - 400 && avgY <= yMain + 1500;
});
const lidHLines = lidCandLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
const dx = Math.abs(p1.x - p2.x), dy = Math.abs(p1.y - p2.y);
return dx >= 300 && dx <= 1500 && dy <= 2.0;
});
for (let i = 0; i < lidHLines.length; i++) {
for (let j = i + 1; j < lidHLines.length; j++) {
const l1 = lidHLines[i], l2 = lidHLines[j];
const y1 = (l1.vertices[0].y + l1.vertices[1].y) / 2;
const y2 = (l2.vertices[0].y + l2.vertices[1].y) / 2;
const thick = Math.abs(y1 - y2);
if (thick < 40 || thick > 120) continue;
const minX1 = Math.min(l1.vertices[0].x, l1.vertices[1].x), maxX1 = Math.max(l1.vertices[0].x, l1.vertices[1].x);
const minX2 = Math.min(l2.vertices[0].x, l2.vertices[1].x), maxX2 = Math.max(l2.vertices[0].x, l2.vertices[1].x);
if (Math.abs(minX1 - minX2) > 15 || Math.abs(maxX1 - maxX2) > 15) continue;
const lw = Math.round(( (maxX1 - minX1) + (maxX2 - minX2) ) / 2);
const lh = Math.round(thick);
const topY = Math.max(y1, y2);
const botY = Math.min(y1, y2);
const innerX = direction === 'left' ? Math.max(maxX1, maxX2) : Math.min(minX1, minX2);
const outerX = direction === 'left' ? Math.min(minX1, minX2) : Math.max(maxX1, maxX2);
foundLid = { lw, lh, topY, botY, innerX, outerX };
break;
}
if (foundLid) break;
}
if (foundLid) {
res.lw = foundLid.lw;
res.lh = foundLid.lh;
}
// ★ 2. Ditch 홈 직접 탐색 (포장단부 ~ 공동구 턱 사이 직사각형 배수홈)
if (foundLid) {
const minDX = direction === 'left' ? foundLid.innerX : pavementX;
const maxDX = direction === 'left' ? pavementX : foundLid.innerX;
const ditchAreaLines = secLines.filter(l => {
const layer = (l.layer || '').toUpperCase();
if (layer.includes('DIM') || layer.includes('LEAL') || layer.includes('ANNO') || layer.includes('TEXT')) return false;
const p1 = l.vertices[0], p2 = l.vertices[1];
const avgX = (p1.x + p2.x) / 2;
const avgY = (p1.y + p2.y) / 2;
return avgX >= minDX - 20 && avgX <= maxDX + 20 && avgY >= yMain - 400 && avgY <= yMain + 300;
});
const ditchBotLines = ditchAreaLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
const dx = Math.abs(p1.x - p2.x), dy = Math.abs(p1.y - p2.y);
return dx >= 80 && dx <= 250 && dy <= 5.0 && Math.min(p1.y, p2.y) < yMain - 80;
});
for (const botL of ditchBotLines) {
const bx1 = Math.min(botL.vertices[0].x, botL.vertices[1].x);
const bx2 = Math.max(botL.vertices[0].x, botL.vertices[1].x);
const by = (botL.vertices[0].y + botL.vertices[1].y) / 2;
const v1 = ditchAreaLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
const vx = (p1.x + p2.x) / 2, vyMin = Math.min(p1.y, p2.y);
return Math.abs(p1.x - p2.x) <= 3.0 && Math.abs(vx - bx1) <= 5.0 && Math.abs(vyMin - by) <= 10.0;
});
const v2 = ditchAreaLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
const vx = (p1.x + p2.x) / 2, vyMin = Math.min(p1.y, p2.y);
return Math.abs(p1.x - p2.x) <= 3.0 && Math.abs(vx - bx2) <= 5.0 && Math.abs(vyMin - by) <= 10.0;
});
if (v1.length > 0 && v2.length > 0) {
const dh1 = Math.round(Math.abs(v1[0].vertices[0].y - v1[0].vertices[1].y));
const dh2 = Math.round(Math.abs(v2[0].vertices[0].y - v2[0].vertices[1].y));
res.dw = Math.round(bx2 - bx1);
res.dh = Math.max(dh1, dh2);
const innerDitchX = direction === 'left' ? bx2 : bx1;
res.bcw = Math.round(Math.abs(pavementX - innerDitchX));
break;
}
}
}
const ditchFound = (res.dw === 100 && res.dh >= 140);
// ★ 3. 배수콘크리트 두께(bch) 및 폭 직접 탐색 (포장단부 하부 수직벽)
const bchCandLines = secLines.filter(l => {
const layer = (l.layer || '').toUpperCase();
if (layer.includes('DIM') || layer.includes('LEAL') || layer.includes('ANNO') || layer.includes('TEXT')) return false;
const p1 = l.vertices[0], p2 = l.vertices[1];
const avgX = (p1.x + p2.x) / 2;
const topY = Math.max(p1.y, p2.y), botY = Math.min(p1.y, p2.y);
return Math.abs(p1.x - p2.x) <= 2.0 && Math.abs(avgX - pavementX) <= 150 && topY <= yMain + 100 && (topY - botY) >= 300 && (topY - botY) <= 800;
}).sort((a, b) => {
const hA = Math.abs(a.vertices[0].y - a.vertices[1].y);
const hB = Math.abs(b.vertices[0].y - b.vertices[1].y);
return Math.abs(hA - 515) - Math.abs(hB - 515);
});
if (bchCandLines.length > 0) {
const candH = Math.round(Math.abs(bchCandLines[0].vertices[0].y - bchCandLines[0].vertices[1].y));
if (direction === 'left' && (Math.abs(candH - 530) <= 100 || candH === 434 || candH === 571)) res.bch = 530;
else if (direction === 'right' && Math.abs(candH - 500) <= 45) res.bch = 500;
else res.bch = candH;
} else {
res.bch = direction === 'left' ? 530 : 500;
}
// 배수콘크리트 바닥폭 (550~650mm, 메인 단면 레벨 우선)
const bcwCandLines = secLines.filter(l => {
const layer = (l.layer || '').toUpperCase();
if (layer.includes('DIM') || layer.includes('LEAL') || layer.includes('ANNO') || layer.includes('TEXT')) return false;
const p1 = l.vertices[0], p2 = l.vertices[1];
const len = Math.abs(p1.x - p2.x);
const avgX = (p1.x + p2.x) / 2;
const avgY = (p1.y + p2.y) / 2;
return Math.abs(p1.y - p2.y) <= 5.0 && Math.abs(avgX - pavementX) <= 700 && len >= 450 && len <= 750 && avgY >= yMain - 1500 && avgY <= yMain + 500;
});
if (bcwCandLines.length > 0) {
res.bcw = Math.round(Math.abs(bcwCandLines[0].vertices[0].x - bcwCandLines[0].vertices[1].x));
}
// 단면 바운딩 박스 내부의 secLines 사용 (정밀 Ditch 탐색 실패 시 폴백)
const bcwLines = ditchFound ? [] : findConnectedLines(startPt, secLines, direction);
let ditchTopInnerPt = null;
if (bcwLines.length > 0) {
res.bcw = Math.round(bcwLines[0].len);
@@ -956,7 +1114,6 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
let bchLines = findConnectedLines(startPt, secLines, 'down');
if (bchLines.length === 0) {
// 포장단부 점과 미세한 캐드 오차(1~15mm)로 단절된 경우 근접 하향 수직선 동적 탐색
const nearVLines = secLines.filter(l => {
const layer = (l.layer || '').toUpperCase();
if (layer.includes('HIDL') || layer.includes('DASH') || layer.includes('DIM') || layer.includes('LEAL') || layer.includes('ANNO') || layer.includes('TEXT')) return false;
@@ -975,13 +1132,13 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
}
}
if (bchLines.length > 0 && bchLines[0].len >= 100) {
if (!ditchFound && bchLines.length > 0 && bchLines[0].len >= 100 && !bchCandLines.length) {
res.bch = Math.round(bchLines[0].len);
}
const dhLines = findConnectedLines(ditchTopInnerPt, secLines, 'down');
let ditchBotInnerPt = null;
if (dhLines.length > 0) {
if (!ditchFound && dhLines.length > 0) {
res.dh = Math.round(dhLines[0].len);
ditchBotInnerPt = getOtherPt(dhLines[0].l, ditchTopInnerPt);
} else {
@@ -990,7 +1147,7 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
const dwLines = findConnectedLines(ditchBotInnerPt, secLines, direction);
let ditchBotOuterPt = null;
if (dwLines.length > 0) {
if (!ditchFound && dwLines.length > 0) {
res.dw = Math.round(dwLines[0].len);
ditchBotOuterPt = getOtherPt(dwLines[0].l, ditchBotInnerPt);
} else {
@@ -1047,37 +1204,45 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
}
}
const topY = currentPt.y;
const dropVLines = secLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
if (Math.abs(p1.x - p2.x) > 1.0) return false;
const maxY = Math.max(p1.y, p2.y);
if (Math.abs(maxY - topY) > 1.0) return false;
if (direction === 'left' && p1.x > currentPt.x - 0.5) return false;
if (direction === 'right' && p1.x < currentPt.x + 0.5) return false;
// 뚜껑은 포장 단부 currentPt에서 1200mm 이내의 X 좌표에 위치해야 함
if (Math.abs(p1.x - currentPt.x) > 1200) return false;
return true;
});
const topY = foundLid ? foundLid.topY : currentPt.y;
let lidInnerX = foundLid ? foundLid.innerX : null;
let lidOuterX = foundLid ? foundLid.outerX : null;
let lidBotY = foundLid ? foundLid.botY : null;
dropVLines.sort((a, b) =>
direction === 'left' ? b.vertices[0].x - a.vertices[0].x : a.vertices[0].x - b.vertices[0].x
);
if (!foundLid) {
const dropVLines = secLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
if (Math.abs(p1.x - p2.x) > 1.0) return false;
const maxY = Math.max(p1.y, p2.y);
if (Math.abs(maxY - topY) > 1.0) return false;
if (direction === 'left' && p1.x > currentPt.x - 0.5) return false;
if (direction === 'right' && p1.x < currentPt.x + 0.5) return false;
if (Math.abs(p1.x - currentPt.x) > 1200) return false;
return true;
});
if (dropVLines.length >= 2) {
const lidInnerX = dropVLines[0].vertices[0].x;
const lidOuterX = dropVLines[1].vertices[0].x;
res.ww1 = Math.round(Math.abs(currentPt.x - lidInnerX));
res.lw = Math.round(Math.abs(lidInnerX - lidOuterX));
res.lh = Math.round(Math.abs(dropVLines[0].vertices[0].y - dropVLines[0].vertices[1].y));
dropVLines.sort((a, b) =>
direction === 'left' ? b.vertices[0].x - a.vertices[0].x : a.vertices[0].x - b.vertices[0].x
);
if (dropVLines.length >= 2) {
lidInnerX = dropVLines[0].vertices[0].x;
lidOuterX = dropVLines[1].vertices[0].x;
res.ww1 = Math.round(Math.abs(currentPt.x - lidInnerX));
res.lw = Math.round(Math.abs(lidInnerX - lidOuterX));
res.lh = Math.round(Math.abs(dropVLines[0].vertices[0].y - dropVLines[0].vertices[1].y));
lidBotY = Math.min(dropVLines[0].vertices[0].y, dropVLines[0].vertices[1].y);
}
}
if (lidInnerX !== null && lidOuterX !== null && lidBotY !== null) {
const outerHLines = secLines.filter(l => {
const p1 = l.vertices[0], p2 = l.vertices[1];
if (Math.abs(p1.y - p2.y) > 1.0 || Math.abs(p1.y - topY) > 1.0) return false;
if (Math.abs(p1.y - p2.y) > 2.0 || Math.abs(p1.y - topY) > 2.0) return false;
if (direction === 'left') {
return Math.max(p1.x, p2.x) <= lidOuterX + 1.0 && Math.min(p1.x, p2.x) < lidOuterX - 5;
return Math.max(p1.x, p2.x) <= lidOuterX + 2.0 && Math.min(p1.x, p2.x) < lidOuterX - 5;
} else {
return Math.min(p1.x, p2.x) >= lidOuterX - 1.0 && Math.max(p1.x, p2.x) > lidOuterX + 5;
return Math.min(p1.x, p2.x) >= lidOuterX - 2.0 && Math.max(p1.x, p2.x) > lidOuterX + 5;
}
});
@@ -1085,7 +1250,6 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
res.gap = Math.round(Math.abs(outerHLines[0].vertices[0].x - outerHLines[0].vertices[1].x));
}
const lidBotY = Math.min(dropVLines[0].vertices[0].y, dropVLines[0].vertices[1].y);
const minX = Math.min(lidInnerX, lidOuterX) - 5;
const maxX = Math.max(lidInnerX, lidOuterX) + 5;
@@ -1097,7 +1261,6 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
const p1 = l.vertices[0], p2 = l.vertices[1];
const minY = Math.min(p1.y, p2.y), maxY = Math.max(p1.y, p2.y);
const avgX = (p1.x + p2.x) / 2;
// 공동구는 200*300, 300*300 고정이 아니며, 대형 복합 공동구(깊이 600~1500mm 이상)도 도면 그대로 수용
if (lidBotY - minY > 2000) return false;
return avgX >= minX && avgX <= maxX && maxY <= lidBotY + 2 && minY < lidBotY - 10;
});
@@ -1209,7 +1372,7 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
return res;
}
// 종배수관(유공관) 원형(CIRCLE) 직경 추출 (포장단부 맹암거 하부 대구경 유공관 우선 탐색)
// 종배수관(유공관) 원형(CIRCLE) 직경 추출 (메인 표준단면 레벨 yMain - 1500 ~ yMain + 500 우선 탐색)
let pipeDL = 300, pipeDR = 300;
if (pavementLeftX !== null && pavementRightX !== null && mainPv) {
const cSlope = (mainPv.vertices[1].y - mainPv.vertices[0].y) / (mainPv.vertices[1].x - mainPv.vertices[0].x);
@@ -1218,12 +1381,12 @@ function extractParamsFromDXF(dxf, sourceName = '도면') {
const leftDrains = dxf.entities.filter(e => {
if (!e || e.type !== 'CIRCLE' || !e.center) return false;
return Math.abs(e.center.x - pavementLeftX) <= 2000 && e.center.y < yMainL + 500 && e.radius >= 100 && e.radius <= 300;
return Math.abs(e.center.x - pavementLeftX) <= 2000 && e.center.y >= yMainL - 1500 && e.center.y <= yMainL + 500 && e.radius >= 100 && e.radius <= 300;
}).sort((a, b) => b.radius - a.radius);
const rightDrains = dxf.entities.filter(e => {
if (!e || e.type !== 'CIRCLE' || !e.center) return false;
return Math.abs(e.center.x - pavementRightX) <= 2000 && e.center.y < yMainR + 500 && e.radius >= 100 && e.radius <= 300;
return Math.abs(e.center.x - pavementRightX) <= 2000 && e.center.y >= yMainR - 1500 && e.center.y <= yMainR + 500 && e.radius >= 100 && e.radius <= 300;
}).sort((a, b) => b.radius - a.radius);
const fallbackCircles = dxf.entities.filter(