feat(rebar): add 3D coupler modeling for P1/P2 (368) and P4/P5 (35) with separate summary table rows

This commit is contained in:
ypshim committed 2026-10-22 18:00:28 +09:00
1 parent 9900aa6d8e
commit b35f5ff0ed
5 files changed
+70 -6

No files matched your search

+5 -1
View File
@@ -57,6 +57,8 @@ const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'd
C16: { spacing: '250', actual: '250', position: '계단부 3·4구간', direction: '교각축', shape: 'L형', basis: 'calc' },
P1: { spacing: '원주 46등분', actual: '—', position: '외측 r 875', direction: '연직', shape: '아래 끝 90° 갈고리, 커플러 이음', basis: 'calc' },
P2: { spacing: '원주 46등분', actual: '—', position: '내측 r 843.2', direction: '연직', shape: '직선, 커플러 이음', basis: 'calc' },
'주철근 커플러': { spacing: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
'띠철근 커플러': { spacing: '120', actual: '120', position: '단부 P5 · 기초안 P4', direction: '원통', shape: 'L 100 · Ø40', basis: 'assumed' },
커플러: { spacing: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
P3: { spacing: '300', actual: '300', position: '일반 구간 (코핑 안 포함)', direction: '원형', shape: '겹침 (추천안)', basis: 'assumed' },
P5: { spacing: '120', actual: '120', position: '단부 구간 (첫 P3 아래)', direction: '원형', shape: '반원 2개가 아닌 원형 1개, 커플러 1개', basis: 'calc' },
@@ -188,7 +190,9 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
const col = columnRebar(COLUMN_P1, m);
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, note: `외측 r ${col.r1}, 마디 ${col.p1Pieces.join(' / ')} (첫 마디에 갈고리 ${col.p1Hook}, 기둥철근.md §3.2)` });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
rows.push({ member: 'column', name: '커플러', size: 'L150 Ø60', count: col.couplers, length: 150, note: '@3000, P2 +150' });
rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: '@3000, P2는 +150 mm 상향 엇갈림' });
const tieCouplerCount = col.p5Count + col.p4Count * 2;
rows.push({ member: 'column', name: '띠철근 커플러', size: 'D19용 (L100 Ø40)', count: tieCouplerCount, length: 100, note: `단부 P5 1개(${col.p5Count}) · 기초안 P4 2개(${col.p4Count * 2}) 폐합용` });
rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @300, 겹침 ${col.lapP3} (추천안, Q-C6)` });
rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: '단부 띠철근 @120, 커플러 1개' });
rows.push({ member: 'column', name: 'P4', size: 'H19', count: col.p4Count, length: col.p4SemicircleLength * 2, note: '확대기초 안, 반원 2개 (길이는 반원 2개 합) · B11 간섭 무시' });
+2 -2
View File
@@ -710,7 +710,7 @@ function placeViewer(stage: 'shape' | 'plan' | 'rebar'): void {
function memberOfBar(name: string): RebarRow['member'] {
if (name === 'S1' || name === 'S2') return 'bearing';
if (name.startsWith('B')) return 'footing';
if (name.startsWith('P') || name === '커플러') return 'column';
if (name.startsWith('P') || name.includes('커플러')) return 'column';
return 'coping';
}
@@ -897,7 +897,7 @@ const ROLE: Record<string, string> = {
S1: '받침블럭', S2: '받침블럭',
C1: '주철근', C2: '주철근', C3: '주철근', C4: '주철근', C5: '주철근', C6: '주철근', C7: '주철근', C8: '주철근',
C9: '전단·띠', C10: '전단·띠', C11: '전단·띠', C12: '측면 수평', C13: '측면 수평', C14: '직각 수평', C15: '계단부', C16: '계단부',
P1: '주철근', P2: '주철근', 커플러: '조립', P3: '띠철근', P5: '띠철근', P4: '띠철근', P6: '크로스타이',
P1: '주철근', P2: '주철근', '주철근 커플러': '조립', '띠철근 커플러': '조립', 커플러: '조립', P3: '띠철근', P5: '띠철근', P4: '띠철근', P6: '크로스타이',
B1: '주철근', B2: '주철근', B3: '주철근', B4: '주철근', B5: '주철근', B6: '주철근', B7: '주철근', B8: '주철근',
'B10-1': '측면 수평', 'B10-2': '측면 수평', B11: '조립 수평', B12: '조립 수평', B9: '전단·띠',
};
+56 -2
View File
@@ -25,6 +25,8 @@ const COLOR = {
shear: 0x6b3fa0,
column: 0x0f5c66,
tie: 0x6b3fa0,
couplerMain: 0x0b3f46,
couplerTie: 0x553088,
footing: 0x0f5c66,
footingBottom: 0x2e7d32,
block: 0xa07a10,
@@ -213,6 +215,52 @@ export function buildRebar3D(m: PierModel): RebarScene {
for (const z of col.p3) ring('P3', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19);
for (const z of col.p5) ring('P5', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19);
for (const z of col.p4) ring('P4', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19);
// Main bar couplers (D32, L150 Ø60, 4단 × 46줄 × 2열 = 368개): P1 at col.couplerP1, P2 at col.couplerP2 (+150 mm)
const r1 = mm(col.r1);
const r2 = mm(col.r2);
for (let i = 0; i < COLUMN_P1.nDiv; i++) {
const th = (2 * Math.PI * i) / COLUMN_P1.nDiv;
const cosTh = Math.cos(th);
const sinTh = Math.sin(th);
// P1 couplers (184)
for (const z of col.couplerP1) {
const cx = sc + r1 * cosTh;
const cz = r1 * sinTh;
const cy = base + mm(z);
addBar('주철근 커플러', c.k, [cx, cy - 0.075, cz], [cx, cy + 0.075, cz], 60, COLOR.couplerMain);
}
// P2 couplers (184, shifted +150 mm)
for (const z of col.couplerP2) {
const cx = sc + r2 * cosTh;
const cz = r2 * sinTh;
const cy = base + mm(z);
addBar('주철근 커플러', c.k, [cx, cy - 0.075, cz], [cx, cy + 0.075, cz], 60, COLOR.couplerMain);
}
}
// Tie couplers (D19, L100 Ø40, 총 35개): P5 1개 (19개) · P4 2개 (16개)
col.p5.forEach((z, j) => {
const th = (2 * Math.PI * (j * 7 + 0.5)) / COLUMN_P1.nDiv;
const cx = sc + rt * Math.cos(th);
const cz = rt * Math.sin(th);
const cy = base + mm(z);
const dx = 0.05 * -Math.sin(th);
const dz = 0.05 * Math.cos(th);
addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie);
});
col.p4.forEach((z) => {
const angles = [(2 * Math.PI * 0.5) / COLUMN_P1.nDiv, (2 * Math.PI * 0.5) / COLUMN_P1.nDiv + Math.PI];
angles.forEach((th) => {
const cx = sc + rt * Math.cos(th);
const cz = rt * Math.sin(th);
const cy = base + mm(z);
const dx = 0.05 * -Math.sin(th);
const dz = 0.05 * Math.cos(th);
addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie);
});
});
const hookP6 = mm(Math.round(hookLength('tie90', NOMINAL_DIAMETER.H16))); // 135 mm
const dP6 = NOMINAL_DIAMETER.H16; // 15.9 mm
const p6Angles = [0, 46.96, 90, 133.04];
@@ -373,10 +421,16 @@ export function buildRebar3D(m: PierModel): RebarScene {
for (const [key, meshes] of byName) {
for (const mesh of meshes) {
const mat = mesh.material as THREE.MeshStandardMaterial;
const on = name === null || key === name;
const on =
name === null ||
key === name ||
(name === '커플러' && key.includes('커플러')) ||
(name === '주철근 커플러' && key === '주철근 커플러') ||
(name === '띠철근 커플러' && key === '띠철근 커플러');
mat.transparent = !on;
mat.opacity = on ? 1 : 0.18;
mat.emissive.setHex(name !== null && key === name ? 0x6b5a00 : 0x000000);
const match = name !== null && (key === name || (name === '커플러' && key.includes('커플러')));
mat.emissive.setHex(match ? 0x6b5a00 : 0x000000);
}
}
};
+2 -1
View File
@@ -27,7 +27,8 @@ describe('③ 철근표 요약 (P1)', () => {
it('column: P1 230, P2 230, couplers 368, P3 38, P5 19, P4 8, P6 32', () => {
expect(count('P1')).toBe(230);
expect(count('P2')).toBe(230);
expect(count('커플러')).toBe(368);
expect(count('주철근 커플러')).toBe(368);
expect(count('띠철근 커플러')).toBe(35);
expect(count('P3')).toBe(38);
expect(count('P5')).toBe(19);
expect(count('P4')).toBe(8);
+5
View File
@@ -67,6 +67,11 @@ describe('3D 철근 모델 (③)', () => {
expect(rb.byName.get('P6')!.length).toBe(8 * 4 * 3);
});
it('couplers in 3D: main bar couplers (368) and tie couplers (35: P5 19 + P4 16)', () => {
expect(rb.byName.get('주철근 커플러')!.length).toBe(368);
expect(rb.byName.get('띠철근 커플러')!.length).toBe(35);
});
it('highlight dims the other bars and keeps the chosen one', () => {
rb.highlight('P3');
const p3 = (rb.byName.get('P3')![0].material as THREE.MeshStandardMaterial).opacity;