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

+33 -1
View File
@@ -6,6 +6,7 @@
* 흉벽·벽체 단면은 모식도다. 흉벽은 저판 상면에서 상단까지 그린다(교좌면 아래 세부는 배근 단계에서).
*/
import type { BearingSole } from '../core/sole-plate';
import { jointPositions } from '../core/abut/joints';
import * as THREE from 'three';
import type { AbutModel, AbutPoint } from '../core/abut/geometry';
import { buildMembers, type AbutMembers } from '../core/abut/members';
@@ -14,7 +15,7 @@ import { resolveSlab, slabGradePct, slabLevelAt, slabSection, type AbutSlab } fr
import type { TerrainGrid } from '../core/ground-model';
import { terrainGeometry } from './terrain';
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'bearings', 'solplates', 'slab'] as const;
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'bearings', 'solplates', 'slab', 'joints'] as const;
/** 말뚝 두부 매입 (m): the drawn pile head goes this far into the 저판 (drawing only). */
const PILE_HEAD_EMBED_M = 0.1;
@@ -245,6 +246,8 @@ export function buildAbutGroup(
slabInput?: Partial<AbutSlab>,
/** Solplate of each bearing (abutSoles, 2026-11-06): the bearing body and the solplate wedge, and the block-top drop. */
soles?: BearingSole[],
/** 수축줄눈 위치 s (m, core/abut/joints.ts). Absent = the default count between the bearings. */
jointS?: number[],
): THREE.Group {
const root = new THREE.Group();
root.name = model.name;
@@ -316,6 +319,35 @@ export function buildAbutGroup(
add('parapet', skewPrism(parapet, -members.shape.parapetBackM, members.shape.parapetBackM), 0x8898aa);
}
// 수축줄눈 (spec_abut §4.9, 2026-11-06): lines only, on the 벽체 · 흉벽 faces (the 저판 is continuous). At each joint s:
// 벽체 전면 (t = 교좌면 끝) 저판 상면 → 교좌면, 흉벽 전면 (t = 0) 교좌면 → 흉벽 상단, 배면 (t = 벽체 배면) 저판 상면 → 브라켓 상면,
// 흉벽 배면 브라켓 상면 → 흉벽 상단. A joint between two steps takes the lower 교좌면. Lifted 3 mm off the faces.
{
const joints = jointS ?? jointPositions(L, model.bearings.map((b) => b.s));
const seatAt = (s: number) => Math.min(...members.wallSegments.filter((g) => s >= g.sFrom - 1e-6 && s <= g.sTo + 1e-6).map((g) => g.seatEl));
const pts: number[] = [];
const line = (s: number, t: number, y0: number, y1: number) => { if (y1 > y0) pts.push(s + skewK * t, y0, t, s + skewK * t, y1, t); };
const lift = 0.003, sh = members.shape;
for (const s of joints) {
const seat = seatAt(s);
if (!Number.isFinite(seat)) continue;
line(s, sh.wallFrontM + lift, footingTop, seat);
line(s, 0 + lift, seat, parapetTopAt(s));
line(s, sh.wallBackM - lift, footingTop, bracketAt(s));
line(s, sh.parapetBackM - lift, bracketAt(s), parapetTopAt(s));
}
if (pts.length) {
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3));
const lines = new THREE.LineSegments(g, new THREE.LineBasicMaterial({ color: 0x1b2733 }));
lines.name = 'joints';
const group = new THREE.Group();
group.name = 'joints';
group.add(lines);
root.add(group);
}
}
if (footingBottom !== undefined) {
// 저판: s 0 … L, t −1.8 … +2.8, from the 기초 하단 to the 저판 상면.
// Skew (2026-11-03): the 저판 ends follow the wing outer lines (s = sEnd + (ws/wt)·t), so the plan is a parallelogram.