feat(bearing): 일체 block as one stepped block per pair, with its rebar (승인 2026-10-26)
- integrated-block.ts: per pair (전열 k · 후열 k) one block centred on the pair midpoint, 2500 across the 교축 and 1200 along the 교각축, with a stepped top (each half to its own bearing row top). Only when 일체 is chosen and every pair is within 100 mm; otherwise 분리 is kept (B02). - Rebar: S2 19 bars at 125 across the 2500 (10 front half, 9 back half), each leg from its half's top; S1 8 bars across the 1200, 3800 long, depth from the lower top. Totals per pier: S1 40, S2 95 (분리: 80 / 80). - Scene and 3D rebar draw the stepped blocks instead of the per-bearing blocks. - The per-bearing blocks stay for the solplate, the pads, and the body. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2af5cfb144
commit
43a18182f3
7 files changed
+216
-6
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis';
|
||||
import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block';
|
||||
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
|
||||
import type { PierInput, Row } from './types';
|
||||
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
|
||||
@@ -170,6 +171,8 @@ export interface PierModel {
|
||||
columnDiameterMm: number;
|
||||
bearings: BearingLocal[];
|
||||
blocks: BlockModel[];
|
||||
/** 일체 블록 (승인 2026-10-26): empty unless 일체 is chosen and allowed. The per-bearing blocks stay for the solplate and pads. */
|
||||
integratedBlocks: IntegratedBlock[];
|
||||
columns: ColumnModel[];
|
||||
footings: FootingModel[];
|
||||
blindings: BlindingModel[];
|
||||
@@ -456,6 +459,7 @@ export function buildPier(input: PierInput): PierModel {
|
||||
columnDiameterMm: colD * 1000,
|
||||
bearings: [...frontPlaced, ...backPlaced].map((b) => ({ no: b.no, row: b.row, k: b.k, s: b.s, g: b.g, n: b.n, z: b.z })),
|
||||
blocks,
|
||||
integratedBlocks: buildIntegratedBlocks(blocks, input.bearingLayout === '일체'),
|
||||
columns,
|
||||
footings,
|
||||
blindings,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { BlockModel } from './geometry';
|
||||
import { integratedCheck, pairEccentricities } from './bearing-layout';
|
||||
|
||||
/**
|
||||
* 일체 블록 (승인 2026-10-26): one block per front–back pair, centred on the pair's midpoint.
|
||||
* Length 2500 across the 교축 (n) covers the pair (C1 600 + C2 1298 + C1 600), width 1200 along the 교각축 (s).
|
||||
* The top is stepped: the front half follows the front block top, the back half the back block top (단이 있는 블록).
|
||||
* Only built when every pair is within the eccentricity limit; otherwise the model keeps 분리 (see integratedCheck).
|
||||
*/
|
||||
export const INTEGRATED_BLOCK = { length: 2500, depth: 1200 } as const;
|
||||
|
||||
export interface IntegratedBlock {
|
||||
frontNo: number;
|
||||
backNo: number;
|
||||
/** Centre along the coping length (s, m) — the mean of the pair. */
|
||||
s: number;
|
||||
/** Centre across the 교축 (n, m) — the midpoint of the pair; the step line is here. */
|
||||
n: number;
|
||||
/** Bottom EL (m): the lower of the two bottoms. */
|
||||
bottom: number;
|
||||
/** Top EL of the front half (전열 side, m). */
|
||||
topFront: number;
|
||||
/** Top EL of the back half (후열 side, m). */
|
||||
topBack: number;
|
||||
/** Mean row skew (deg) of the pair. */
|
||||
rowSkewDeg: number;
|
||||
}
|
||||
|
||||
/** Integrated blocks of a pier: empty unless 일체 is chosen and every pair is within the limit. */
|
||||
export function buildIntegratedBlocks(blocks: BlockModel[], chosen: boolean): IntegratedBlock[] {
|
||||
if (!chosen || !integratedCheck(blocks).allowed) return [];
|
||||
const pick = (no: number, row: BlockModel['row']) => blocks.find((b) => b.no === no && b.row === row)!;
|
||||
return pairEccentricities(blocks).map((p) => {
|
||||
const f = pick(p.frontNo, '전열');
|
||||
const b = pick(p.backNo, '후열');
|
||||
return {
|
||||
frontNo: p.frontNo,
|
||||
backNo: p.backNo,
|
||||
s: (f.s + b.s) / 2,
|
||||
n: (f.n + b.n) / 2,
|
||||
bottom: Math.min(f.bottom, b.bottom),
|
||||
topFront: f.top,
|
||||
topBack: b.top,
|
||||
rowSkewDeg: (f.rowSkewDeg + b.rowSkewDeg) / 2,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PierModel } from '../geometry';
|
||||
import { INTEGRATED_BLOCK, type IntegratedBlock } from '../integrated-block';
|
||||
|
||||
/**
|
||||
* 받침블럭 철근 (spec §6.3 · plan C2), 배치 분리: one block per bearing, 1200 × 1200.
|
||||
@@ -64,6 +65,10 @@ export interface BlockRebar {
|
||||
* since the bar depths are measured from the block top. h1 / h2 include the 50 pad and would count it twice.
|
||||
*/
|
||||
export function bearingBlockRebar(model: PierModel): BlockRebar[] {
|
||||
// 일체 (승인 2026-10-26): one block per pair replaces the two per-bearing blocks.
|
||||
if (model.integratedBlocks.length > 0) {
|
||||
return model.integratedBlocks.map((ib) => ({ no: ib.frontNo, row: '전열', bars: integratedBlockBars(ib) }));
|
||||
}
|
||||
return model.blocks.map((b) => ({
|
||||
no: b.no,
|
||||
row: b.row,
|
||||
@@ -71,6 +76,30 @@ export function bearingBlockRebar(model: PierModel): BlockRebar[] {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebar of one 일체 block (승인 2026-10-26, 받침블럭철근.md "일체 a 2500").
|
||||
* S2 runs along the 교각축 (1200), so its bars are spaced across the 2500 length: 19 bars at 125. Its legs are measured
|
||||
* from the top of the half it sits in; the bar at the step line (centre) counts with the front half.
|
||||
* S1 runs along the 2500 length, 8 bars across the 1200 depth, its depth taken from the lower top (cover on the higher half is larger).
|
||||
*/
|
||||
export function integratedBlockBars(ib: IntegratedBlock): BarRow[] {
|
||||
const half = INTEGRATED_BLOCK.length / 2;
|
||||
const s2 = barsAcross(INTEGRATED_BLOCK.length);
|
||||
const frontCount = Array.from({ length: s2.count }, (_, k) => s2.firstEnd + k * s2.spacing).filter((x) => x <= half + 1e-9).length;
|
||||
const backCount = s2.count - frontCount;
|
||||
const hFront = (ib.topFront - ib.bottom) * 1000;
|
||||
const hBack = (ib.topBack - ib.bottom) * 1000;
|
||||
const legS2 = (h: number) => BLOCK_REBAR.legLength - (h - BLOCK_REBAR.depthS2);
|
||||
const s1 = barsAcross(INTEGRATED_BLOCK.depth);
|
||||
const hLow = Math.min(hFront, hBack);
|
||||
const rows: BarRow[] = [
|
||||
{ name: 'S2', count: frontCount, spacing: s2.spacing, firstEnd: s2.firstEnd, length: barLength(INTEGRATED_BLOCK.depth), embedment: legS2(hFront) },
|
||||
];
|
||||
if (backCount > 0) rows.push({ name: 'S2', count: backCount, spacing: s2.spacing, firstEnd: s2.firstEnd, length: barLength(INTEGRATED_BLOCK.depth), embedment: legS2(hBack) });
|
||||
rows.push({ name: 'S1', count: s1.count, spacing: s1.spacing, firstEnd: s1.firstEnd, length: barLength(INTEGRATED_BLOCK.length), embedment: BLOCK_REBAR.legLength - (hLow - BLOCK_REBAR.depthS1) });
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Total bars of one pier: bars per block × blocks, for S1 and S2 separately. */
|
||||
export function totals(rebar: BlockRebar[]): { S1: number; S2: number } {
|
||||
return rebar.reduce(
|
||||
|
||||
@@ -94,14 +94,14 @@ export function validatePier(input: PierInput, model: PierModel, issues: Issue[]
|
||||
validateGround(input, issues);
|
||||
validatePileEmbedment(input, model, issues);
|
||||
validateStrataNValues(input, issues);
|
||||
// 일체 (승인 2026-10-26): the block shape of 일체 is not modelled yet, so the model keeps 분리 and says so.
|
||||
// 일체 (승인 2026-10-26): the model builds one stepped block per pair when allowed; otherwise it keeps 분리.
|
||||
if (input.bearingLayout === '일체') {
|
||||
const check = integratedCheck(model.blocks);
|
||||
if (!check.allowed) {
|
||||
const worst = check.overLimit.map((p) => `전열 ${p.frontNo}·후열 ${p.backNo} ${p.eccentricityMm.toFixed(0)} mm`).join(', ');
|
||||
issues.push({ code: 'B02', level: 'W', pier: input.name, message: `일체를 골랐지만 편심이 100 mm를 넘는 쌍이 있어 분리로 계산합니다 (${worst || '쌍 없음'}).` });
|
||||
} else {
|
||||
issues.push({ code: 'B01', level: 'I', pier: input.name, message: '일체 배치: 블록 형상은 아직 분리 기준으로 계산합니다 (일체 블록 모델은 다음 단계).' });
|
||||
issues.push({ code: 'B01', level: 'I', pier: input.name, message: '일체 배치: 전열·후열 쌍마다 단이 있는 블록 (2500 × 1200)으로 계산합니다.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { sideRowLevels } from '../core/coping-geom';
|
||||
import { columnRebar, columnInputFor, COLUMN_P1 } from '../core/rebar/column';
|
||||
import { barsAcross, BLOCK_REBAR } from '../core/rebar/bearing-block';
|
||||
import { blockFramePoint } from '../core/geometry';
|
||||
import { INTEGRATED_BLOCK } from '../core/integrated-block';
|
||||
import { bendRadius, DEFAULT_MATERIAL, FckByMember, hookLength, Material, materialFor, NOMINAL_DIAMETER } from '../core/rebar/common';
|
||||
import { diameterGroupIndex, DIAMETER_GROUPS, memberOfBar, type RebarMemberKey } from '../core/rebar/color';
|
||||
import { stretchMatrix } from './scene';
|
||||
@@ -474,6 +475,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
const dBar = NOMINAL_DIAMETER.H19;
|
||||
// The bars are drawn in the block's own frame (a along its s side, c along its n side) and turned with the block.
|
||||
for (const b of m.blocks) {
|
||||
if (m.integratedBlocks.length > 0) break; // 일체: drawn per pair below
|
||||
const z2 = b.top - BLOCK_REBAR.depthS2 / 1000;
|
||||
const z1 = b.top - BLOCK_REBAR.depthS1 / 1000;
|
||||
const P = (a: number, c: number, y: number): [number, number, number] => {
|
||||
@@ -490,6 +492,33 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
}
|
||||
}
|
||||
|
||||
// 일체 block (승인 2026-10-26): S2 runs along the 교각축 (1200) with 19 bars across the 2500 (교축), and S1 runs
|
||||
// along the 2500 with 8 bars across the 1200. Each S2 bar sits under its own half's top (front: offset ≤ 0).
|
||||
for (const ib of m.integratedBlocks) {
|
||||
const k = m.blocks.find((b) => b.no === ib.frontNo && b.row === '전열')!.k;
|
||||
const frame = { s: ib.s, n: ib.n, rowSkewDeg: ib.rowSkewDeg };
|
||||
const P = (a: number, c: number, y: number): [number, number, number] => {
|
||||
const [s, n] = blockFramePoint(frame, a, c);
|
||||
return [s, y, n];
|
||||
};
|
||||
const acrossN = barsAcross(INTEGRATED_BLOCK.length);
|
||||
const acrossS = barsAcross(INTEGRATED_BLOCK.depth);
|
||||
const halfS = (INTEGRATED_BLOCK.depth - 2 * BLOCK_REBAR.cover) / 2000;
|
||||
const halfN = (INTEGRATED_BLOCK.length - 2 * BLOCK_REBAR.cover) / 2000;
|
||||
const z1 = Math.min(ib.topFront, ib.topBack) - BLOCK_REBAR.depthS1 / 1000;
|
||||
for (const off of Array.from({ length: acrossN.count }, (_, i) => (acrossN.firstEnd + i * acrossN.spacing) / 1000 - INTEGRATED_BLOCK.length / 2000)) {
|
||||
const z2 = (off <= 1e-9 ? ib.topFront : ib.topBack) - BLOCK_REBAR.depthS2 / 1000;
|
||||
addBar('S2', k, P(-halfS, off, z2), P(halfS, off, z2), dBar, COLOR.block);
|
||||
addBar('S2', k, P(-halfS, off, z2 - legM), P(-halfS, off, z2), dBar, COLOR.block);
|
||||
addBar('S2', k, P(halfS, off, z2), P(halfS, off, z2 - legM), dBar, COLOR.block);
|
||||
}
|
||||
for (const off of Array.from({ length: acrossS.count }, (_, i) => (acrossS.firstEnd + i * acrossS.spacing) / 1000 - INTEGRATED_BLOCK.depth / 2000)) {
|
||||
addBar('S1', k, P(off, -halfN, z1), P(off, halfN, z1), dBar, COLOR.block);
|
||||
addBar('S1', k, P(off, -halfN, z1 - legM), P(off, -halfN, z1), dBar, COLOR.block);
|
||||
addBar('S1', k, P(off, halfN, z1), P(off, halfN, z1 - legM), dBar, COLOR.block);
|
||||
}
|
||||
}
|
||||
|
||||
const show = (names: ReadonlySet<string> | null, isolate: boolean) => {
|
||||
// 커플러 as a name selects every coupler bar.
|
||||
const isOn = (key: string) => names === null || names.has(key) || (names.has('커플러') && key.includes('커플러'));
|
||||
|
||||
+22
-4
@@ -3,6 +3,7 @@ import type { PierModel } from '../core/geometry';
|
||||
import { solplateGeometry } from '../core/solplate';
|
||||
import type { TerrainGrid } from '../core/ground-model';
|
||||
import { terrainGeometry } from './terrain';
|
||||
import { INTEGRATED_BLOCK } from '../core/integrated-block';
|
||||
|
||||
export type PartName = 'coping' | 'columns' | 'footings' | 'piles' | 'blinding' | 'blocks' | 'pads' | 'bearings' | 'solplates' | 'ground';
|
||||
|
||||
@@ -131,14 +132,18 @@ export function buildPierScene(m: PierModel, terrain?: TerrainGrid): PierScene {
|
||||
add('piles', g, 1, p.k, planesOf(m, p.k));
|
||||
}
|
||||
|
||||
// 일체 (승인 2026-10-26): the per-bearing concrete blocks give way to one stepped block per pair (below).
|
||||
const integrated = m.integratedBlocks.length > 0;
|
||||
for (const b of m.blocks) {
|
||||
// Turn the block so its sides follow the bearing row (rowSkewDeg); the 3D turn is rotateY(−skew).
|
||||
const turn = (-b.rowSkewDeg * Math.PI) / 180;
|
||||
const h = b.top - b.bottom;
|
||||
const g = new THREE.BoxGeometry(1.2, h, 1.2);
|
||||
g.rotateY(turn);
|
||||
g.translate(b.s, b.bottom + h / 2, b.n);
|
||||
add('blocks', g, 1, b.k);
|
||||
if (!integrated) {
|
||||
const g = new THREE.BoxGeometry(1.2, h, 1.2);
|
||||
g.rotateY(turn);
|
||||
g.translate(b.s, b.bottom + h / 2, b.n);
|
||||
add('blocks', g, 1, b.k);
|
||||
}
|
||||
const pad = new THREE.BoxGeometry(1.0, 0.05, 1.0);
|
||||
pad.rotateY(turn);
|
||||
pad.translate(b.s, b.padTop - 0.025, b.n);
|
||||
@@ -155,6 +160,19 @@ export function buildPierScene(m: PierModel, terrain?: TerrainGrid): PierScene {
|
||||
}
|
||||
}
|
||||
|
||||
for (const ib of m.integratedBlocks) {
|
||||
const k = m.blocks.find((b) => b.no === ib.frontNo && b.row === '전열')!.k;
|
||||
const turn = (-ib.rowSkewDeg * Math.PI) / 180;
|
||||
// Two halves of 1250 across the 교축, each to its own top: the front half (lower n) and the back half.
|
||||
for (const [top, zc] of [[ib.topFront, ib.n - INTEGRATED_BLOCK.length / 4000], [ib.topBack, ib.n + INTEGRATED_BLOCK.length / 4000]] as const) {
|
||||
const h = top - ib.bottom;
|
||||
const g = new THREE.BoxGeometry(INTEGRATED_BLOCK.depth / 1000, h, INTEGRATED_BLOCK.length / 2000);
|
||||
g.rotateY(turn);
|
||||
g.translate(ib.s, ib.bottom + h / 2, zc);
|
||||
add('blocks', g, 1, k);
|
||||
}
|
||||
}
|
||||
|
||||
// Ground: the terrain grid around the piers when the ground model has one (수 m 떨어진 지형까지 보임),
|
||||
// otherwise a translucent plane at the ground EL, used only to judge soil cover (not a computed input).
|
||||
if (terrain) {
|
||||
|
||||
Reference in new issue
Block a user