feat(shape): 다주형 코핑 하면을 오목형으로 (Level D+200 · 오목 500 · 헌치 1500)

spec §3·§4.4(2026-11-06 개정)와 3주형형상배근.md §2.2(2주형 DWG 실측)를 따른다.
- 기둥 사이 하면: Level 끝에서 헌치 수평 1500 동안 500 올라가 평탄, 다음 기둥 앞에서 내려옴
  (이전 Level + 300, 헌치 300 × 300 대체)
- 다주형 Level 폭 기본값 = D + 200 (입력값이 있으면 입력 우선). T형은 2100 유지
- 오목 평탄부가 없으면 RB-M01 오류
- 3주형 테스트는 오목형이 성립하는 20 m 코핑으로 바꿈 (P1 길이 12 m는 RB-M01)
- 2주형 DWG 치수(L 24200, D 3000) 하면 꺾임점 테스트 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 14:11:15 +09:00
1 parent 42316104df
commit 370bd3d7cd
8 files changed
+79 -34

No files matched your search

+22 -7
View File
@@ -10,14 +10,25 @@ import { SOLPLATE_RULE_DEFAULT, solplatePlan } from './solplate-rule';
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
import { castShape } from './cast-in-place-shape';
/** Default Level width (mm) when no input: T형 2100 (P1), 2·3주형 D + 200 (spec §3, 2주형 DWG 2026-11-06). */
export function defaultLevelWidthMm(type: PierInput['type'], columnDiameterMm: number): number {
return type === 'T형' ? P1_COPING_GEOM.levelWidth : columnDiameterMm + DIM.multiLevelExtra * 1000;
}
/** Dimensions in m. Sources: intent.PR sheet (P1 rules) and spec.md §3 defaults. */
export const DIM = {
columnDiameter: 2.0,
copingH1: 1.0,
copingH2: 1.5,
levelWidth: 2.1,
/** Multi-column coping: underside between columns sits this far above Level, with 45° haunches of the same size (spec §4.4, Q3). */
haunch: 0.3,
/**
* Multi-column coping, 오목형 underside (spec §4.4, 3주형형상배근.md §2.2, 2주형 DWG 2026-11-06):
* from each Level end it rises concaveRise over the horizontal haunch length concaveHaunch, stays flat, and comes back down.
*/
concaveRise: 0.5,
concaveHaunch: 1.5,
/** Multi-column Level width = D + this (spec §3, 2주형 DWG). T형 keeps levelWidth (= D + 100 for P1). */
multiLevelExtra: 0.2,
padThickness: 0.05,
padSize: 1.0,
blockSize: 1.2,
@@ -251,9 +262,9 @@ export function buildPier(input: PierInput): PierModel {
// Coping heights and Level width (m): inputs, with the P1 values when absent (1단계).
const copingH1 = (input.copingH1Mm ?? P1_COPING_GEOM.h1) / 1000;
const copingH2 = (input.copingH2Mm ?? P1_COPING_GEOM.h2) / 1000;
const levelW = (input.levelWidthMm ?? P1_COPING_GEOM.levelWidth) / 1000;
// Column diameter (m): the input, with 2.0 m when absent (승인 2026-10-23).
const colD = (input.columnDiameterMm ?? DIM.columnDiameter * 1000) / 1000;
const levelW = (input.levelWidthMm ?? defaultLevelWidthMm(input.type, colD * 1000)) / 1000;
const planHalfB = B / 2;
const ip1v: Vec2 = { e: ip1.x, n: ip1.y };
const ip2v: Vec2 = { e: ip2.x, n: ip2.y };
@@ -346,7 +357,8 @@ export function buildPier(input: PierInput): PierModel {
const levelEnd = Math.max(...colG) + levelW / 2;
if (levelStart < 0 || levelEnd > L) throw new Error('코핑 길이가 Level 구간보다 짧습니다.');
// Underside from the right end to the left end. Between columns (2·3주형) it rises 300 above Level with 300 × 300 haunches (spec §4.4, Q3).
// Underside from the right end to the left end. Between columns (2·3주형) it is 오목형: from each Level end it rises
// h_r (500) over the horizontal haunch l_h (1500), stays flat, and comes back down (spec §4.4, 3주형형상배근.md §2.2).
const underside: [number, number][] = [[L, endBottom]];
const levelZones = [...colG].sort((a, b) => b - a).map((s) => [s - levelW / 2, s + levelW / 2] as const);
levelZones.forEach(([zoneStart, zoneEnd], k) => {
@@ -354,9 +366,12 @@ export function buildPier(input: PierInput): PierModel {
const next = levelZones[k + 1];
if (!next) return;
const gap = zoneStart - next[1];
if (gap <= 2 * DIM.haunch) throw new Error('기둥 사이 간격이 헌치(300 × 2)보다 좁아 코핑 하면을 만들 수 없습니다.');
const raisedEl = levelBottom + DIM.haunch;
underside.push([zoneStart - DIM.haunch, raisedEl], [next[1] + DIM.haunch, raisedEl], [next[1], levelBottom]);
// RB-M01: no flat part left between the two haunches.
if (gap <= 2 * DIM.concaveHaunch + 1e-9) {
throw new Error(`기둥 사이 간격 ${Math.round(gap * 1000)} mm가 오목형 헌치(${DIM.concaveHaunch * 1000} × 2)보다 짧아 코핑 하면을 만들 수 없습니다 (RB-M01).`);
}
const raisedEl = levelBottom + DIM.concaveRise;
underside.push([zoneStart - DIM.concaveHaunch, raisedEl], [next[1] + DIM.concaveHaunch, raisedEl], [next[1], levelBottom]);
});
// Elevation outline in coping coordinates for one row of tops, closed by the shared underside.
+2 -2
View File
@@ -82,7 +82,7 @@ import { applyCopy, COPY_ITEMS, planCopy, type CopyItem } from './core/copy';
import { copyCandidates, copyItemsFor, copyItemsWithOverrides, hasCopyItems, selectedTargets, type CopyCandidate } from './core/copy-targets';
import { quantityTargetNames } from './core/quantity-scope';
import { aggregateQuantities, type QuantityTotal } from './core/quantity-total';
import { pileGeometry } from './core/geometry';
import { defaultLevelWidthMm, pileGeometry } from './core/geometry';
import { rebarSummary, type RebarRow } from './core/rebar/summary';
import { shearSegments } from './core/rebar/coping';
import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar3d';
@@ -388,7 +388,7 @@ function withCopingDefaults(input: PierInput): PierInput {
pileThicknessMm: input.pileThicknessMm ?? 12,
copingH1Mm: input.copingH1Mm ?? P1_COPING_GEOM.h1,
copingH2Mm: input.copingH2Mm ?? P1_COPING_GEOM.h2,
levelWidthMm: input.levelWidthMm ?? P1_COPING_GEOM.levelWidth,
levelWidthMm: input.levelWidthMm ?? defaultLevelWidthMm(input.type, input.columnDiameterMm ?? 2000),
};
}