feat(abut): ② 확대기초 · 말뚝 카드를 시안대로 작성 (평면 삽도, 버림, 열별 본수)
- 선택: 평면 skew 평행사변형(기본) / 직사각(비활성, 다음 단계), 말뚝 원기둥 D600 / 강관 · PHC(비활성, 규칙 미정) - 평면 삽도 (src/view/abut-footing-svg.ts): 저판, 벽체 윤곽, 교대축, 말뚝 3열(skew만큼 밀림), 열 위치 · 본수, 폭 치수 - 입력: 폭 4600, 두께 1100, 앞굽 1200 (뒷굽 자동 1000), 버림 여유 · 두께 100 / 100, 말뚝 지름 600 (QA-5), 연단 650, 본수 규칙 간격 2000 - 열 위치 "650 · 2000 · 1300 · 650" 입력 (전면 연단 자동, 합 = 폭 검사), 열별 말뚝 본수 표 − / + (0 = 본수 규칙) - 자동 값: 뒷굽, 길이(교좌면 길이), 말뚝 길이(①에서 수정), 선단 EL, 본수 규칙 문구. 시안 메모 - 3D: 교대 버림을 저판 아래에 그림 (부재 표시 "버림" 토글 적용), 말뚝 지름 입력 반영, 말뚝 열을 skew만큼 s 방향으로 밀어 배치 - 예전 저장 파일의 앞굽 끝 · 뒷굽 끝은 폭 · 앞굽으로 바꿔 읽음 - 테스트 추가 · 갱신. 전체 661건 통과, 타입 검사 · 빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
55b679932e
commit
372d759eb7
10 files changed
+425
-46
No files matched your search
+18
-6
@@ -61,6 +61,15 @@ export function restoreAbutDefault(defaults: AbutRecord): AbutRecord {
|
||||
}
|
||||
|
||||
/** 형상 입력 change (벽체·흉벽, 확대기초·말뚝, mm): refused with a Korean message when the shape would not build. */
|
||||
/** Several shape values at once (e.g. the 열 위치 "650 · 2000 · 1300 · 650"), checked together. */
|
||||
export function applyShapeEdits(rec: AbutRecord, values: Partial<AbutShape>): AbutRecord {
|
||||
for (const v of Object.values(values)) if (!Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const shape = { ...(rec.shape ?? {}), ...values };
|
||||
const issues = shapeIssues(shape);
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, shape };
|
||||
}
|
||||
|
||||
export function applyShapeEdit(rec: AbutRecord, key: keyof AbutShape, value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
const shape = { ...(rec.shape ?? {}), [key]: value };
|
||||
@@ -82,12 +91,15 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'w
|
||||
{ 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' },
|
||||
// 확대기초 · 말뚝 (분리 카드, 시안 순서). 뒷굽 · 길이 · 선단 EL은 자동, 열 위치와 열별 본수는 카드에서 따로 입력한다.
|
||||
{ key: 'footingWidthMm', label: '폭 (mm)', group: 'footing' },
|
||||
{ key: 'footingThicknessMm', label: '두께 (mm)', group: 'footing' },
|
||||
{ key: 'footingToeMm', label: '앞굽 (mm)', group: 'footing' },
|
||||
{ key: 'blindingMarginMm', label: '버림 여유 한쪽 (mm)', group: 'footing' },
|
||||
{ key: 'blindingThicknessMm', label: '버림 두께 (mm)', group: 'footing' },
|
||||
{ key: 'pileDiameterMm', label: '말뚝 지름 (mm)', group: 'footing', note: 'QA-5' },
|
||||
{ key: 'pileEdgeMm', label: '연단 · 길이 방향 (mm)', group: 'footing' },
|
||||
{ key: 'pileSpacingMm', label: '본수 규칙 간격 (mm)', group: 'footing' },
|
||||
];
|
||||
|
||||
/** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 H는 교대 받침 시트 값으로 고정 (2026-11-03). */
|
||||
|
||||
@@ -3,7 +3,10 @@ import { ABUT_SHAPE_DEFAULT, normalizeShape, wallThicknessMm, type AbutShape } f
|
||||
|
||||
/** 형상 입력 중 벽체·흉벽 카드와 확대기초·말뚝 카드의 항목 (변경 표시를 카드별로 나눈다). */
|
||||
const WALL_KEYS: (keyof AbutShape)[] = ['wallFrontMm', 'parapetHeightMm', 'parapetThicknessMm', 'bracketHeightMm', 'bracketWidthMm', 'shrinkJointCount', 'parapetLevelLeftMm', 'parapetLevelRightMm', 'seatStepIgnoreMm'];
|
||||
const FOOTING_KEYS: (keyof AbutShape)[] = ['footingThicknessMm', 'footingFrontMm', 'footingBackMm', 'pileEdgeMm', 'pileSpacingMm'];
|
||||
const FOOTING_KEYS: (keyof AbutShape)[] = [
|
||||
'footingWidthMm', 'footingThicknessMm', 'footingToeMm', 'blindingMarginMm', 'blindingThicknessMm', 'pileDiameterMm', 'pileEdgeMm', 'pileSpacingMm',
|
||||
'pileRowBackEdgeMm', 'pileRowGap1Mm', 'pileRowGap2Mm', 'pileCountRow1', 'pileCountRow2', 'pileCountRow3',
|
||||
];
|
||||
|
||||
/** 부재 · 교대 list in ② (시안): the card each row opens. */
|
||||
export type AbutMemberId = 'bearing' | 'shape' | 'footing' | 'wing' | 'slab' | 'ground';
|
||||
|
||||
@@ -74,12 +74,13 @@ export function buildMembers(model: AbutModel, foundation: { bottomEl?: number;
|
||||
const parapetTopStartEl = parapetTopEl(first.seatEl, first.s, slope, 0, shape.parapetHeightM);
|
||||
const parapetTopEndEl = parapetTopEl(first.seatEl, first.s, slope, L, shape.parapetHeightM);
|
||||
|
||||
const count = Math.floor((L - 2 * shape.pileEdgeM) / shape.pileSpacingM) + 1;
|
||||
const spacing = count > 1 ? (L - 2 * shape.pileEdgeM) / (count - 1) : 0;
|
||||
const piles: PileRow[] = shape.pileRowsT.map((t) => ({
|
||||
t,
|
||||
s: Array.from({ length: count }, (_, k) => shape.pileEdgeM + k * spacing),
|
||||
}));
|
||||
// 본수 규칙 int((L − 2·연단)/간격) + 1; 열별 본수가 0이 아니면 그 값 (2026-11-04, 시안 열별 말뚝 본수).
|
||||
const ruleCount = Math.floor((L - 2 * shape.pileEdgeM) / shape.pileSpacingM) + 1;
|
||||
const piles: PileRow[] = shape.pileRowsT.map((t, r) => {
|
||||
const count = shape.pileCounts[r] > 0 ? shape.pileCounts[r] : ruleCount;
|
||||
const spacing = count > 1 ? (L - 2 * shape.pileEdgeM) / (count - 1) : 0;
|
||||
return { t, s: Array.from({ length: count }, (_, k) => shape.pileEdgeM + k * spacing) };
|
||||
});
|
||||
const pileBottomEl = footingBottomEl === undefined || foundation.pileLengthMm === undefined ? undefined : footingBottomEl - foundation.pileLengthMm / 1000;
|
||||
|
||||
return {
|
||||
|
||||
+84
-19
@@ -4,7 +4,8 @@
|
||||
*
|
||||
* 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.
|
||||
* so 벽체 두께 = 브라켓 폭 + 흉벽 두께 + 교좌면 폭 (시안: 300 + 500 + 1600 = 2400, 자동).
|
||||
* 저판 (시안 확대기초·말뚝): 폭과 앞굽을 입력하고 뒷굽은 자동 (폭 − 앞굽 − 벽체 두께). 앞굽 끝 t = 교좌면 폭 + 앞굽, 뒷굽 끝 = 앞굽 끝 − 폭.
|
||||
*/
|
||||
export interface AbutShape {
|
||||
wallFrontMm: number;
|
||||
@@ -14,10 +15,25 @@ export interface AbutShape {
|
||||
/** 브라켓 폭 (mm): 흉벽 배면에서 벽체 배면까지 (시안 300). */
|
||||
bracketWidthMm: number;
|
||||
footingThicknessMm: number;
|
||||
footingFrontMm: number;
|
||||
footingBackMm: number;
|
||||
/** 저판 폭 (mm, 교대축 직각) and 앞굽 (mm, 벽체 전면에서). 뒷굽은 자동. */
|
||||
footingWidthMm: number;
|
||||
footingToeMm: number;
|
||||
/** 말뚝 연단 (길이 방향, mm)과 본수 규칙의 간격 (mm): 본수 = int((L − 2·연단)/간격) + 1. */
|
||||
pileEdgeMm: number;
|
||||
pileSpacingMm: number;
|
||||
/** 말뚝 지름 (mm, 원기둥, QA-5). */
|
||||
pileDiameterMm: number;
|
||||
/** 말뚝 열 위치 (mm, 저판 배면부터): 배면 연단, 3열→2열 간격, 2열→1열 간격. 전면 연단은 자동 (폭 − 합). */
|
||||
pileRowBackEdgeMm: number;
|
||||
pileRowGap1Mm: number;
|
||||
pileRowGap2Mm: number;
|
||||
/** 열별 본수 (1열 전면, 2열, 3열 배면). 0 = 본수 규칙대로. */
|
||||
pileCountRow1: number;
|
||||
pileCountRow2: number;
|
||||
pileCountRow3: number;
|
||||
/** 버림 여유 (한쪽, mm)와 두께 (mm). */
|
||||
blindingMarginMm: number;
|
||||
blindingThicknessMm: number;
|
||||
/** 수축줄눈 개수 (개). Shown in the 삽도; not used by the geometry yet. */
|
||||
shrinkJointCount: number;
|
||||
/** 흉벽 상단 Level 좌·우 (mm). Not used by the geometry (QA-4, 가정). */
|
||||
@@ -34,10 +50,19 @@ export const ABUT_SHAPE_DEFAULT: AbutShape = {
|
||||
bracketHeightMm: 850,
|
||||
bracketWidthMm: 300,
|
||||
footingThicknessMm: 1100,
|
||||
footingFrontMm: 2800,
|
||||
footingBackMm: -1800,
|
||||
footingWidthMm: 4600,
|
||||
footingToeMm: 1200,
|
||||
pileEdgeMm: 650,
|
||||
pileSpacingMm: 2000,
|
||||
pileDiameterMm: 600,
|
||||
pileRowBackEdgeMm: 650,
|
||||
pileRowGap1Mm: 2000,
|
||||
pileRowGap2Mm: 1300,
|
||||
pileCountRow1: 0,
|
||||
pileCountRow2: 0,
|
||||
pileCountRow3: 0,
|
||||
blindingMarginMm: 100,
|
||||
blindingThicknessMm: 100,
|
||||
shrinkJointCount: 1,
|
||||
parapetLevelLeftMm: 450,
|
||||
parapetLevelRightMm: 995,
|
||||
@@ -48,13 +73,39 @@ export const ABUT_SHAPE_DEFAULT: AbutShape = {
|
||||
* 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> {
|
||||
export function normalizeShape(
|
||||
changes?: Partial<AbutShape> & { wallThicknessMm?: number; footingFrontMm?: number; footingBackMm?: 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 { wallThicknessMm, footingFrontMm, footingBackMm, ...rest } = changes;
|
||||
const seat = rest.wallFrontMm ?? ABUT_SHAPE_DEFAULT.wallFrontMm;
|
||||
const parapet = rest.parapetThicknessMm ?? ABUT_SHAPE_DEFAULT.parapetThicknessMm;
|
||||
return { ...rest, bracketWidthMm: wallThicknessMm - front - parapet };
|
||||
const out: Partial<AbutShape> = { ...rest };
|
||||
if (wallThicknessMm !== undefined && rest.bracketWidthMm === undefined) out.bracketWidthMm = wallThicknessMm - seat - parapet;
|
||||
// 저판 끝 (before 2026-11-04) → 폭 · 앞굽: 앞굽 = 앞굽 끝 − 교좌면 폭, 폭 = 앞굽 끝 − 뒷굽 끝.
|
||||
if ((footingFrontMm !== undefined || footingBackMm !== undefined) && rest.footingWidthMm === undefined && rest.footingToeMm === undefined) {
|
||||
const front = footingFrontMm ?? seat + ABUT_SHAPE_DEFAULT.footingToeMm;
|
||||
const back = footingBackMm ?? front - ABUT_SHAPE_DEFAULT.footingWidthMm;
|
||||
out.footingToeMm = front - seat;
|
||||
out.footingWidthMm = front - back;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 저판 앞굽 끝·뒷굽 끝 (mm, t) and the 뒷굽 (mm, 자동). */
|
||||
export function footingEnds(v: AbutShape): { frontMm: number; backMm: number; heelMm: number } {
|
||||
const frontMm = v.wallFrontMm + v.footingToeMm;
|
||||
const backMm = frontMm - v.footingWidthMm;
|
||||
return { frontMm, backMm, heelMm: v.footingWidthMm - v.footingToeMm - wallThicknessMm(v) };
|
||||
}
|
||||
|
||||
/** 말뚝 열 위치 t (mm): 배면 3열, 2열, 전면 1열 (저판 배면부터 잰 값으로). 전면 연단은 자동. */
|
||||
export function pileRowsMm(v: AbutShape): { back: number; middle: number; front: number; frontEdgeMm: number } {
|
||||
const { backMm } = footingEnds(v);
|
||||
const back = backMm + v.pileRowBackEdgeMm;
|
||||
const middle = back + v.pileRowGap1Mm;
|
||||
const front = middle + v.pileRowGap2Mm;
|
||||
return { back, middle, front, frontEdgeMm: v.footingWidthMm - v.pileRowBackEdgeMm - v.pileRowGap1Mm - v.pileRowGap2Mm };
|
||||
}
|
||||
|
||||
/** 벽체 두께 (mm, 자동) = 브라켓 폭 + 흉벽 두께 + 교좌면 폭. */
|
||||
@@ -74,14 +125,17 @@ export interface ResolvedShape {
|
||||
footingBackM: number;
|
||||
pileEdgeM: number;
|
||||
pileSpacingM: number;
|
||||
/** 말뚝 열 t (m): 배면 3열, 2열, 전면 1열. */
|
||||
pileRowsT: number[];
|
||||
/** 열별 본수 (배면 3열, 2열, 전면 1열의 순서), 0 = 본수 규칙. */
|
||||
pileCounts: number[];
|
||||
pileDiameterM: number;
|
||||
blindingMarginM: number;
|
||||
blindingThicknessM: number;
|
||||
/** 인접 무시 기준 (m). */
|
||||
seatStepIgnoreM: number;
|
||||
}
|
||||
|
||||
/** The pile rows (t, m) of the spec: 배면 열, 중간 열, 전면 열. Not an input on the card. */
|
||||
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, ...normalizeShape(changes) };
|
||||
@@ -93,11 +147,15 @@ export function resolveShape(changes?: Partial<AbutShape>): ResolvedShape {
|
||||
parapetBackM: -v.parapetThicknessMm / 1000,
|
||||
bracketHeightM: v.bracketHeightMm / 1000,
|
||||
footingThicknessM: v.footingThicknessMm / 1000,
|
||||
footingFrontM: v.footingFrontMm / 1000,
|
||||
footingBackM: v.footingBackMm / 1000,
|
||||
footingFrontM: footingEnds(v).frontMm / 1000,
|
||||
footingBackM: footingEnds(v).backMm / 1000,
|
||||
pileEdgeM: v.pileEdgeMm / 1000,
|
||||
pileSpacingM: v.pileSpacingMm / 1000,
|
||||
pileRowsT: PILE_ROWS_T,
|
||||
pileRowsT: [pileRowsMm(v).back / 1000, pileRowsMm(v).middle / 1000, pileRowsMm(v).front / 1000],
|
||||
pileCounts: [v.pileCountRow3, v.pileCountRow2, v.pileCountRow1],
|
||||
pileDiameterM: v.pileDiameterMm / 1000,
|
||||
blindingMarginM: v.blindingMarginMm / 1000,
|
||||
blindingThicknessM: v.blindingThicknessMm / 1000,
|
||||
seatStepIgnoreM: v.seatStepIgnoreMm / 1000,
|
||||
};
|
||||
}
|
||||
@@ -113,9 +171,16 @@ export function shapeIssues(changes: Partial<AbutShape>): string[] {
|
||||
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.parapetThicknessMm + v.bracketWidthMm);
|
||||
if (!(v.footingFrontMm > v.wallFrontMm)) out.push('저판 앞굽 끝은 벽체 전면보다 앞에 있어야 합니다.');
|
||||
if (!(v.footingBackMm < wallBack)) out.push('저판 뒷굽 끝은 벽체 배면보다 뒤에 있어야 합니다.');
|
||||
if (!(v.footingToeMm > 0)) out.push('저판 앞굽은 0보다 커야 합니다.');
|
||||
if (!(footingEnds(v).heelMm > 0)) out.push(`저판 뒷굽(자동 ${footingEnds(v).heelMm})이 0보다 커야 합니다: 폭 > 앞굽 + 벽체 두께.`);
|
||||
if (!(v.pileDiameterMm > 0)) out.push('말뚝 지름은 0보다 커야 합니다.');
|
||||
if (!(v.pileRowBackEdgeMm > 0) || !(v.pileRowGap1Mm > 0) || !(v.pileRowGap2Mm > 0) || !(pileRowsMm(v).frontEdgeMm > 0)) {
|
||||
out.push('말뚝 열 위치는 0보다 크고, 합이 저판 폭보다 작아야 합니다 (전면 연단 > 0).');
|
||||
}
|
||||
for (const n of [v.pileCountRow1, v.pileCountRow2, v.pileCountRow3]) {
|
||||
if (!(Number.isInteger(n) && n >= 0)) out.push('열별 말뚝 본수는 0(규칙) 이상의 정수여야 합니다.');
|
||||
}
|
||||
if (!(v.blindingMarginMm >= 0) || !(v.blindingThicknessMm >= 0)) out.push('버림 여유와 두께는 0 이상이어야 합니다.');
|
||||
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