fix(abut): 수축줄눈을 받침 사이 중간에 · 기본 개수 ceil(L/10) − 1 · 3D 선 표시

spec_abut §3 · §4.9 (QA-7 가정)와 맞지 않던 세 가지를 고친다.
- 위치: 등분점(1개면 L/2)이 아니라 가장 가까운 '받침 사이 중간'(= 교좌면 계단 경계). 이전에는 운곡교 교대
  5기 모두 줄눈이 가운데 받침 ③ 블럭을 지났다 (A1: 줄눈 6.099 · 받침 6.037). 이제 A1은 IP3에서 4.905 m
- 개수: 입력이 없으면 ceil(L/10) − 1 (길이에 따라 자동, 이전에는 1 고정). 입력 칸은 비우면 자동
- 3D: 벽체 전면 · 흉벽 전면 · 벽체 배면 · 흉벽 배면에 선으로 표시(저판은 연속이라 없음), '벽체·흉벽' 토글을 따름
- 공통 계산 core/abut/joints.ts를 ② 정면·배면 삽도, 교대 상세 요약, 입력 칸 안내, 3D가 함께 씀
- 테스트: 개수 규칙, A1 위치 = ②·③ 중간 = 계단 경계, 여러 개 · 받침 부족 시 등분, 3D 선 4면

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 16:23:04 +09:00
1 parent 1fda705b75
commit bff1502ecf
8 files changed
+158 -13

No files matched your search

+32 -3
View File
@@ -36,6 +36,7 @@ import {
} from './core/abut/edit';
import { ABUT_FOOTING_REBAR_DEFAULT, abutFootingGeom, abutFootingRebar, ABUT_FOOTING_SIZES, type AbutFootingGeom, type AbutFootingRebar, type AbutFootingRebarSettings } from './core/abut/rebar/footing';
import { lenText, skewAngle } from './core/abut/rebar/skew-layout';
import { defaultJointCount, jointPositions } from './core/abut/joints';
import { ABUT_BLOCK_PAD_MM, ABUT_BLOCK_REBAR_DEFAULT, abutBlockRebar, type AbutBlockRebar, type AbutBlockSettings } from './core/abut/rebar/block';
import { ABUT_BLOCK_COLORS, abutBlockPlanSvg as abutBlockRebarPlanSvg, abutBlockSectionSvg as abutBlockRebarSectionSvg } from './view/abut-block-rebar-svg';
import { ABUT_SLAB_REBAR_DEFAULT, ABUT_SLAB_SIZES, abutSlabGeom, abutSlabRebar, type AbutSlabGeom, type AbutSlabRebar, type AbutSlabRebarSettings } from './core/abut/rebar/slab';
@@ -156,7 +157,7 @@ const PART_GROUPS: { label: string; pier: PartName[]; abut: string[] }[] = [
{ label: '말뚝', pier: ['piles'], abut: ['piles'] },
{ label: '버림', pier: ['blinding'], abut: ['blinding'] },
{ label: '지반면', pier: ['ground'], abut: ['ground'] },
{ label: '벽체·흉벽', pier: [], abut: ['wall', 'parapet'] },
{ label: '벽체·흉벽', pier: [], abut: ['wall', 'parapet', 'joints'] },
// 헌치 sits at the wing ends (wings.ts builds it per wing), so it follows the 날개벽 toggle (2026-11-03).
{ label: '날개벽', pier: [], abut: ['wings', 'haunches'] },
{ label: '접속슬래브', pier: [], abut: ['slab'] },
@@ -665,7 +666,8 @@ function setupScene(): void {
pairs: state.bridgePairs, slopeI: rec.slopeI,
inputAzimuthDeg: Object.fromEntries(rec.bearings.filter((x) => x.azimuthDeg !== undefined).map((x) => [String(x.no), x.azimuthDeg!])),
});
const group = buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, groundModel?.terrainGrid, rec.slab, soles);
const group = buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, groundModel?.terrainGrid, rec.slab, soles,
jointPositions(model.length, model.bearings.map((b) => b.s), rec.shape?.shrinkJointCount));
group.visible = abutShown(state.stage, state.abutActive, rec.name);
// The 부재 표시 toggles hold for a new abutment group too.
for (const [name, on] of Object.entries(state.abutVisible)) {
@@ -1831,7 +1833,7 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
footingFrontM: s.footingFrontM,
footingBackM: s.footingBackM,
bracketHeightM: s.bracketHeightM,
shrinkJointCount: v.shrinkJointCount,
shrinkJointsS: jointPositions(built.model.length, built.model.bearings.map((b) => b.s), rec.shape?.shrinkJointCount),
bracketWidthMm: v.bracketWidthMm,
parapetThicknessMm: v.parapetThicknessMm,
seatWidthMm: v.wallFrontMm,
@@ -1891,6 +1893,33 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
}
}
for (const f of SHAPE_EDIT_FIELDS) {
// 수축줄눈 개수 (spec_abut §3): 입력이 없으면 ceil(L/10) − 1 (교대 길이에 따름), 위치는 받침 사이 중간.
if (f.key === 'shrinkJointCount') {
// ① has no built model (② builds it above): build it here for the length and the bearings.
let jm: AbutModel | null = built?.model ?? null;
if (!jm) { try { jm = buildAbut(toAbutInput(rec)); } catch { jm = null; } }
const auto = jm ? defaultJointCount(jm.length) : ABUT_SHAPE_DEFAULT.shrinkJointCount;
const typed = rec.shape?.shrinkJointCount;
const id = idOf(`shape-${f.key}`);
const lab = el('label', f.label);
lab.htmlFor = id;
const input = numberInput(id, typed === undefined ? '' : String(typed));
input.placeholder = `자동 ${auto}`;
if (typed !== undefined) input.classList.add('changed-input');
input.addEventListener('change', () => {
try {
if (input.value.trim() === '') {
const { shrinkJointCount: _drop, ...rest } = rec.shape ?? {};
apply({ ...rec, shape: Object.keys(rest).length ? rest : undefined });
} else apply(applyShapeEdit(rec, f.key, Number(input.value)));
} catch (err) {
fail(err, input, typed === undefined ? '' : String(typed));
}
});
const where = jm ? jointPositions(jm.length, jm.bearings.map((b) => b.s), typed).map((s) => (jm!.length - s).toFixed(3)).reverse().join(' / ') : '—';
shape.append(lab, input, el('span', `기본 자동 ceil(L/10) − 1 = ${auto} · 위치 받침 사이 중간 (IP3에서 ${where} m) · QA-7`, 'muted small'));
continue;
}
const current = normalizeShape(rec.shape)[f.key] ?? ABUT_SHAPE_DEFAULT[f.key];
const id = idOf(`shape-${f.key}`);
const lab = el('label', f.label);