feat(rebar): 조립 철근(B11·B12)을 격자 가운데에 배치 (양끝 여유 차이 ≤ 1칸)

- assemblyGridIndices: 단마다 k칸 간격, 엇갈림 2칸 유지, 짝수 단 시작칸을 가운데 정렬
- 개수는 그대로 (B11 30, B12 24), 3D와 단면 삽도가 같은 위치를 씀
- 테스트: 양끝 여유 차이 ≤ 1칸, 엇갈림 2칸

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 17:47:21 +09:00
1 parent 20d8546b6f
commit de20b84c00
4 files changed
+34 -16

No files matched your search

+17 -8
View File
@@ -138,19 +138,28 @@ export function assemblyEndOffset(): number {
}
/** B11 (along W, between the B10-2 levels) and B12 (along L), 4s apart with a 2s stagger per level (§8). */
/**
* Grid indices of the assembly bars (B11 or B12) on one level, centred in the grid (승인 2026-10-26, 추천안).
* Bars every k steps; the level's 2-step stagger alternates (even levels start at e0, odd levels at e0 + 2), and e0 centres
* the even group so the left and right gaps differ by at most one step. n = positions − 1; idx = level index (0, 1, …).
*/
export function assemblyGridIndices(n: number, k: number, idx: number): number[] {
const cEven = Math.floor(n / k) + 1;
const cOdd = Math.floor((n - 2) / k) + 1;
const e0 = Math.max(0, Math.floor((n - (cEven - 1) * k - 2) / 2));
const even = idx % 2 === 0;
const start = even ? e0 : e0 + 2;
const count = even ? cEven : cOdd;
return Array.from({ length: count }, (_, m) => start + m * k).filter((i) => i <= n);
}
export function assemblyBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] {
const levelsJ = assemblyJs(H, f);
// Bars every k grid steps (승인 2026-10-26: k·s); the levels alternate a 2-step stagger (엇갈림 2s).
const perLevel = (n: number, k: number) =>
levelsJ.map((_, idx) => {
const i0 = idx % 2 === 0 ? 0 : 2;
return Math.floor((n - i0) / k) + 1;
});
// B11 is spread along L (교각축 직각 grid) and B12 along W (교각축 grid), per 확대기초철근 (1).md §8.
const nW = grid(W, f, 'sAxis').positions - 1;
const nL = grid(L, f, 'sPerp').positions - 1;
const b11 = perLevel(nL, f.kB11).reduce((a, b) => a + b, 0);
const b12 = perLevel(nW, f.kB12).reduce((a, b) => a + b, 0);
const b11 = levelsJ.reduce((a, _, idx) => a + assemblyGridIndices(nL, f.kB11, idx).length, 0);
const b12 = levelsJ.reduce((a, _, idx) => a + assemblyGridIndices(nW, f.kB12, idx).length, 0);
const hook = Math.round(hookLength('main90', d('S16'))); // 246 mm, rounded as in the document (§8.3)
const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, f);
const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, f);
+2 -3
View File
@@ -1,6 +1,6 @@
import type { PierModel } from '../core/geometry';
import { NOMINAL_DIAMETER } from '../core/rebar/common';
import { assemblyJs, b10Levels, b9Bars, grid, gridPositions, layers, sideDepth } from '../core/rebar/footing';
import { assemblyGridIndices, assemblyJs, b10Levels, b9Bars, grid, gridPositions, layers, sideDepth } from '../core/rebar/footing';
import { DEFAULT_FOOTING_REBAR } from '../core/rebar/footing-settings';
/**
@@ -83,9 +83,8 @@ export function footingSectionSvg(model: PierModel): string {
js.forEach((j, idx) => {
const zA = fs.cBottom + j * fs.sV;
parts.push(line('b11', 'B11', xa, xb, y(zA), COLOR.b11, 1.2));
const i0 = idx % 2 === 0 ? 0 : 2;
const xsB12 = gridPositions(W, fs, 'sAxis');
for (let i = i0; i < xsB12.length; i += fs.kB12) parts.push(dot('b12', 'B12', x(xsB12[i]), y(zA + dB10), COLOR.b12, 2.2));
for (const i of assemblyGridIndices(xsB12.length - 1, fs.kB12, idx)) parts.push(dot('b12', 'B12', x(xsB12[i]), y(zA + dB10), COLOR.b12, 2.2));
});
// Dimensions: width, grid, covers, and the B10 levels.
+3 -4
View File
@@ -1,6 +1,6 @@
import * as THREE from 'three';
import type { PierModel } from '../core/geometry';
import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, gridPositions, grid, layers, legLength, legOffsets, sideDepth } from '../core/rebar/footing';
import { assemblyEndOffset, assemblyGridIndices, assemblyJs, b10Levels, b9Bars, gridPositions, grid, layers, legLength, legOffsets, sideDepth } from '../core/rebar/footing';
import { bottomPiece, MAIN, topSplicePieces, widthLayout } from '../core/rebar/coping-main';
import { C14_INSET, c14Bars, CAP, COPING_REBAR, SHEAR_LOOP_STEP, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping';
import { sideRowLevels } from '../core/coping-geom';
@@ -478,12 +478,11 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
assemblyJs(Hf, fs).forEach((jA, k) => {
const zA = bottom + mm(fs.cBottom + jA * fs.sV);
const zB = zA + dB10;
const i0 = k % 2 === 0 ? 0 : 2;
for (let iL = i0; iL < xsL.length; iL += fs.kB11) {
for (const iL of assemblyGridIndices(xsL.length - 1, fs.kB11, k)) {
const x = f.s + mm(xsL[iL]);
polyline('B11', [[x, zA - hookAsm, -nAsm], [x, zA, -nAsm], [x, zA, nAsm], [x, zA - hookAsm, nAsm]], NOMINAL_DIAMETER.S16);
}
for (let iW = i0; iW < ysW.length; iW += fs.kB12) {
for (const iW of assemblyGridIndices(ysW.length - 1, fs.kB12, k)) {
const n = mm(ysW[iW]);
polyline('B12', [[f.s - sAsm, zB - hookAsm, n], [f.s - sAsm, zB, n], [f.s + sAsm, zB, n], [f.s + sAsm, zB - hookAsm, n]], NOMINAL_DIAMETER.S16);
}
+12 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { assemblyBars, assemblyJs, b9Bars, bottomBars, crossBars, sideBars } from '../src/core/rebar/footing';
import { assemblyBars, assemblyGridIndices, assemblyJs, b9Bars, bottomBars, crossBars, sideBars } from '../src/core/rebar/footing';
import { DEFAULT_FOOTING_REBAR } from '../src/core/rebar/footing-settings';
import { footingSectionSvg } from '../src/view/footing-section-svg';
import { loadP1 } from './helpers';
@@ -38,4 +38,15 @@ describe('확대기초 교각축 직각 단면 (폭 W) (승인 2026-10-26, 추
it('has one bubble per highlighted bar family', () => {
for (const name of ['B1', 'B2', 'B3', 'B4', 'B5', 'B11', 'B10-1']) expect(svg).toContain(`data-bubble="${name}"`);
});
it('조립 철근은 격자 가운데에 놓임: 양끝 여유 차이 ≤ 1칸 (B11 n 79, B12 n 61, k 8, 엇갈림 2칸 유지)', () => {
const levels = assemblyJs(H, fs).length;
for (const [n, k] of [[79, 8], [61, 8]] as const) {
const all = Array.from({ length: levels }, (_, idx) => assemblyGridIndices(n, k, idx)).flat();
const left = Math.min(...all);
const right = n - Math.max(...all);
expect(Math.abs(left - right)).toBeLessThanOrEqual(1);
}
expect(assemblyGridIndices(61, 8, 1)[0] - assemblyGridIndices(61, 8, 0)[0]).toBe(2);
});
});