feat(abut): ② 벽체·흉벽 카드를 시안대로 작성 (삽도 2개, 브라켓 폭 입력, 확대기초·말뚝 분리)
- 삽도 2개 (src/view/abut-wall-svg.ts): 교대축 직각 단면(① 구간, "300 + 500 + 1600 = 2400", 흉벽 상단·브라켓 상면·교좌면·저판 EL)과 정면·배면도(교좌면 계단, 흉벽 상단 경사, 받침, 수축줄눈). 생성본 값, 가로·세로 같은 축척 - 벽체 두께는 자동 (= 브라켓 폭 + 흉벽 두께 + 교좌면 폭). 입력은 브라켓 폭(기본 300). 예전 저장 파일의 벽체 두께는 브라켓 폭으로 바꿔 읽음 (normalizeShape) - 인접 무시 기준(기본 5 mm) 입력: 이웃 받침의 교좌면 EL 차이가 이 값보다 작으면 한 구간으로 묶음 (A1 단차 71 mm, 변화 없음) - 카드 구성: 벽체 형식(기본형만, QA-8) → 삽도 → 입력 격자(시안 순서, QA-4 표시) → 자동 값(벽체 두께, 흉벽 상단 IP3/IP1, 교대축) → 구간 표 → 확인 필요 → 시안 메모 - 저판·말뚝 입력 5개를 새 "확대기초 · 말뚝" 카드로 분리. 부재 목록 6행(시안 순서), 변경 표시도 카드별로 나눔 - 테스트 갱신·추가. 전체 655건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
796a267205
commit
55b679932e
11 files changed
+401
-64
No files matched your search
+17
-14
@@ -71,20 +71,23 @@ export function applyShapeEdit(rec: AbutRecord, key: keyof AbutShape, value: num
|
||||
|
||||
|
||||
/** 형상 입력 fields on the 교대 카드 (시안 ② 벽체·흉벽, 확대기초·말뚝), in the order of the mockup. */
|
||||
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [
|
||||
{ key: 'wallFrontMm', label: '교좌면 폭 (흉벽 전면 기준, mm)' },
|
||||
{ key: 'wallThicknessMm', label: '벽체 두께 (mm)' },
|
||||
{ key: 'parapetHeightMm', label: '흉벽 높이 (mm)' },
|
||||
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)' },
|
||||
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)' },
|
||||
{ key: 'footingThicknessMm', label: '저판 두께 (mm)' },
|
||||
{ key: 'footingFrontMm', label: '저판 앞굽 끝 (mm)' },
|
||||
{ key: 'footingBackMm', label: '저판 뒷굽 끝 (mm)' },
|
||||
{ key: 'pileEdgeMm', label: '말뚝 연단 (mm)' },
|
||||
{ key: 'pileSpacingMm', label: '말뚝 간격 (mm)' },
|
||||
{ key: 'shrinkJointCount', label: '수축줄눈 개수 (개)' },
|
||||
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm, 계산 미사용)' },
|
||||
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm, 계산 미사용)' },
|
||||
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'wall' | 'footing'; note?: string }[] = [
|
||||
// 벽체 · 흉벽 (시안 입력 격자 순서). 벽체 두께는 자동 = 브라켓 폭 + 흉벽 두께 + 교좌면 폭.
|
||||
{ key: 'wallFrontMm', label: '교좌면 폭 (mm)', group: 'wall' },
|
||||
{ key: 'parapetHeightMm', label: '흉벽 높이 (mm)', group: 'wall' },
|
||||
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)', group: 'wall' },
|
||||
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)', group: 'wall' },
|
||||
{ key: 'bracketWidthMm', label: '브라켓 폭 (mm)', group: 'wall' },
|
||||
{ key: 'shrinkJointCount', label: '수축줄눈 개수', group: 'wall' },
|
||||
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||||
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||||
{ key: 'seatStepIgnoreMm', label: '인접 무시 기준 (mm)', group: 'wall' },
|
||||
// 확대기초 · 말뚝 (분리 카드).
|
||||
{ key: 'footingThicknessMm', label: '저판 두께 (mm)', group: 'footing' },
|
||||
{ key: 'footingFrontMm', label: '저판 앞굽 끝 (mm)', group: 'footing' },
|
||||
{ key: 'footingBackMm', label: '저판 뒷굽 끝 (mm)', group: 'footing' },
|
||||
{ key: 'pileEdgeMm', label: '말뚝 연단 (mm)', group: 'footing' },
|
||||
{ key: 'pileSpacingMm', label: '말뚝 간격 (mm)', group: 'footing' },
|
||||
];
|
||||
|
||||
/** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 H는 교대 받침 시트 값으로 고정 (2026-11-03). */
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { WingForm } from './geometry';
|
||||
import { ABUT_SHAPE_DEFAULT, type AbutShape } from './shape';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, wallThicknessMm, type AbutShape } from './shape';
|
||||
|
||||
/** 형상 입력 중 벽체·흉벽 카드와 확대기초·말뚝 카드의 항목 (변경 표시를 카드별로 나눈다). */
|
||||
const WALL_KEYS: (keyof AbutShape)[] = ['wallFrontMm', 'parapetHeightMm', 'parapetThicknessMm', 'bracketHeightMm', 'bracketWidthMm', 'shrinkJointCount', 'parapetLevelLeftMm', 'parapetLevelRightMm', 'seatStepIgnoreMm'];
|
||||
const FOOTING_KEYS: (keyof AbutShape)[] = ['footingThicknessMm', 'footingFrontMm', 'footingBackMm', 'pileEdgeMm', 'pileSpacingMm'];
|
||||
|
||||
/** 부재 · 교대 list in ② (시안): the card each row opens. */
|
||||
export type AbutMemberId = 'bearing' | 'shape' | 'wing' | 'slab' | 'ground';
|
||||
export type AbutMemberId = 'bearing' | 'shape' | 'footing' | 'wing' | 'slab' | 'ground';
|
||||
|
||||
/** Cards that take input in this step; slab and ground only show their values (phase 1, 2026-11-03). */
|
||||
export const ABUT_MEMBER_EDITABLE: Record<AbutMemberId, boolean> = {
|
||||
bearing: true,
|
||||
shape: true,
|
||||
footing: true,
|
||||
wing: true,
|
||||
slab: false,
|
||||
ground: false,
|
||||
@@ -32,14 +37,13 @@ export interface AbutMemberRow {
|
||||
}
|
||||
|
||||
/**
|
||||
* The five rows of the 부재 · 교대 list. `slabWidth` is the computed 접속슬래브 폭 text, passed in
|
||||
* The six rows of the 부재 · 교대 list (시안 순서). `slabWidth` is the computed 접속슬래브 폭 text, passed in
|
||||
* so this stays a pure function of the record.
|
||||
*/
|
||||
export function abutMemberRows(rec: AbutMemberInput, slabWidth: string): AbutMemberRow[] {
|
||||
const shape = { ...ABUT_SHAPE_DEFAULT, ...(rec.shape ?? {}) };
|
||||
const shapeChanged = (Object.keys(ABUT_SHAPE_DEFAULT) as (keyof AbutShape)[]).some(
|
||||
(key) => (rec.shape?.[key] ?? ABUT_SHAPE_DEFAULT[key]) !== ABUT_SHAPE_DEFAULT[key],
|
||||
);
|
||||
const changes = normalizeShape(rec.shape);
|
||||
const shape = { ...ABUT_SHAPE_DEFAULT, ...changes };
|
||||
const changed = (keys: (keyof AbutShape)[]) => keys.some((key) => (changes[key] ?? ABUT_SHAPE_DEFAULT[key]) !== ABUT_SHAPE_DEFAULT[key]);
|
||||
const wings = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
return [
|
||||
{
|
||||
@@ -51,8 +55,14 @@ export function abutMemberRows(rec: AbutMemberInput, slabWidth: string): AbutMem
|
||||
{
|
||||
id: 'shape',
|
||||
label: '벽체·흉벽',
|
||||
summary: `벽체 두께 ${shape.wallThicknessMm} · 흉벽 ${shape.parapetHeightMm}`,
|
||||
changed: shapeChanged,
|
||||
summary: `벽체 두께 ${wallThicknessMm(shape)} (자동) · 흉벽 ${shape.parapetHeightMm}`,
|
||||
changed: changed(WALL_KEYS),
|
||||
},
|
||||
{
|
||||
id: 'footing',
|
||||
label: '확대기초 · 말뚝',
|
||||
summary: `저판 ${shape.footingThicknessMm} · 말뚝 연단 ${shape.pileEdgeMm} · 간격 ${shape.pileSpacingMm}`,
|
||||
changed: changed(FOOTING_KEYS),
|
||||
},
|
||||
{
|
||||
id: 'wing',
|
||||
|
||||
@@ -55,12 +55,17 @@ export function buildMembers(model: AbutModel, foundation: { bottomEl?: number;
|
||||
const footingBottomEl = foundation.bottomEl;
|
||||
const footingTopEl = footingBottomEl === undefined ? NaN : footingBottomEl + shape.footingThicknessM;
|
||||
|
||||
const wallSegments: WallSegment[] = sorted.map((b, k) => ({
|
||||
sFrom: bounds[k],
|
||||
sTo: bounds[k + 1],
|
||||
seatEl: b.seatEl,
|
||||
heightM: b.seatEl - footingTopEl,
|
||||
}));
|
||||
// 인접 무시 기준 (2026-11-03): a step smaller than shape.seatStepIgnoreM joins the segment before it (one 교좌면 EL).
|
||||
const wallSegments: WallSegment[] = [];
|
||||
sorted.forEach((b, k) => {
|
||||
const prev = wallSegments[wallSegments.length - 1];
|
||||
// Compared with the neighbouring bearing (인접), not with the first step of the joined run.
|
||||
if (prev && k > 0 && Math.abs(b.seatEl - sorted[k - 1].seatEl) < shape.seatStepIgnoreM) {
|
||||
prev.sTo = bounds[k + 1];
|
||||
return;
|
||||
}
|
||||
wallSegments.push({ sFrom: bounds[k], sTo: bounds[k + 1], seatEl: b.seatEl, heightM: b.seatEl - footingTopEl });
|
||||
});
|
||||
|
||||
// 받침 ①(첫 입력)과 마지막 입력 받침이 흉벽 상단선을 정한다 (spec §4.4).
|
||||
const first = model.bearings[0];
|
||||
|
||||
+37
-9
@@ -2,15 +2,17 @@
|
||||
* 교대 형상 입력 (2026-11-03, 시안 ② 벽체·흉벽 · 확대기초·말뚝 카드): the values that used to be constants, in mm.
|
||||
* The defaults are the spec_abut values. A record keeps only the values the user changed; the rest come from here.
|
||||
*
|
||||
* Axis frame: t (교대축 직각) is + toward the front. 벽체 전면 = wallFrontMm; 배면 = wallFrontMm − wallThicknessMm.
|
||||
* 흉벽 sits behind the axis (t −parapetThicknessMm … 0). 저판: footingFrontMm (앞굽 끝) … footingBackMm (뒷굽 끝).
|
||||
* Axis frame: t (교대축 직각) is + toward the front. 벽체 전면 = wallFrontMm (교좌면 폭, 흉벽 전면 t 0에서).
|
||||
* 흉벽 sits behind the axis (t −parapetThicknessMm … 0); the 브라켓 (bracketWidthMm) behind it. 벽체 배면 = −(흉벽 두께 + 브라켓 폭),
|
||||
* so 벽체 두께 = 브라켓 폭 + 흉벽 두께 + 교좌면 폭 (시안: 300 + 500 + 1600 = 2400, 자동). 저판: footingFrontMm … footingBackMm.
|
||||
*/
|
||||
export interface AbutShape {
|
||||
wallFrontMm: number;
|
||||
wallThicknessMm: number;
|
||||
parapetHeightMm: number;
|
||||
parapetThicknessMm: number;
|
||||
bracketHeightMm: number;
|
||||
/** 브라켓 폭 (mm): 흉벽 배면에서 벽체 배면까지 (시안 300). */
|
||||
bracketWidthMm: number;
|
||||
footingThicknessMm: number;
|
||||
footingFrontMm: number;
|
||||
footingBackMm: number;
|
||||
@@ -21,14 +23,16 @@ export interface AbutShape {
|
||||
/** 흉벽 상단 Level 좌·우 (mm). Not used by the geometry (QA-4, 가정). */
|
||||
parapetLevelLeftMm: number;
|
||||
parapetLevelRightMm: number;
|
||||
/** 인접 무시 기준 (mm): 이웃 교좌면 EL 차이가 이 값보다 작으면 계단을 두지 않고 한 구간으로 묶는다 (교각 Q2와 같은 뜻). */
|
||||
seatStepIgnoreMm: number;
|
||||
}
|
||||
|
||||
export const ABUT_SHAPE_DEFAULT: AbutShape = {
|
||||
wallFrontMm: 1600,
|
||||
wallThicknessMm: 2400,
|
||||
parapetHeightMm: 2500,
|
||||
parapetThicknessMm: 500,
|
||||
bracketHeightMm: 850,
|
||||
bracketWidthMm: 300,
|
||||
footingThicknessMm: 1100,
|
||||
footingFrontMm: 2800,
|
||||
footingBackMm: -1800,
|
||||
@@ -37,8 +41,27 @@ export const ABUT_SHAPE_DEFAULT: AbutShape = {
|
||||
shrinkJointCount: 1,
|
||||
parapetLevelLeftMm: 450,
|
||||
parapetLevelRightMm: 995,
|
||||
seatStepIgnoreMm: 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* The user's changes with the old 벽체 두께 input (before 2026-11-03) turned into the 브라켓 폭:
|
||||
* 브라켓 폭 = 벽체 두께 − 교좌면 폭 − 흉벽 두께. Saved projects keep opening with the same wall.
|
||||
*/
|
||||
export function normalizeShape(changes?: Partial<AbutShape> & { wallThicknessMm?: number }): Partial<AbutShape> {
|
||||
if (!changes) return {};
|
||||
const { wallThicknessMm, ...rest } = changes;
|
||||
if (wallThicknessMm === undefined || rest.bracketWidthMm !== undefined) return rest;
|
||||
const front = rest.wallFrontMm ?? ABUT_SHAPE_DEFAULT.wallFrontMm;
|
||||
const parapet = rest.parapetThicknessMm ?? ABUT_SHAPE_DEFAULT.parapetThicknessMm;
|
||||
return { ...rest, bracketWidthMm: wallThicknessMm - front - parapet };
|
||||
}
|
||||
|
||||
/** 벽체 두께 (mm, 자동) = 브라켓 폭 + 흉벽 두께 + 교좌면 폭. */
|
||||
export function wallThicknessMm(v: AbutShape): number {
|
||||
return v.bracketWidthMm + v.parapetThicknessMm + v.wallFrontMm;
|
||||
}
|
||||
|
||||
/** The same values in m, for the geometry (the model works in m). */
|
||||
export interface ResolvedShape {
|
||||
wallFrontM: number;
|
||||
@@ -52,6 +75,8 @@ export interface ResolvedShape {
|
||||
pileEdgeM: number;
|
||||
pileSpacingM: number;
|
||||
pileRowsT: number[];
|
||||
/** 인접 무시 기준 (m). */
|
||||
seatStepIgnoreM: number;
|
||||
}
|
||||
|
||||
/** The pile rows (t, m) of the spec: 배면 열, 중간 열, 전면 열. Not an input on the card. */
|
||||
@@ -59,8 +84,8 @@ export const PILE_ROWS_T = [-1.15, 0.85, 2.15];
|
||||
|
||||
/** Merges the user's changes over the defaults and converts to m. */
|
||||
export function resolveShape(changes?: Partial<AbutShape>): ResolvedShape {
|
||||
const v: AbutShape = { ...ABUT_SHAPE_DEFAULT, ...(changes ?? {}) };
|
||||
const wallBackMm = v.wallFrontMm - v.wallThicknessMm;
|
||||
const v: AbutShape = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(changes) };
|
||||
const wallBackMm = -(v.parapetThicknessMm + v.bracketWidthMm);
|
||||
return {
|
||||
wallFrontM: v.wallFrontMm / 1000,
|
||||
wallBackM: wallBackMm / 1000,
|
||||
@@ -73,22 +98,25 @@ export function resolveShape(changes?: Partial<AbutShape>): ResolvedShape {
|
||||
pileEdgeM: v.pileEdgeMm / 1000,
|
||||
pileSpacingM: v.pileSpacingMm / 1000,
|
||||
pileRowsT: PILE_ROWS_T,
|
||||
seatStepIgnoreM: v.seatStepIgnoreMm / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
/** Input problems in Korean; empty when the shape can be built. */
|
||||
export function shapeIssues(changes: Partial<AbutShape>): string[] {
|
||||
const v: AbutShape = { ...ABUT_SHAPE_DEFAULT, ...changes };
|
||||
const v: AbutShape = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(changes) };
|
||||
const out: string[] = [];
|
||||
if (!(v.wallThicknessMm > 0)) out.push('벽체 두께는 0보다 커야 합니다.');
|
||||
if (!(v.wallFrontMm > 0)) out.push('교좌면 폭은 0보다 커야 합니다.');
|
||||
if (!(v.bracketWidthMm >= 0)) out.push('브라켓 폭은 0 이상이어야 합니다.');
|
||||
if (!(v.parapetHeightMm > 0)) out.push('흉벽 높이는 0보다 커야 합니다.');
|
||||
if (!(v.parapetThicknessMm > 0)) out.push('흉벽 두께는 0보다 커야 합니다.');
|
||||
if (!(v.bracketHeightMm >= 0)) out.push('브라켓 높이는 0 이상이어야 합니다.');
|
||||
if (!(v.footingThicknessMm > 0)) out.push('저판 두께는 0보다 커야 합니다.');
|
||||
if (!(v.pileEdgeMm > 0) || !(v.pileSpacingMm > 0)) out.push('말뚝 연단과 간격은 0보다 커야 합니다.');
|
||||
const wallBack = v.wallFrontMm - v.wallThicknessMm;
|
||||
const wallBack = -(v.parapetThicknessMm + v.bracketWidthMm);
|
||||
if (!(v.footingFrontMm > v.wallFrontMm)) out.push('저판 앞굽 끝은 벽체 전면보다 앞에 있어야 합니다.');
|
||||
if (!(v.footingBackMm < wallBack)) out.push('저판 뒷굽 끝은 벽체 배면보다 뒤에 있어야 합니다.');
|
||||
if (!(Number.isInteger(v.shrinkJointCount) && v.shrinkJointCount >= 0)) out.push('수축줄눈 개수는 0 이상의 정수여야 합니다.');
|
||||
if (!(v.seatStepIgnoreMm >= 0)) out.push('인접 무시 기준은 0 이상이어야 합니다.');
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user