Files
dxf-to-3d-gasiseol/src/build/generate-v4.mjs
T
Claude HaikuandClaude Haiku 4.5 d6abe6707a v4: 평면·단면 기반 통합모델 생성기와 뷰어 추가
계획도 블록 중심 보정과 A-A 단면 표고(말뚝 6그룹, 띠장, 굴착 바닥, 도로면)를 반영해
엄지말뚝·토류판·띠장·수직 PIECE·버팀보·스크류잭·채널·중간말뚝을 배치한다.
뷰어에 부재별 온오프·투명도, 거리 측정, 시점 전환, 부드러운 휠 줌을 추가한다.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-21 10:45:48 +09:00

219 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* generate-v4.mjs - 평면도 기준 통합 모델 (엄지말뚝 2열 기준 재배치)
*
* 기준 (사용자 제공):
* - 엄지말뚝 상단 열 A 시작점 (-1248.4329, -2617.9386), 하단 열 B 시작점 (-1248.4329, -2631.1386)
* - 각 열은 X 방향 1m 간격 61개 (엄지말뚝 h-pile2), 열 사이 Y 13.2m
* - 토류판은 같은 열의 엄지말뚝 사이에만 60개씩 배치
*
* 나머지 부재 (가시설4, H-PILE, 스터럿): 계획도 데이터에서 말뚝 열 4개 중 상단/하단 열과의
* 상대 위치(Δx, Δy)를 유지해 A/B 열 기준으로 재배치.
*/
import DxfParser from 'dxf-parser';
import fs from 'fs';
const MEMBERS = {
'h-pile2': { memberType: '엄지말뚝', spec: 'H400×400×13/21', shape: 'H', h: 0.40, w: 0.40, tf: 0.021, tw: 0.013, length: 8.5, axis: 'vertical' },
'띠장': { memberType: '띠장', spec: 'H300×300×10/15', shape: 'H', h: 0.30, w: 0.30, tf: 0.015, tw: 0.010, length: 60, axis: 'horizontal' },
'토류판': { memberType: '토류판', spec: 'T=100 (0.9×0.1)', shape: 'PANEL', w: 0.90, t: 0.10, length: 8.5, axis: 'vertical' },
'H-PILE': { memberType: '중간말뚝', spec: 'H300×300×10/15', shape: 'H', h: 0.30, w: 0.30, tf: 0.015, tw: 0.010, length: 8.5, axis: 'vertical' },
'가시설4': { memberType: '수직 H300 (미확정)', spec: 'H300×300×10/15', shape: 'H', h: 0.30, w: 0.30, tf: 0.015, tw: 0.010, length: 8.5, axis: 'vertical' },
'스터럿': { memberType: '버팀보', spec: 'H300×300×10/15', shape: 'H', h: 0.30, w: 0.30, tf: 0.015, tw: 0.010, length: 12.8, axis: 'horizontal' },
'수직PIECE': { memberType: '수직 PIECE', spec: 'H300×300×10/15', shape: 'H', h: 0.30, w: 0.30, tf: 0.015, tw: 0.010, length: 0.6, axis: 'vertical' },
'스크류잭': { memberType: '스크류잭', spec: '1000kN', shape: 'JACK', h: 0.309, w: 0.30, length: 0.5995, axis: 'jackY' },
'채널': { memberType: '채널', spec: 'ㄷ-380×100×10.5/16', shape: 'CHANNEL', h: 0.38, w: 0.10, tf: 0.016, tw: 0.0105, length: 1, axis: 'channelX' },
'버팀보Y': { memberType: '버팀보', spec: 'H300×300×10/15', shape: 'H', h: 0.30, w: 0.30, tf: 0.015, tw: 0.010, length: 12.2, axis: 'horizontalY' }
};
const ROW_A = { x: -1248.4329, y: -2617.9386 };
const ROW_B = { x: -1248.4329, y: -2631.1386 };
const PILES_PER_ROW = 61;
const SPACING = 1.0;
// A-A 단면: 엄지말뚝 상단 EL 77.97, 하단 EL 69.47 (L=8.5). 띠장 2단 EL 77.1, 74.9
const PILE_LENGTH = 8.5;
const PILE_CENTER_Z = 77.97 - PILE_LENGTH / 2;
const WALE_LEVELS = [77.1, 74.9];
const WALE_LENGTH = (PILES_PER_ROW - 1) * SPACING;
const PILE_HALF = 0.2;
const WALE_HALF = 0.15;
const PANEL_HALF = 0.05;
const FLANGE_T = 0.021;
const plan = new DxfParser().parse(fs.readFileSync('RAG/LH/가시설계획_LH.dxf', 'utf-8'));
const inserts = plan.entities.filter(e => e.type === 'INSERT' && MEMBERS[e.name]);
const parts = [];
const pushPart = (blockName, pos) => {
const m = MEMBERS[blockName];
parts.push({
blockType: blockName,
memberType: m.memberType,
spec: m.spec,
shape: m.shape,
dims: { h: m.h, w: m.w, tf: m.tf, tw: m.tw, t: m.t, length: pos.length ?? m.length },
axis: m.axis,
position: { x: +pos.x.toFixed(4), y: +pos.y.toFixed(4), z: +(pos.z ?? PILE_CENTER_Z).toFixed(4) }
});
};
// A-A 단면: 벽체 방향 X 구간별 말뚝 상단 표고 (0.3m 단위 6그룹, 말뚝 길이 8.5m)
const SECTION_X0 = -1136.05, SECTION_X1 = -1080.05;
const GROUP_EDGES = [-1128.40, -1119.40, -1110.40, -1096.40, -1086.40];
const GROUP_TOPS = [77.97, 77.67, 77.37, 77.07, 76.77, 76.47];
const groupOf = secX => {
let g = 0;
while (g < GROUP_EDGES.length && secX > GROUP_EDGES[g]) g++;
return g;
};
const pileGroup = j => {
const secX = SECTION_X0 + ((WALE_LENGTH - j * SPACING) / WALE_LENGTH) * (SECTION_X1 - SECTION_X0);
return groupOf(secX);
};
const topOfPile = j => GROUP_TOPS[pileGroup(j)];
[ROW_A, ROW_B].forEach(row => {
for (let i = 0; i < PILES_PER_ROW; i++) {
const top = topOfPile(i);
pushPart('h-pile2', { x: row.x + i * SPACING, y: row.y, z: top - PILE_LENGTH / 2 });
}
const panelInner = (row === ROW_A ? -1 : 1) * (PILE_HALF - FLANGE_T - PANEL_HALF);
for (let j = 0; j < PILES_PER_ROW - 1; j++) {
const top = Math.min(topOfPile(j), topOfPile(j + 1));
pushPart('토류판', { x: row.x + j * SPACING + SPACING / 2, y: row.y + panelInner, z: top - PILE_LENGTH / 2 });
}
const innerSign = row === ROW_A ? -1 : 1;
const waleY = row.y + innerSign * (PILE_HALF + WALE_HALF);
for (let g = 0; g < GROUP_TOPS.length; g++) {
const piles = [];
for (let j = 0; j < PILES_PER_ROW; j++) if (pileGroup(j) === g) piles.push(j);
if (piles.length === 0) continue;
const x0 = row.x + piles[0] * SPACING;
const x1 = row.x + piles[piles.length - 1] * SPACING;
const shift = GROUP_TOPS[g] - GROUP_TOPS[0];
WALE_LEVELS.forEach(level => {
const c = level + shift;
[c - WALE_HALF, c + WALE_HALF].forEach(z => {
pushPart('띠장', { x: (x0 + x1) / 2, y: waleY, z, length: Math.max(x1 - x0, SPACING) + 2 * PILE_HALF });
});
});
}
});
const blockCenter = name => {
const xs = [], ys = [];
plan.blocks[name].entities.forEach(e => {
if (e.vertices) e.vertices.forEach(v => { xs.push(v.x); ys.push(v.y); });
if (e.center && e.radius) { xs.push(e.center.x - e.radius, e.center.x + e.radius); ys.push(e.center.y - e.radius, e.center.y + e.radius); }
});
return { x: (Math.min(...xs) + Math.max(...xs)) / 2, y: (Math.min(...ys) + Math.max(...ys)) / 2 };
};
const trueCenter = i => {
const c = blockCenter(i.name), r = ((i.rotation || 0) * Math.PI) / 180;
return {
x: i.position.x + c.x * Math.cos(r) - c.y * Math.sin(r),
y: i.position.y + c.x * Math.sin(r) + c.y * Math.cos(r)
};
};
const inWall = p => p.x >= ROW_A.x - 0.5 && p.x <= ROW_A.x + WALE_LENGTH + 0.5 && p.y >= ROW_B.y - 1 && p.y <= ROW_A.y + 1;
const planCenters = name => plan.entities.filter(e => e.type === 'INSERT' && e.name === name).map(trueCenter).filter(inWall);
const BUTT_STATIONS = [0.5, ...Array.from({ length: 12 }, (_, k) => 4.5 + 5 * k)];
const WALE_OFFSET = PILE_HALF + WALE_HALF;
const waleYA = ROW_A.y - WALE_OFFSET;
const waleYB = ROW_B.y + WALE_OFFSET;
const PIECE_HALF = 0.15;
const pieceYA = waleYA - WALE_HALF - PIECE_HALF;
const pieceYB = waleYB + WALE_HALF + PIECE_HALF;
const BUTT_CENTER_Y = (pieceYA + pieceYB) / 2;
const JACK_L = 0.5995;
const BUTT_LENGTH = (pieceYA - pieceYB) - 2 * PIECE_HALF - JACK_L;
const buttEndA = pieceYA - PIECE_HALF;
const buttEndB = pieceYB + PIECE_HALF;
const jackOnA = i => (BUTT_STATIONS.length - 1 - i) % 2 === 0;
BUTT_STATIONS.forEach((s, i) => {
const g = pileGroup(Math.round(s / SPACING));
const shift = GROUP_TOPS[g] - GROUP_TOPS[0];
const onA = jackOnA(i);
const buttY = onA ? (buttEndB + buttEndA - JACK_L) / 2 : (buttEndB + JACK_L + buttEndA) / 2;
WALE_LEVELS.forEach(level => {
const c = level + shift;
pushPart('수직PIECE', { x: ROW_A.x + s, y: pieceYA, z: c });
pushPart('수직PIECE', { x: ROW_B.x + s, y: pieceYB, z: c });
pushPart('스크류잭', onA
? { x: ROW_A.x + s, y: buttEndA - JACK_L / 2, z: c }
: { x: ROW_A.x + s, y: buttEndB + JACK_L / 2, z: c });
pushPart('버팀보Y', { x: ROW_A.x + s, y: buttY, z: c, length: BUTT_LENGTH });
});
});
const hpCenters = planCenters('H-PILE');
hpCenters.forEach(p => {
const j = Math.round((p.x - ROW_A.x) / SPACING);
pushPart('H-PILE', { x: p.x, y: p.y, z: topOfPile(j) - PILE_LENGTH / 2 });
});
const CHANNEL_H = 0.38, CHANNEL_B = 0.10, H_PILE_HALF = 0.15;
const runs = [];
[...hpCenters].sort((a, b) => a.x - b.x).forEach(p => {
const last = runs[runs.length - 1];
if (last && Math.abs(last.y - p.y) < 0.01) last.xs.push(p.x);
else runs.push({ y: p.y, xs: [p.x] });
});
runs.forEach(r => {
const x0 = Math.min(...r.xs), x1 = Math.max(...r.xs);
const xc = (x0 + x1) / 2;
const g = pileGroup(Math.round((xc - ROW_A.x) / SPACING));
const shift = GROUP_TOPS[g] - GROUP_TOPS[0];
const cy = r.y > BUTT_CENTER_Y ? r.y - H_PILE_HALF - CHANNEL_B / 2 : r.y + H_PILE_HALF + CHANNEL_B / 2;
WALE_LEVELS.forEach(level => {
const c = level + shift;
pushPart('채널', { x: xc, y: cy, z: c + WALE_HALF + CHANNEL_H / 2, length: x1 - x0 + 2 * H_PILE_HALF });
});
});
const counts = {};
parts.forEach(p => { counts[p.memberType] = (counts[p.memberType] || 0) + 1; });
// 굴착 바닥: A-A 녹색 경사선 (단면 X -1146.49, EL 72.91 → X -1070.91, EL 70.58) 을 벽체 X 범위에 대응
const GREEN_P0 = { secX: -1146.49, el: 72.91 }, GREEN_P1 = { secX: -1070.91, el: 70.58 };
const greenEl = secX => GREEN_P0.el + ((secX - GREEN_P0.secX) * (GREEN_P1.el - GREEN_P0.el)) / (GREEN_P1.secX - GREEN_P0.secX);
const secOfPlanX = x => SECTION_X1 - ((x - ROW_A.x) * (SECTION_X1 - SECTION_X0)) / WALE_LENGTH;
const EXC_STEP = 1;
const SURFACE_EXTEND = 5;
const excStations = Array.from({ length: (WALE_LENGTH + 2 * SURFACE_EXTEND) / EXC_STEP + 1 }, (_, k) => ROW_A.x - SURFACE_EXTEND + k * EXC_STEP);
const excavation = {
yA: ROW_A.y - PILE_HALF,
yB: ROW_B.y + PILE_HALF,
profile: excStations.map(x => ({ x, z: greenEl(secOfPlanX(x)) }))
};
// 도로면: A-A 가장 높은 녹색 점선 (단면 X -1146.49, EL 77.01 → X -1070.91, EL 74.68), 엄지말뚝 외측 양쪽
const ROAD_P0 = { secX: -1146.49, el: 77.01 }, ROAD_P1 = { secX: -1070.91, el: 74.68 };
const roadEl = secX => ROAD_P0.el + ((secX - ROAD_P0.secX) * (ROAD_P1.el - ROAD_P0.el)) / (ROAD_P1.secX - ROAD_P0.secX);
const ROAD_WIDTH = 10;
const roadProfile = excStations.map(x => ({ x, z: roadEl(secOfPlanX(x)) }));
const road = [
{ y0: ROW_A.y + PILE_HALF, y1: ROW_A.y + PILE_HALF + ROAD_WIDTH, profile: roadProfile },
{ y0: ROW_B.y - PILE_HALF - ROAD_WIDTH, y1: ROW_B.y - PILE_HALF, profile: roadProfile }
];
const xs = parts.map(p => p.position.x), ys = parts.map(p => p.position.y);
const stats = {
timestamp: new Date().toISOString(),
method: 'v4-plan-2row',
unit: 'm',
origin: '평면도 좌표 (엄지말뚝 열 A/B 시작점 기준)',
totalParts: parts.length,
partsByType: counts,
parts,
excavation,
road
};
fs.writeFileSync('out/gasiseol-v4-stats.json', JSON.stringify(stats, null, 2), 'utf-8');
console.log('부재 구성:', counts);
console.log(`범위 X ${Math.min(...xs).toFixed(1)}~${Math.max(...xs).toFixed(1)} Y ${Math.min(...ys).toFixed(1)}~${Math.max(...ys).toFixed(1)}`);