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;
|
||||
|
||||
+136
-4
@@ -8,8 +8,9 @@ import { parseAbutments, toAbutInput, type AbutRecord } from './core/abut/parse'
|
||||
import { buildAbut } from './core/abut/geometry';
|
||||
import { structureOrder } from './core/abut/order';
|
||||
import { ABUT_DETAIL_LABELS, ABUT_SEGMENT_HEADER, abutInputRows, abutSegmentRows, abutSummaryRows } from './core/abut/summary';
|
||||
import { applyAbutEdit, applyBearingEdit, applyBearingRule, applyShapeEdit, ABUT_EDIT_FIELDS, BEARING_RULE_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, resolveShape, wallThicknessMm } from './core/abut/shape';
|
||||
import { applyAbutEdit, applyBearingEdit, applyBearingRule, applyShapeEdit, applyShapeEdits, ABUT_EDIT_FIELDS, BEARING_RULE_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit';
|
||||
import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, wallThicknessMm, type AbutShape } from './core/abut/shape';
|
||||
import { abutFootingPlanSvg } from './view/abut-footing-svg';
|
||||
import { abutWallElevationSvg, abutWallSectionSvg, type AbutWallDrawing } from './view/abut-wall-svg';
|
||||
import { abutShown, piersShown } from './core/abut/visibility';
|
||||
import { abutBearingPlanSvg, abutBearingSectionSvg, abutSolplateSvg, type AbutBearingDetail } from './view/abut-bearing-svg';
|
||||
@@ -27,7 +28,7 @@ import { abutMemberRows, ABUT_MEMBER_EDITABLE, type AbutMemberId } from './core/
|
||||
import type { AbutModel, WingForm } from './core/abut/geometry';
|
||||
import { buildMembers, type AbutMembers } from './core/abut/members';
|
||||
import { buildWings } from './core/abut/wings';
|
||||
import { buildAbutGroup, cameraForBox } from './view/abut-scene';
|
||||
import { abutSkewK, buildAbutGroup, cameraForBox } from './view/abut-scene';
|
||||
import { fitCamera } from './view/fit';
|
||||
import { buildPier, type PierModel } from './core/geometry';
|
||||
import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate';
|
||||
@@ -104,7 +105,7 @@ const PART_GROUPS: { label: string; pier: PartName[]; abut: string[] }[] = [
|
||||
{ label: '기둥', pier: ['columns'], abut: [] },
|
||||
{ label: '확대기초', pier: ['footings'], abut: ['footing'] },
|
||||
{ label: '말뚝', pier: ['piles'], abut: ['piles'] },
|
||||
{ label: '버림', pier: ['blinding'], abut: [] },
|
||||
{ label: '버림', pier: ['blinding'], abut: ['blinding'] },
|
||||
{ label: '지반면', pier: ['ground'], abut: ['ground'] },
|
||||
{ label: '벽체·흉벽', pier: [], abut: ['wall', 'parapet'] },
|
||||
// 헌치 sits at the wing ends (wings.ts builds it per wing), so it follows the 날개벽 toggle (2026-11-03).
|
||||
@@ -892,6 +893,94 @@ function fillAbutBearingCard(
|
||||
box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule, H).join(' · ')}`, 'muted small abut-checks'));
|
||||
}
|
||||
|
||||
/** 확대기초 · 말뚝 카드의 자동 값, 열 위치, 열별 본수 표(− / +), 메모 (시안, 2026-11-04). 말뚝 길이는 ①에서 고친다. */
|
||||
function fillAbutFootingExtras(
|
||||
box: HTMLDivElement,
|
||||
rec: AbutRecord,
|
||||
v: AbutShape,
|
||||
built: { model: AbutModel; members: AbutMembers },
|
||||
apply: (next: AbutRecord) => void,
|
||||
fail: (err: unknown, input: HTMLInputElement, back: string) => void,
|
||||
idOf: (suffix: string) => string,
|
||||
): void {
|
||||
const ends = footingEnds(v);
|
||||
const rows = pileRowsMm(v);
|
||||
const L = built.model.length;
|
||||
const auto = (name: string, value: string, note: string) => box.append(el('label', name), el('span', value, 'num'), el('span', note, 'muted small'));
|
||||
auto('뒷굽 (자동)', `${ends.heelMm} mm`, '= 폭 − 앞굽 − 벽체 두께');
|
||||
auto('길이 (자동)', `${L.toFixed(3)} m`, '교좌면 길이 (skew)');
|
||||
auto('말뚝 길이', `${Math.round(rec.pileLengthMm)} mm`, '① 기본 정보에서 수정');
|
||||
auto('선단 EL (자동)', built.members.pileBottomEl === undefined ? '—' : `${built.members.pileBottomEl.toFixed(3)} m`, '= 기초 하단 − 말뚝 길이');
|
||||
// 열 위치 (배면부터): "650 · 2000 · 1300 · 650". The last one (전면 연단) follows from the 폭.
|
||||
const id = idOf('pile-rows');
|
||||
const lab = el('label', '열 위치 · 배면부터 (mm)');
|
||||
lab.htmlFor = id;
|
||||
const text = `${v.pileRowBackEdgeMm} · ${v.pileRowGap1Mm} · ${v.pileRowGap2Mm} · ${rows.frontEdgeMm}`;
|
||||
const input = el('input');
|
||||
input.id = id;
|
||||
input.type = 'text';
|
||||
input.value = text;
|
||||
input.addEventListener('change', () => {
|
||||
const n = input.value.split(/[·,\s]+/).filter(Boolean).map(Number);
|
||||
try {
|
||||
if (n.length < 3 || n.some((x) => !Number.isFinite(x))) throw new Error('열 위치는 "650 · 2000 · 1300 · 650"처럼 숫자 3~4개로 입력하세요.');
|
||||
if (n.length === 4 && Math.abs(n.reduce((a, b) => a + b, 0) - v.footingWidthMm) > 0.5) throw new Error(`열 위치의 합이 폭 ${v.footingWidthMm}과 같아야 합니다.`);
|
||||
apply(applyShapeEdits(rec, { pileRowBackEdgeMm: n[0], pileRowGap1Mm: n[1], pileRowGap2Mm: n[2] }));
|
||||
} catch (err) {
|
||||
fail(err, input, text);
|
||||
}
|
||||
});
|
||||
box.append(lab, input, el('span', `전면 연단 ${rows.frontEdgeMm} (자동)`, 'muted small'));
|
||||
auto('본수 규칙', `int((L − ${2 * v.pileEdgeMm})/${v.pileSpacingMm}) + 1`, `= ${Math.floor((L * 1000 - 2 * v.pileEdgeMm) / v.pileSpacingMm) + 1}본`);
|
||||
// 열별 말뚝 본수 (− / +): 0 = 본수 규칙.
|
||||
const piles = built.members.piles;
|
||||
const total = piles.reduce((a, r) => a + r.s.length, 0);
|
||||
box.append(el('p', `열별 말뚝 본수 · 합계 ${total}본`, 'lbl'));
|
||||
const table = document.createElement('table');
|
||||
table.className = 'abut-seg';
|
||||
const head = table.createTHead().insertRow();
|
||||
for (const h of ['열', '위치 (교대축 기준)', '본수', '본 간격']) head.append(el('th', h));
|
||||
const body = table.createTBody();
|
||||
const order: { key: 'pileCountRow1' | 'pileCountRow2' | 'pileCountRow3'; name: string; row: number }[] = [
|
||||
{ key: 'pileCountRow1', name: '1열 · 전면', row: 2 },
|
||||
{ key: 'pileCountRow2', name: '2열', row: 1 },
|
||||
{ key: 'pileCountRow3', name: '3열 · 배면', row: 0 },
|
||||
];
|
||||
for (const o of order) {
|
||||
const r = piles[o.row];
|
||||
if (!r) continue;
|
||||
const n = r.s.length;
|
||||
const gap = n > 1 ? Math.round(((L - 2 * v.pileEdgeMm / 1000) / (n - 1)) * 1000) : 0;
|
||||
const tr = body.insertRow();
|
||||
tr.append(el('td', o.name), el('td', `${r.t >= 0 ? '+' : '−'}${Math.round(Math.abs(r.t) * 1000)}`));
|
||||
const cell = el('td');
|
||||
const step = (delta: number) => {
|
||||
const b = el('button', delta < 0 ? '−' : '+', 'btn small');
|
||||
b.type = 'button';
|
||||
b.disabled = delta < 0 && n <= 2;
|
||||
b.addEventListener('click', () => {
|
||||
try {
|
||||
apply(applyShapeEdit(rec, o.key, n + delta));
|
||||
} catch (err) {
|
||||
$<HTMLElement>('abut-panel-plan-msg').textContent = (err as Error).message;
|
||||
}
|
||||
});
|
||||
return b;
|
||||
};
|
||||
cell.append(step(-1), el('span', ` ${n} `, 'num'), step(1));
|
||||
if (v[o.key] > 0) cell.append(el('span', ' 수정됨', 'muted small abut-note-warn'));
|
||||
tr.append(cell, el('td', n > 1 ? `${v.pileEdgeMm} · ${n - 1}@${gap} · ${v.pileEdgeMm}` : `${v.pileEdgeMm}`));
|
||||
}
|
||||
box.append(table);
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
'시안 메모 · 확대기초는 교대 저판입니다(엑셀 "저판"). skew 평면에서는 말뚝 열이 skew만큼 밀리고, 연단 650은 그 열의 끝에서 잽니다. 직사각 평면은 다음 단계입니다. 말뚝 지름(D600 / 609.6)과 전면 열 본수는 확인 필요(QA-5).',
|
||||
'muted small abut-memo',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Shows only the card of the open row in the ② 교대 editor; the basic items stay visible. */
|
||||
function applyAbutMemberCard(rec: AbutRecord): void {
|
||||
const member = state.abutMember;
|
||||
@@ -1085,6 +1174,48 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
shape.append(figs);
|
||||
}
|
||||
}
|
||||
// ② 확대기초 · 말뚝 카드 (시안, 2026-11-04): 평면 · 말뚝 선택 → 평면 삽도 → 입력 격자 → 자동 값 → 열 위치 → 열별 본수 → 메모.
|
||||
const shapeAll = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
||||
if (planCard) {
|
||||
const pick = (title: string, options: [string, boolean, string?][]) => {
|
||||
const row = el('div', undefined, 'abut-bearing-pick');
|
||||
row.append(el('span', title, 'muted small'));
|
||||
for (const [name, on, why] of options) {
|
||||
const b = el('button', name, 'btn small');
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(on));
|
||||
if (why) {
|
||||
b.disabled = true;
|
||||
b.title = why;
|
||||
}
|
||||
row.append(b);
|
||||
}
|
||||
return row;
|
||||
};
|
||||
footingBox.append(
|
||||
pick('평면', [['skew 평행사변형 (기본)', true], ['직사각', false, '다음 단계 (길이 = 교좌면 길이 + 폭 × |cot skew|, 100 올림)']]),
|
||||
pick('말뚝', [[`원기둥 D${shapeAll.pileDiameterMm}`, true], ['강관 · PHC', false, '규칙 미정']]),
|
||||
);
|
||||
if (built) {
|
||||
const rows = pileRowsMm(shapeAll);
|
||||
const names = ['3열 · 배면', '2열', '1열 · 전면'];
|
||||
const fig = el('figure', undefined, 'abut-fig abut-footing-fig');
|
||||
fig.innerHTML = abutFootingPlanSvg({
|
||||
lengthM: built.model.length,
|
||||
skewK: abutSkewK(built.model),
|
||||
footingFrontM: built.members.shape.footingFrontM,
|
||||
footingBackM: built.members.shape.footingBackM,
|
||||
wallFrontM: built.members.shape.wallFrontM,
|
||||
wallBackM: built.members.shape.wallBackM,
|
||||
rows: built.members.piles.map((r, i) => ({ t: r.t, s: r.s, name: names[i] ?? `${i + 1}열` })),
|
||||
pileDiameterM: built.members.shape.pileDiameterM,
|
||||
rowDimsMm: [shapeAll.pileRowBackEdgeMm, shapeAll.pileRowGap1Mm, shapeAll.pileRowGap2Mm, rows.frontEdgeMm],
|
||||
footingWidthMm: shapeAll.footingWidthMm,
|
||||
});
|
||||
fig.append(el('figcaption', '평면 · 축척 같음 · 실선 = 벽체, 굵은 선 = 확대기초 · 열마다 skew만큼 밀림', 'muted small'));
|
||||
footingBox.append(fig);
|
||||
}
|
||||
}
|
||||
for (const f of SHAPE_EDIT_FIELDS) {
|
||||
const current = normalizeShape(rec.shape)[f.key] ?? ABUT_SHAPE_DEFAULT[f.key];
|
||||
const id = idOf(`shape-${f.key}`);
|
||||
@@ -1132,6 +1263,7 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
qa.append(el('p', '확인 필요 (spec_abut)', 'lbl'));
|
||||
for (const line of ABUT_QA_NOTES) qa.append(el('p', line, 'muted small'));
|
||||
shape.append(qa);
|
||||
if (built) fillAbutFootingExtras(footingBox, rec, shapeAll, built, apply, fail, idOf);
|
||||
shape.append(
|
||||
el(
|
||||
'p',
|
||||
|
||||
@@ -918,3 +918,10 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
|
||||
#abut-panel-plan .abut-note-warn { color: #8a5a00; }
|
||||
#abut-panel-plan .abut-memo { background: #f6f8fa; border: 1px solid #d5dbe2; border-radius: 6px; padding: 8px; }
|
||||
@media (max-width: 900px) { #abut-panel-plan .abut-wall-figs { grid-template-columns: minmax(0, 1fr); } }
|
||||
|
||||
/* ② 확대기초 · 말뚝 카드 (2026-11-04): the plan drawing, notes and the 열별 본수 table span the card. */
|
||||
#abut-panel-plan-footing > figure, #abut-panel-plan-footing > p, #abut-panel-plan-footing > table { grid-column: 1 / -1; }
|
||||
#abut-panel-plan-footing > .abut-seg { border-collapse: collapse; font-size: 12px; width: 100%; }
|
||||
#abut-panel-plan-footing > .abut-seg th, #abut-panel-plan-footing > .abut-seg td { border-bottom: 1px solid #e6eaef; padding: 4px 6px; text-align: left; font-family: 'JetBrains Mono', ui-monospace, monospace; }
|
||||
#abut-panel-plan-footing > .abut-seg th { color: #55606e; font-weight: 500; font-family: inherit; }
|
||||
#abut-panel-plan .abut-footing-fig { max-width: 760px; }
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 교대 확대기초 · 말뚝 평면 삽도 (2026-11-04, 시안 ② 확대기초·말뚝 카드). 순수 문자열 함수.
|
||||
* 생성본 값으로 그린다: 저판(skew 평행사변형, 굵은 선), 벽체 윤곽(실선), 교대축(점선), 말뚝 3열(원), 열 위치, 폭 치수.
|
||||
* 가로(s)와 세로(t) 축척이 같다. 위가 전면, 아래가 배면이다.
|
||||
*/
|
||||
|
||||
export interface AbutFootingDrawing {
|
||||
/** 교대축 길이 L (m). */
|
||||
lengthM: number;
|
||||
/** s per m of t along the wing (skew): s = s_axis + k·t. */
|
||||
skewK: number;
|
||||
/** t (m): 저판 앞굽 끝, 뒷굽 끝, 벽체 전면, 벽체 배면. */
|
||||
footingFrontM: number;
|
||||
footingBackM: number;
|
||||
wallFrontM: number;
|
||||
wallBackM: number;
|
||||
/** 말뚝 열: t (m), s (m, 교대축 기준, skew 전), 열 이름 (1열 · 전면 …). */
|
||||
rows: { t: number; s: number[]; name: string }[];
|
||||
pileDiameterM: number;
|
||||
/** 열 위치 치수 (mm, 배면부터): 배면 연단, 간격, 간격, 전면 연단. */
|
||||
rowDimsMm: [number, number, number, number];
|
||||
footingWidthMm: number;
|
||||
}
|
||||
|
||||
const INK = '#18212b';
|
||||
const MUTED = '#55606e';
|
||||
const label = (x: number, y: number, s: string, anchor = 'start', fill = INK) =>
|
||||
`<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" font-size="11" fill="${fill}" font-family="JetBrains Mono, monospace">${s}</text>`;
|
||||
|
||||
export function abutFootingPlanSvg(d: AbutFootingDrawing): string {
|
||||
const k = 36; // px per m, both directions
|
||||
const sMin = Math.min(d.skewK * d.footingFrontM, d.skewK * d.footingBackM) - 0.4;
|
||||
const sMax = d.lengthM + Math.max(d.skewK * d.footingFrontM, d.skewK * d.footingBackM) + 0.4;
|
||||
const tTop = d.footingFrontM + 0.4;
|
||||
const X = (s: number, t: number) => 20 + (s + d.skewK * t - sMin) * k;
|
||||
const Y = (t: number) => 14 + (tTop - t) * k;
|
||||
const W = 20 + (sMax - sMin) * k + 170;
|
||||
const H = Y(d.footingBackM - 0.4) + 34;
|
||||
const quad = (t0: number, t1: number, style: string) =>
|
||||
`<polygon points="${[[0, t0], [d.lengthM, t0], [d.lengthM, t1], [0, t1]].map(([s, t]) => `${X(s, t).toFixed(1)},${Y(t).toFixed(1)}`).join(' ')}" ${style}/>`;
|
||||
const parts: string[] = [];
|
||||
parts.push(quad(d.footingBackM, d.footingFrontM, `fill="#e6ebf0" stroke="${INK}" stroke-width="2"`));
|
||||
parts.push(quad(d.wallBackM, d.wallFrontM, `fill="none" stroke="${MUTED}"`));
|
||||
parts.push(`<line x1="${X(0, 0).toFixed(1)}" y1="${Y(0).toFixed(1)}" x2="${X(d.lengthM, 0).toFixed(1)}" y2="${Y(0).toFixed(1)}" stroke="#c0392b" stroke-dasharray="6 3"/>`);
|
||||
parts.push(label(X(d.lengthM, 0) + 6, Y(0) + 4, '교대축', 'start', '#c0392b'));
|
||||
const r = Math.max(2, (d.pileDiameterM / 2) * k);
|
||||
for (const row of d.rows) {
|
||||
for (const s of row.s) parts.push(`<circle cx="${X(s, row.t).toFixed(1)}" cy="${Y(row.t).toFixed(1)}" r="${r.toFixed(1)}" fill="#ffffff" stroke="${INK}"/>`);
|
||||
const tLabel = `${row.t >= 0 ? '+' : '−'}${Math.round(Math.abs(row.t) * 1000)}`;
|
||||
parts.push(label(X(d.lengthM, row.t) + r + 8, Y(row.t) + 4, `${row.name} · t ${tLabel} · ${row.s.length}본`));
|
||||
}
|
||||
const [e1, g1, g2, e2] = d.rowDimsMm;
|
||||
parts.push(label(20, H - 10, `${d.footingWidthMm} = 뒷굽쪽 ${e1} · ${g1} · ${g2} · ${e2} 전면 · 위 = 전면`, 'start', MUTED));
|
||||
return [
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="확대기초 평면: skew 평행사변형, 폭 ${d.footingWidthMm}, 말뚝 ${d.rows.map((r2) => r2.s.length).join(' + ')}본">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
+26
-4
@@ -12,7 +12,7 @@ import { buildWings, HAUNCH_LEG_M, WING_THICKNESS_M, type WingSolid } from '../c
|
||||
import type { TerrainGrid } from '../core/ground-model';
|
||||
import { terrainGeometry } from './terrain';
|
||||
|
||||
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'slab'] as const;
|
||||
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'slab'] as const;
|
||||
/** 접속슬래브 두께 (m), spec_abut §4.8. */
|
||||
const SLAB_THICKNESS_M = 0.45;
|
||||
/** 접속슬래브 길이 (배면 쪽, m), spec_abut §4.8 "7000 (날개벽 축 방향)". */
|
||||
@@ -118,6 +118,12 @@ function wingDirectionAxis(model: AbutModel): { ws: number; wt: number } {
|
||||
return { ws: w.e * model.u.e + w.n * model.u.n, wt: w.e * front.e + w.n * front.n };
|
||||
}
|
||||
|
||||
/** Skew of the plan (s per m of t along the wing), the same one the 저판 · 벽체 · 말뚝 열 use. */
|
||||
export function abutSkewK(model: AbutModel): number {
|
||||
const d = wingDirectionAxis(model);
|
||||
return d.ws / d.wt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Placement of the abutment in the pier scene (2026-11-03). The pier scene maps (E, N, EL) to
|
||||
* (E − pivot.E, EL, −(N − pivot.N)); the abutment's local (s, t, EL) → (E, N) with s along u and t along the front
|
||||
@@ -220,6 +226,20 @@ export function buildAbutGroup(
|
||||
[skewK * members.shape.footingFrontM, members.shape.footingFrontM],
|
||||
];
|
||||
add('footing', prism(footProfile, members.shape.footingThicknessM, new THREE.Vector3(0, footingBottom, 0), ex, ez, ey), 0x5a6a78);
|
||||
// 버림 (2026-11-04): under the 저판, wider by the 여유 on every side, the same skew.
|
||||
const m = members.shape.blindingMarginM;
|
||||
const th = members.shape.blindingThicknessM;
|
||||
if (th > 0) {
|
||||
const tb = members.shape.footingBackM - m;
|
||||
const tf = members.shape.footingFrontM + m;
|
||||
const blindProfile: [number, number][] = [
|
||||
[skewK * tb - m, tb],
|
||||
[L + skewK * tb + m, tb],
|
||||
[L + skewK * tf + m, tf],
|
||||
[skewK * tf - m, tf],
|
||||
];
|
||||
add('blinding', prism(blindProfile, th, new THREE.Vector3(0, footingBottom - th, 0), ex, ez, ey), 0x9aa29a);
|
||||
}
|
||||
}
|
||||
|
||||
// 받침블럭 (spec §4.2, §6.6): concrete 1200 × 1200 from the 교좌면 to 50 below z, and the 몰탈패드 1000 × 1000 × 50 up to z.
|
||||
@@ -267,13 +287,15 @@ export function buildAbutGroup(
|
||||
slabGroup.add(slabMesh);
|
||||
root.add(slabGroup);
|
||||
|
||||
// 말뚝: D 600 (r 0.3) at each row and bar, from the 기초 하단 down to the 선단.
|
||||
// 말뚝: 원기둥 (지름 입력, 기본 D600) at each row, from the 기초 하단 down to the 선단.
|
||||
if (members.pileBottomEl !== undefined && footingBottom !== undefined) {
|
||||
const height = footingBottom - members.pileBottomEl;
|
||||
for (const row of members.piles) {
|
||||
for (const s of row.s) {
|
||||
const geom = new THREE.CylinderGeometry(0.3, 0.3, height, 16);
|
||||
geom.translate(s, members.pileBottomEl + height / 2, row.t);
|
||||
// 말뚝 지름은 입력값 (QA-5). Each row moves along s with the skew, like the 저판 (시안: 열마다 skew만큼 밀림).
|
||||
const r = members.shape.pileDiameterM / 2;
|
||||
const geom = new THREE.CylinderGeometry(r, r, height, 16);
|
||||
geom.translate(s + skewK * row.t, members.pileBottomEl + height / 2, row.t);
|
||||
add('piles', geom, 0x7d8b99);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as THREE from 'three';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildAbut } from '../src/core/abut/geometry';
|
||||
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
|
||||
import { buildMembers } from '../src/core/abut/members';
|
||||
import { buildWings } from '../src/core/abut/wings';
|
||||
import { abutSkewK, buildAbutGroup } from '../src/view/abut-scene';
|
||||
import { abutFootingPlanSvg } from '../src/view/abut-footing-svg';
|
||||
|
||||
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
|
||||
const input = toAbutInput(rec);
|
||||
const model = buildAbut(input);
|
||||
const group = (changes = {}) => {
|
||||
const m = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm }, changes);
|
||||
return { m, g: buildAbutGroup(model, m, buildWings(model, m)) };
|
||||
};
|
||||
|
||||
describe('교대 확대기초 · 말뚝 (시안 ②, 3D)', () => {
|
||||
it('draws the 버림 under the 저판: 100 thick, 100 wider on each side', () => {
|
||||
const { m, g } = group();
|
||||
const blind = new THREE.Box3().setFromObject(g.getObjectByName('blinding') as THREE.Object3D);
|
||||
const foot = new THREE.Box3().setFromObject(g.getObjectByName('footing') as THREE.Object3D);
|
||||
expect(blind.max.y).toBeCloseTo(m.footingBottomEl!, 4); // float32 vertices
|
||||
expect(blind.max.y - blind.min.y).toBeCloseTo(0.1, 4);
|
||||
expect(foot.min.z - blind.min.z).toBeCloseTo(0.1, 4);
|
||||
expect(blind.max.z - foot.max.z).toBeCloseTo(0.1, 4);
|
||||
});
|
||||
|
||||
it('no 버림 when its thickness is 0', () => {
|
||||
expect(group({ blindingThicknessMm: 0 }).g.getObjectByName('blinding')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('the piles follow the 지름 input and shift with the skew row by row', () => {
|
||||
const { m, g } = group({ pileDiameterMm: 800 });
|
||||
const piles = (g.getObjectByName('piles') as THREE.Group).children;
|
||||
expect(piles).toHaveLength(18);
|
||||
const box = new THREE.Box3().setFromObject(piles[0]);
|
||||
expect(box.max.x - box.min.x).toBeCloseTo(0.8, 2);
|
||||
const k = abutSkewK(model);
|
||||
const row = m.piles[0];
|
||||
expect((box.min.x + box.max.x) / 2).toBeCloseTo(row.s[0] + k * row.t, 6);
|
||||
});
|
||||
|
||||
it('평면 삽도: the rows, their t and counts, and the width line', () => {
|
||||
const { m } = group();
|
||||
const svg = abutFootingPlanSvg({
|
||||
lengthM: model.length, skewK: abutSkewK(model), footingFrontM: m.shape.footingFrontM, footingBackM: m.shape.footingBackM,
|
||||
wallFrontM: m.shape.wallFrontM, wallBackM: m.shape.wallBackM,
|
||||
rows: m.piles.map((r, i) => ({ t: r.t, s: r.s, name: ['3열 · 배면', '2열', '1열 · 전면'][i] })),
|
||||
pileDiameterM: m.shape.pileDiameterM, rowDimsMm: [650, 2000, 1300, 650], footingWidthMm: 4600,
|
||||
});
|
||||
for (const s of ['1열 · 전면 · t +2150 · 6본', '2열 · t +850 · 6본', '3열 · 배면 · t −1150 · 6본', '4600 = 뒷굽쪽 650 · 2000 · 1300 · 650 전면', '교대축']) expect(svg).toContain(s);
|
||||
expect(svg.match(/<circle /g)).toHaveLength(18);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, resolveShape, shapeIssues, wallThicknessMm, type AbutShape } from '../src/core/abut/shape';
|
||||
import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, shapeIssues, wallThicknessMm, type AbutShape } from '../src/core/abut/shape';
|
||||
|
||||
describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말뚝)', () => {
|
||||
it('the defaults equal the spec values (wall +1.6 … −0.8, footing +2.8 … −1.8, piles 650 / 2000)', () => {
|
||||
@@ -22,8 +22,10 @@ describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말
|
||||
});
|
||||
|
||||
it('refuses a footing that does not cover the wall, and non-positive sizes', () => {
|
||||
expect(shapeIssues({ footingBackMm: -500 }).join(' ')).toContain('뒷굽');
|
||||
expect(shapeIssues({ footingFrontMm: 1000 }).join(' ')).toContain('앞굽');
|
||||
expect(shapeIssues({ footingWidthMm: 3300 }).join(' ')).toContain('뒷굽'); // 3300 − 1200 − 2400 < 0
|
||||
expect(shapeIssues({ footingToeMm: 0 }).join(' ')).toContain('앞굽');
|
||||
expect(shapeIssues({ pileRowGap2Mm: 2000 }).join(' ')).toContain('열 위치'); // 650 + 2000 + 2000 ≥ 4600 − 0
|
||||
expect(shapeIssues({ pileCountRow1: 1.5 }).join(' ')).toContain('열별 말뚝 본수');
|
||||
expect(shapeIssues({ bracketWidthMm: -1 }).join(' ')).toContain('브라켓 폭');
|
||||
expect(shapeIssues({ seatStepIgnoreMm: -1 }).join(' ')).toContain('인접 무시 기준');
|
||||
expect(shapeIssues({ pileSpacingMm: 0 }).join(' ')).toContain('말뚝');
|
||||
@@ -44,8 +46,10 @@ const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abu
|
||||
describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
|
||||
it('the card has one field per shape value, in the mockup order', () => {
|
||||
expect(SHAPE_EDIT_FIELDS.map((f) => f.key)[0]).toBe('wallFrontMm');
|
||||
expect(SHAPE_EDIT_FIELDS).toHaveLength(14);
|
||||
expect(SHAPE_EDIT_FIELDS.filter((f) => f.group === 'footing').map((f) => f.key)).toEqual(['footingThicknessMm', 'footingFrontMm', 'footingBackMm', 'pileEdgeMm', 'pileSpacingMm']);
|
||||
expect(SHAPE_EDIT_FIELDS).toHaveLength(17);
|
||||
expect(SHAPE_EDIT_FIELDS.filter((f) => f.group === 'footing').map((f) => f.key)).toEqual([
|
||||
'footingWidthMm', 'footingThicknessMm', 'footingToeMm', 'blindingMarginMm', 'blindingThicknessMm', 'pileDiameterMm', 'pileEdgeMm', 'pileSpacingMm',
|
||||
]);
|
||||
});
|
||||
|
||||
it('the new drawing values do not change the geometry (the model ignores them)', () => {
|
||||
@@ -59,7 +63,7 @@ describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
|
||||
});
|
||||
|
||||
it('a footing that does not cover the wall is refused by the edit rule', () => {
|
||||
expect(() => applyShapeEdit(rec, 'footingBackMm', -500)).toThrow('뒷굽');
|
||||
expect(() => applyShapeEdit(rec, 'footingWidthMm', 3300)).toThrow('뒷굽');
|
||||
expect(rec.shape).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -83,4 +87,19 @@ describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
|
||||
expect(buildMembers(model, foundation).wallSegments).toHaveLength(5);
|
||||
expect(buildMembers(model, foundation, { seatStepIgnoreMm: 100 }).wallSegments).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('저판: 폭 4600 · 앞굽 1200 → 뒷굽 1000 (자동), the ends +2.8 / −1.8; old 앞굽 끝 · 뒷굽 끝 files open the same', () => {
|
||||
expect(footingEnds(ABUT_SHAPE_DEFAULT)).toEqual({ frontMm: 2800, backMm: -1800, heelMm: 1000 });
|
||||
expect(normalizeShape({ footingFrontMm: 3000, footingBackMm: -2000 } as Partial<AbutShape>)).toEqual({ footingToeMm: 1400, footingWidthMm: 5000 });
|
||||
});
|
||||
|
||||
it('열 위치 650 · 2000 · 1300 · (650) gives the rows −1.15 / +0.85 / +2.15; 열별 본수 overrides the rule', () => {
|
||||
expect(pileRowsMm(ABUT_SHAPE_DEFAULT)).toEqual({ back: -1150, middle: 850, front: 2150, frontEdgeMm: 650 });
|
||||
const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) });
|
||||
const rule = buildMembers(model, {});
|
||||
expect(rule.piles.map((r) => r.s.length)).toEqual([6, 6, 6]);
|
||||
const more = buildMembers(model, {}, { pileCountRow1: 7 });
|
||||
expect(more.piles.map((r) => r.s.length)).toEqual([6, 6, 7]); // 3열 배면, 2열, 1열 전면
|
||||
expect(more.piles[2].t).toBeCloseTo(2.15, 9);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user