계획도 블록 중심 보정과 A-A 단면 표고(말뚝 6그룹, 띠장, 굴착 바닥, 도로면)를 반영해 엄지말뚝·토류판·띠장·수직 PIECE·버팀보·스크류잭·채널·중간말뚝을 배치한다. 뷰어에 부재별 온오프·투명도, 거리 측정, 시점 전환, 부드러운 휠 줌을 추가한다. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
219 lines
10 KiB
JavaScript
219 lines
10 KiB
JavaScript
#!/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)}`);
|