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:
ypshimandClaude Haiku 5.5 committed 2026-10-26 12:02:33 +09:00
1 parent 2af5cfb144
commit 43a18182f3
7 files changed
+216 -6

No files matched your search

+29
View File
@@ -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
View File
@@ -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) {