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
@@ -199,6 +199,7 @@
|
||||
<div id="abut-panel-plan-bearings" class="edit-row"></div>
|
||||
<div id="abut-panel-plan-wing" class="edit-row"></div>
|
||||
<div id="abut-panel-plan-shape" class="edit-row abut-shape-view"></div>
|
||||
<div id="abut-panel-plan-footing" class="edit-row abut-shape-view"></div>
|
||||
<p id="abut-panel-plan-msg" class="muted" aria-live="polite"></p>
|
||||
</section>
|
||||
<section class="panel viewer" aria-labelledby="member-title">
|
||||
|
||||
+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;
|
||||
}
|
||||
+91
-15
@@ -9,7 +9,8 @@ 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, resolveShape } from './core/abut/shape';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, resolveShape, wallThicknessMm } from './core/abut/shape';
|
||||
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';
|
||||
import {
|
||||
@@ -718,7 +719,7 @@ const ABUT_QA_NOTES = [
|
||||
const ABUT_SLOTS = {
|
||||
shape: { root: 'abut-panel', name: 'abut-panel-name', input: 'abut-panel-input', toggle: 'abut-panel-edit-toggle', editor: 'abut-panel-editor', summary: 'abut-panel-summary', basic: 'abut-panel-basic', bearings: 'abut-panel-bearings', shape: 'abut-panel-shape', msg: 'abut-panel-msg', prefix: 'abs' },
|
||||
// ② has no 편집 toggle and no basic items: the card sits under the 3D view and shows the picked member (2026-11-03).
|
||||
plan: { wing: 'abut-panel-plan-wing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
plan: { wing: 'abut-panel-plan-wing', footing: 'abut-panel-plan-footing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
} as const;
|
||||
|
||||
/** Shows the 교대 입력 in place of the pier info when an abutment is selected (2026-11-03). */
|
||||
@@ -896,6 +897,7 @@ function applyAbutMemberCard(rec: AbutRecord): void {
|
||||
const member = state.abutMember;
|
||||
$<HTMLElement>('abut-panel-plan-bearings').hidden = member !== 'bearing';
|
||||
$<HTMLElement>('abut-panel-plan-shape').hidden = member !== 'shape';
|
||||
$<HTMLElement>('abut-panel-plan-footing').hidden = member !== 'footing';
|
||||
$<HTMLElement>('abut-panel-plan-wing').hidden = member !== 'wing';
|
||||
// The card title is the picked row; its 요약 follows the name (시안 머리줄).
|
||||
const slabWidth = abutSummaryRows(rec).find(([label]) => label === '접속슬래브 폭')?.[1] ?? '—';
|
||||
@@ -1022,8 +1024,69 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
bearings.append(zLab, zIn, sLab, sIn);
|
||||
}
|
||||
|
||||
// ② 벽체·흉벽 카드 (시안 순서, 2026-11-03): 벽체 형식 → 삽도 2개 → 입력 격자 → 자동 값 → 구간 표 → 확인 필요 → 메모.
|
||||
// 저판·말뚝 입력은 확대기초 · 말뚝 카드로 나눈다. ①은 모든 형상 입력을 한 곳에 둔다.
|
||||
const footingBox = 'footing' in slot ? $<HTMLDivElement>(slot.footing) : shape;
|
||||
footingBox.replaceChildren();
|
||||
let built: { model: AbutModel; members: AbutMembers } | null = null;
|
||||
if (planCard) {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
built = { model, members: buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape) };
|
||||
} catch {
|
||||
// A model that does not build shows no 삽도 or 구간 표 (the errors are reported on import).
|
||||
}
|
||||
const kinds = el('div', undefined, 'abut-bearing-pick');
|
||||
kinds.append(el('span', '벽체 형식', 'muted small'));
|
||||
for (const [name, on] of [['기본형', true], ['브라켓형', false], ['오목형', false], ['반일체식', false]] as const) {
|
||||
const b = el('button', name, 'btn small');
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(on));
|
||||
if (!on) {
|
||||
b.disabled = true;
|
||||
b.title = '규칙 미정 (QA-8)';
|
||||
}
|
||||
kinds.append(b);
|
||||
}
|
||||
kinds.append(el('span', '브라켓형 · 오목형 · 반일체식은 규칙 미정 (QA-8)', 'muted small'));
|
||||
shape.append(kinds);
|
||||
if (built) {
|
||||
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
||||
const s = built.members.shape;
|
||||
const drawing: AbutWallDrawing = {
|
||||
lengthM: built.model.length,
|
||||
segments: built.members.wallSegments,
|
||||
bearings: built.model.bearings,
|
||||
footingTopEl: built.members.footingTopEl,
|
||||
footingBottomEl: built.members.footingBottomEl ?? built.members.footingTopEl - s.footingThicknessM,
|
||||
parapetTopStartEl: built.members.parapetTopStartEl,
|
||||
parapetTopEndEl: built.members.parapetTopEndEl,
|
||||
wallFrontM: s.wallFrontM,
|
||||
parapetBackM: s.parapetBackM,
|
||||
wallBackM: s.wallBackM,
|
||||
footingFrontM: s.footingFrontM,
|
||||
footingBackM: s.footingBackM,
|
||||
bracketHeightM: s.bracketHeightM,
|
||||
shrinkJointCount: v.shrinkJointCount,
|
||||
bracketWidthMm: v.bracketWidthMm,
|
||||
parapetThicknessMm: v.parapetThicknessMm,
|
||||
seatWidthMm: v.wallFrontMm,
|
||||
};
|
||||
const figs = el('div', undefined, 'abut-wall-figs');
|
||||
for (const [svg, caption] of [
|
||||
[abutWallSectionSvg(drawing), '교대축 직각 단면 · ① 구간 · 축척 같음'],
|
||||
[abutWallElevationSvg(drawing), '정면 · 배면에서 본 모습 · 왼쪽 IP3(① 쪽) → 오른쪽 IP1 · 수축줄눈 표시'],
|
||||
]) {
|
||||
const fig = el('figure', undefined, 'abut-fig');
|
||||
fig.innerHTML = svg;
|
||||
fig.append(el('figcaption', caption, 'muted small'));
|
||||
figs.append(fig);
|
||||
}
|
||||
shape.append(figs);
|
||||
}
|
||||
}
|
||||
for (const f of SHAPE_EDIT_FIELDS) {
|
||||
const current = rec.shape?.[f.key] ?? ABUT_SHAPE_DEFAULT[f.key];
|
||||
const current = normalizeShape(rec.shape)[f.key] ?? ABUT_SHAPE_DEFAULT[f.key];
|
||||
const id = idOf(`shape-${f.key}`);
|
||||
const lab = el('label', f.label);
|
||||
lab.htmlFor = id;
|
||||
@@ -1036,33 +1099,46 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
fail(err, input, String(current));
|
||||
}
|
||||
});
|
||||
shape.append(lab, input, el('span', `기본 ${ABUT_SHAPE_DEFAULT[f.key]}`, 'muted small'));
|
||||
const note = el('span', `기본 ${ABUT_SHAPE_DEFAULT[f.key]}${f.note ? ` · ${f.note}` : ''}`, f.note ? 'muted small abut-note-warn' : 'muted small');
|
||||
(planCard && f.group === 'footing' ? footingBox : shape).append(lab, input, note);
|
||||
}
|
||||
// 구간 표 (시안 하단): one row per 교좌면 step, from the model (2026-11-03).
|
||||
if (planCard) {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
// 자동 값 (시안 입력 격자 마지막 줄): 벽체 두께, 흉벽 상단 IP3 / IP1 끝 (가정, QA-4), 교대축.
|
||||
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
||||
shape.append(el('label', '벽체 두께 (자동)'), el('span', `${wallThicknessMm(v)} mm`, 'num'), el('span', '= 브라켓 폭 + 흉벽 두께 + 교좌면 폭', 'muted small'));
|
||||
if (built) {
|
||||
shape.append(
|
||||
el('label', '흉벽 상단 IP3 / IP1 끝 (m)'),
|
||||
el('span', `${built.members.parapetTopEndEl.toFixed(3)} / ${built.members.parapetTopStartEl.toFixed(3)}`, 'num'),
|
||||
el('span', '가정 (QA-4)', 'muted small abut-note-warn'),
|
||||
el('label', '교대축 (자동)'),
|
||||
el('span', `${built.model.length.toFixed(3)} m`, 'num'),
|
||||
el('span', 'IP1 → IP3', 'muted small'),
|
||||
);
|
||||
// 구간 표 (시안 하단): one row per 교좌면 step.
|
||||
const table = document.createElement('table');
|
||||
table.className = 'abut-seg';
|
||||
const thead = table.createTHead().insertRow();
|
||||
for (const h of ABUT_SEGMENT_HEADER) thead.append(el('th', h));
|
||||
const tbody = table.createTBody();
|
||||
for (const row of abutSegmentRows(model, members)) {
|
||||
for (const row of abutSegmentRows(built.model, built.members)) {
|
||||
const tr = tbody.insertRow();
|
||||
for (const cell of row) tr.append(el('td', cell));
|
||||
}
|
||||
shape.append(table);
|
||||
} catch {
|
||||
// A model that does not build shows no 구간 표 (the errors are reported on import).
|
||||
shape.append(el('p', '교좌면 계단 구간 · IP3(① 쪽)에서 잰 거리', 'lbl'), table);
|
||||
}
|
||||
}
|
||||
// 확인 필요 (spec_abut, 시안 오른쪽 패널): the open 교대 questions that touch the shape (2026-11-03).
|
||||
if (planCard) {
|
||||
// 확인 필요 (spec_abut, 시안 오른쪽 패널): the open 교대 questions that touch the shape (2026-11-03).
|
||||
const qa = el('div', undefined, 'abut-qa');
|
||||
qa.append(el('p', '확인 필요 (spec_abut)', 'lbl'));
|
||||
for (const line of ABUT_QA_NOTES) qa.append(el('p', line, 'muted small'));
|
||||
shape.append(qa);
|
||||
shape.append(
|
||||
el(
|
||||
'p',
|
||||
'시안 메모 · 벽체는 교좌면 계단 구간마다 높이가 다릅니다. 흉벽 높이 2500은 ① 구간 교좌면 기준(가정, QA-4)이고, Level 좌·우는 계산에 쓰지 않습니다. 인접 무시 기준보다 작은 교좌면 단차는 한 구간으로 묶습니다.',
|
||||
'muted small abut-memo',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -910,3 +910,11 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
|
||||
#abut-panel-plan .abut-fig svg { display: block; width: 100%; height: auto; }
|
||||
#abut-panel-plan > .edit-row { padding: 0 12px 10px; }
|
||||
@media (max-width: 900px) { #abut-panel-plan .abut-bearing-figs { grid-template-columns: minmax(0, 1fr); } }
|
||||
|
||||
/* ② 벽체·흉벽 / 확대기초·말뚝 카드 (2026-11-03): the two 삽도 side by side, notes and tables across the card. */
|
||||
#abut-panel-plan-footing.abut-shape-view { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr); gap: 6px 10px; align-items: center; }
|
||||
#abut-panel-plan .abut-wall-figs { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 10px; }
|
||||
#abut-panel-plan-shape > p, #abut-panel-plan-shape > .abut-wall-figs { grid-column: 1 / -1; }
|
||||
#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); } }
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 교대 벽체·흉벽 삽도 (2026-11-03, 시안 ② 벽체·흉벽 카드): 교대축 직각 단면(① 구간)과 정면·배면도. 순수 문자열 함수.
|
||||
* 생성본(교대 모델·부재)의 값으로 그린다. 두 그림 모두 가로·세로 축척이 같다(시안 "축척 같음").
|
||||
*/
|
||||
|
||||
export interface AbutWallDrawing {
|
||||
/** 교대축 길이 L (m), IP1 → IP3. */
|
||||
lengthM: number;
|
||||
/** 교좌면 계단 구간 (s, m는 IP1에서), 구간마다 교좌면 EL. */
|
||||
segments: { sFrom: number; sTo: number; seatEl: number }[];
|
||||
/** 받침 (s, m), 받침 번호, 패드 상면 z (m), 교좌면 EL (m). */
|
||||
bearings: { no: number; s: number; z: number; seatEl: number }[];
|
||||
footingTopEl: number;
|
||||
footingBottomEl: number;
|
||||
/** 흉벽 상단 EL at s = 0 (IP1 끝) and s = L (IP3 끝). */
|
||||
parapetTopStartEl: number;
|
||||
parapetTopEndEl: number;
|
||||
/** t (m, 전면 +): 벽체 전면, 흉벽 배면, 벽체 배면, 저판 앞굽 끝·뒷굽 끝. */
|
||||
wallFrontM: number;
|
||||
parapetBackM: number;
|
||||
wallBackM: number;
|
||||
footingFrontM: number;
|
||||
footingBackM: number;
|
||||
bracketHeightM: number;
|
||||
shrinkJointCount: number;
|
||||
/** 시안 치수 줄 "브라켓 폭 + 흉벽 두께 + 교좌면 폭 = 벽체 두께" (mm). */
|
||||
bracketWidthMm: number;
|
||||
parapetThicknessMm: number;
|
||||
seatWidthMm: number;
|
||||
}
|
||||
|
||||
const INK = '#18212b';
|
||||
const MUTED = '#55606e';
|
||||
const CONC = '#d9c9a8';
|
||||
const CONC_DARK = '#c7b48d';
|
||||
const FOOT = '#b9c3ce';
|
||||
const t3 = (v: number) => v.toFixed(3);
|
||||
const label = (x: number, y: number, s: string, anchor = 'middle', 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>`;
|
||||
const poly = (pts: [number, number][], style: string) => `<polygon points="${pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ')}" ${style}/>`;
|
||||
|
||||
/** 흉벽 상단 EL at s (linear between the two ends). */
|
||||
function parapetTopAt(d: AbutWallDrawing, s: number): number {
|
||||
return d.parapetTopStartEl + ((d.parapetTopEndEl - d.parapetTopStartEl) * s) / d.lengthM;
|
||||
}
|
||||
|
||||
/**
|
||||
* 교대축 직각 단면 (① 구간 = 받침 1번이 놓인 구간): 저판, 벽체(기초 상면 ~ 교좌면), 흉벽 배면 단차부(브라켓 상면까지),
|
||||
* 흉벽(흉벽 상단까지), 받침 블럭. 왼쪽이 배면, 오른쪽이 전면.
|
||||
*/
|
||||
export function abutWallSectionSvg(d: AbutWallDrawing): string {
|
||||
const b1 = d.bearings.find((b) => b.no === 1) ?? d.bearings[0];
|
||||
const seg = d.segments.find((g) => b1.s >= g.sFrom && b1.s <= g.sTo) ?? d.segments[0];
|
||||
const sAt = b1.s;
|
||||
const top = parapetTopAt(d, sAt);
|
||||
const bracket = top - d.bracketHeightM;
|
||||
const k = 46; // px per m, both directions
|
||||
const tMin = d.footingBackM - 0.6;
|
||||
const elMax = top + 0.7;
|
||||
const X = (t: number) => (t - tMin) * k;
|
||||
const Y = (el: number) => (elMax - el) * k + 10;
|
||||
const W = (d.footingFrontM + 1.4 - tMin) * k;
|
||||
const H = Y(d.footingBottomEl) + 40;
|
||||
const rect = (t0: number, t1: number, e0: number, e1: number, style: string) =>
|
||||
poly([[X(t0), Y(e0)], [X(t1), Y(e0)], [X(t1), Y(e1)], [X(t0), Y(e1)]], style);
|
||||
const halfBlock = 0.6;
|
||||
const blockT = (d.wallFrontM + 0) / 2; // drawn at the middle of the 교좌면 width
|
||||
return [
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="교대축 직각 단면 (① 구간): 흉벽 상단 ${t3(top)}, 브라켓 상면 ${t3(bracket)}, 교좌면 ${t3(seg.seatEl)}, 저판 상면 ${t3(d.footingTopEl)}, 기초 하단 ${t3(d.footingBottomEl)}">`,
|
||||
rect(d.footingBackM, d.footingFrontM, d.footingBottomEl, d.footingTopEl, `fill="${FOOT}" stroke="${MUTED}"`),
|
||||
rect(d.wallBackM, d.wallFrontM, d.footingTopEl, seg.seatEl, `fill="${CONC}" stroke="${MUTED}"`),
|
||||
rect(d.wallBackM, d.parapetBackM, seg.seatEl, bracket, `fill="${CONC_DARK}" stroke="${MUTED}"`),
|
||||
rect(d.parapetBackM, 0, seg.seatEl, top, `fill="${CONC_DARK}" stroke="${MUTED}"`),
|
||||
rect(blockT - halfBlock, blockT + halfBlock, seg.seatEl, b1.z, `fill="#9aa7b5" stroke="${INK}"`),
|
||||
`<line x1="${X(0).toFixed(1)}" y1="4" x2="${X(0).toFixed(1)}" y2="${(H - 30).toFixed(1)}" stroke="#c0392b" stroke-dasharray="6 3"/>`,
|
||||
label(X(0), 14, '교대축', 'middle', '#c0392b'),
|
||||
label((X(d.wallBackM) + X(d.wallFrontM)) / 2, Y(top) - 18, `${d.bracketWidthMm} + ${d.parapetThicknessMm} + ${d.seatWidthMm} = ${d.bracketWidthMm + d.parapetThicknessMm + d.seatWidthMm}`),
|
||||
label(X(0) + 6, Y(top) - 4, `흉벽 상단 ${t3(top)}`, 'start'),
|
||||
label(X(d.wallBackM) - 6, Y(bracket) + 4, `브라켓 상면 ${t3(bracket)}`, 'end'),
|
||||
label(X(d.wallFrontM) + 6, Y(seg.seatEl) + 4, `교좌면 ${t3(seg.seatEl)}`, 'start'),
|
||||
label(X(d.wallFrontM) + 6, Y((seg.seatEl + d.footingTopEl) / 2) + 4, `벽체 ${(seg.seatEl - d.footingTopEl).toFixed(3)}`, 'start'),
|
||||
label(X(d.footingFrontM) + 6, Y(d.footingTopEl) + 4, `상면 ${t3(d.footingTopEl)}`, 'start'),
|
||||
label(X(d.footingFrontM) + 6, Y(d.footingBottomEl) + 4, `기초 하단 ${t3(d.footingBottomEl)}`, 'start'),
|
||||
label(X(d.footingBackM), H - 8, '← 배면', 'start', MUTED),
|
||||
label(X(d.footingFrontM), H - 8, '전면 →', 'end', MUTED),
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 정면·배면도: 교좌면 계단(구간별 교좌면 EL), 흉벽 상단 경사, 받침 블럭, 수축줄눈, 저판. 왼쪽 IP3(① 쪽), 오른쪽 IP1.
|
||||
*/
|
||||
export function abutWallElevationSvg(d: AbutWallDrawing): string {
|
||||
const k = 46;
|
||||
const top = Math.max(d.parapetTopStartEl, d.parapetTopEndEl);
|
||||
const elMax = top + 0.6;
|
||||
const pad = 40;
|
||||
const X = (s: number) => pad + (d.lengthM - s) * k; // IP3 (s = L) on the left
|
||||
const Y = (el: number) => (elMax - el) * k + 10;
|
||||
const W = d.lengthM * k + 2 * pad;
|
||||
const H = Y(d.footingBottomEl) + 40;
|
||||
const parts: string[] = [];
|
||||
parts.push(poly([[X(d.lengthM), Y(d.footingTopEl)], [X(0), Y(d.footingTopEl)], [X(0), Y(d.footingBottomEl)], [X(d.lengthM), Y(d.footingBottomEl)]], `fill="${FOOT}" stroke="${MUTED}"`));
|
||||
// 흉벽 (behind) from the 교좌면 steps up to the sloped top line.
|
||||
parts.push(poly([[X(d.lengthM), Y(d.footingTopEl)], [X(0), Y(d.footingTopEl)], [X(0), Y(d.parapetTopStartEl)], [X(d.lengthM), Y(d.parapetTopEndEl)]], `fill="${CONC_DARK}" stroke="${MUTED}"`));
|
||||
// 벽체 front: one block per 교좌면 step.
|
||||
for (const g of d.segments) {
|
||||
parts.push(poly([[X(g.sTo), Y(d.footingTopEl)], [X(g.sFrom), Y(d.footingTopEl)], [X(g.sFrom), Y(g.seatEl)], [X(g.sTo), Y(g.seatEl)]], `fill="${CONC}" stroke="${MUTED}"`));
|
||||
}
|
||||
for (const b of d.bearings) {
|
||||
parts.push(poly([[X(b.s + 0.6), Y(b.seatEl)], [X(b.s - 0.6), Y(b.seatEl)], [X(b.s - 0.6), Y(b.z)], [X(b.s + 0.6), Y(b.z)]], `fill="#9aa7b5" stroke="${INK}"`));
|
||||
parts.push(label(X(b.s), Y(b.z) - 6, ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧'][b.no - 1] ?? String(b.no)));
|
||||
}
|
||||
for (let j = 1; j <= d.shrinkJointCount; j++) {
|
||||
const s = (d.lengthM * j) / (d.shrinkJointCount + 1);
|
||||
parts.push(`<line x1="${X(s).toFixed(1)}" y1="${Y(parapetTopAt(d, s)).toFixed(1)}" x2="${X(s).toFixed(1)}" y2="${Y(d.footingTopEl).toFixed(1)}" stroke="${INK}" stroke-dasharray="5 3"/>`);
|
||||
parts.push(label(X(s) + 4, Y(d.footingTopEl) - 8, `수축줄눈 ${j}`, 'start'));
|
||||
}
|
||||
const first = d.segments.find((g) => d.lengthM >= g.sFrom && d.lengthM <= g.sTo) ?? d.segments[d.segments.length - 1];
|
||||
const last = d.segments.find((g) => g.sFrom <= 0 && g.sTo >= 0) ?? d.segments[0];
|
||||
parts.push(label(X(d.lengthM), Y(d.parapetTopEndEl) - 6, t3(d.parapetTopEndEl), 'start'));
|
||||
parts.push(label(X(0), Y(d.parapetTopStartEl) - 6, t3(d.parapetTopStartEl), 'end'));
|
||||
parts.push(label(X(d.lengthM) - 4, Y(first.seatEl) + 4, t3(first.seatEl), 'end'));
|
||||
parts.push(label(X(0) + 4, Y(last.seatEl) + 4, t3(last.seatEl), 'start'));
|
||||
parts.push(label(X(d.lengthM) - 4, Y(d.footingBottomEl) + 4, t3(d.footingBottomEl), 'end'));
|
||||
parts.push(label(X(d.lengthM), H - 8, 'IP3', 'middle', MUTED));
|
||||
parts.push(label(X(0), H - 8, 'IP1', 'middle', MUTED));
|
||||
parts.push(label((X(0) + X(d.lengthM)) / 2, H - 8, `${d.lengthM.toFixed(3)} m`));
|
||||
return [
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="정면·배면도: 흉벽 상단 ${t3(d.parapetTopEndEl)} (IP3) → ${t3(d.parapetTopStartEl)} (IP1), 교좌면 계단 ${d.segments.length}구간, 수축줄눈 ${d.shrinkJointCount}">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
@@ -7,9 +7,9 @@ const base = {
|
||||
};
|
||||
|
||||
describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => {
|
||||
it('has the five rows of the mockup, in order', () => {
|
||||
it('has the six rows of the mockup, in order', () => {
|
||||
const rows = abutMemberRows(base, '11.253 m');
|
||||
expect(rows.map((r) => r.id)).toEqual(['bearing', 'shape', 'wing', 'slab', 'ground']);
|
||||
expect(rows.map((r) => r.id)).toEqual(['bearing', 'shape', 'footing', 'wing', 'slab', 'ground']);
|
||||
expect(rows[0].label).toBe('교량받침·받침블럭·솔플레이트');
|
||||
});
|
||||
|
||||
@@ -20,7 +20,11 @@ describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => {
|
||||
it('marks the shape when a value differs from the default', () => {
|
||||
const rows = abutMemberRows({ ...base, shape: { parapetHeightMm: 2200 } }, '—');
|
||||
expect(rows.find((r) => r.id === 'shape')?.changed).toBe(true);
|
||||
expect(rows.find((r) => r.id === 'shape')?.summary).toBe('벽체 두께 2400 · 흉벽 2200');
|
||||
expect(rows.find((r) => r.id === 'shape')?.summary).toBe('벽체 두께 2400 (자동) · 흉벽 2200');
|
||||
expect(rows.find((r) => r.id === 'footing')?.changed).toBe(false);
|
||||
const foot = abutMemberRows({ ...base, shape: { footingThicknessMm: 1300 } }, '—');
|
||||
expect(foot.find((r) => r.id === 'footing')?.changed).toBe(true);
|
||||
expect(foot.find((r) => r.id === 'shape')?.changed).toBe(false);
|
||||
});
|
||||
|
||||
it('marks the wing when a side is not the general form, and names both sides', () => {
|
||||
@@ -41,7 +45,7 @@ describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => {
|
||||
expect(rows.find((r) => r.id === 'ground')?.summary).toBe('지반고 33.984 m');
|
||||
});
|
||||
|
||||
it('only the bearing, shape and wing cards take input in this step', () => {
|
||||
expect(ABUT_MEMBER_EDITABLE).toEqual({ bearing: true, shape: true, wing: true, slab: false, ground: false });
|
||||
it('only the bearing, shape, footing and wing cards take input in this step', () => {
|
||||
expect(ABUT_MEMBER_EDITABLE).toEqual({ bearing: true, shape: true, footing: true, wing: true, slab: false, ground: false });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ABUT_SHAPE_DEFAULT, resolveShape, shapeIssues } from '../src/core/abut/shape';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, 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)', () => {
|
||||
@@ -11,17 +11,21 @@ describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말
|
||||
expect(r.footingBackM).toBeCloseTo(-1.8, 9);
|
||||
expect(r.pileEdgeM).toBeCloseTo(0.65, 9);
|
||||
expect(shapeIssues({})).toEqual([]);
|
||||
expect(ABUT_SHAPE_DEFAULT.wallThicknessMm).toBe(2400);
|
||||
expect(ABUT_SHAPE_DEFAULT.bracketWidthMm).toBe(300);
|
||||
expect(wallThicknessMm(ABUT_SHAPE_DEFAULT)).toBe(2400); // 300 + 500 + 1600 (자동)
|
||||
});
|
||||
|
||||
it('a changed wall thickness moves the back face (front − thickness)', () => {
|
||||
expect(resolveShape({ wallThicknessMm: 2000 }).wallBackM).toBeCloseTo(-0.4, 9);
|
||||
it('a changed 브라켓 폭 moves the back face; an old saved 벽체 두께 still opens as the same wall', () => {
|
||||
expect(resolveShape({ bracketWidthMm: 0 }).wallBackM).toBeCloseTo(-0.5, 9);
|
||||
expect(normalizeShape({ wallThicknessMm: 2000 } as Partial<AbutShape>)).toEqual({ bracketWidthMm: -100 });
|
||||
expect(resolveShape({ wallThicknessMm: 2000 } as Partial<AbutShape>).wallBackM).toBeCloseTo(-0.4, 9);
|
||||
});
|
||||
|
||||
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({ wallThicknessMm: 0 }).join(' ')).toContain('벽체 두께');
|
||||
expect(shapeIssues({ bracketWidthMm: -1 }).join(' ')).toContain('브라켓 폭');
|
||||
expect(shapeIssues({ seatStepIgnoreMm: -1 }).join(' ')).toContain('인접 무시 기준');
|
||||
expect(shapeIssues({ pileSpacingMm: 0 }).join(' ')).toContain('말뚝');
|
||||
});
|
||||
});
|
||||
@@ -40,7 +44,8 @@ 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(13);
|
||||
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']);
|
||||
});
|
||||
|
||||
it('the new drawing values do not change the geometry (the model ignores them)', () => {
|
||||
@@ -71,4 +76,11 @@ describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
|
||||
const wider = buildMembers(model, {}, { pileSpacingMm: 3000 });
|
||||
expect(wider.piles[0].s.length).toBeLessThan(6);
|
||||
});
|
||||
|
||||
it('인접 무시 기준: steps smaller than the value join one segment (A1 steps are 71 mm)', () => {
|
||||
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 foundation = { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm };
|
||||
expect(buildMembers(model, foundation).wallSegments).toHaveLength(5);
|
||||
expect(buildMembers(model, foundation, { seatStepIgnoreMm: 100 }).wallSegments).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as XLSX from 'xlsx';
|
||||
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 { abutWallElevationSvg, abutWallSectionSvg, type AbutWallDrawing } from '../src/view/abut-wall-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 m = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm });
|
||||
const d: AbutWallDrawing = {
|
||||
lengthM: model.length,
|
||||
segments: m.wallSegments,
|
||||
bearings: model.bearings,
|
||||
footingTopEl: m.footingTopEl,
|
||||
footingBottomEl: m.footingBottomEl!,
|
||||
parapetTopStartEl: m.parapetTopStartEl,
|
||||
parapetTopEndEl: m.parapetTopEndEl,
|
||||
wallFrontM: m.shape.wallFrontM,
|
||||
parapetBackM: m.shape.parapetBackM,
|
||||
wallBackM: m.shape.wallBackM,
|
||||
footingFrontM: m.shape.footingFrontM,
|
||||
footingBackM: m.shape.footingBackM,
|
||||
bracketHeightM: m.shape.bracketHeightM,
|
||||
shrinkJointCount: 1,
|
||||
bracketWidthMm: 300,
|
||||
parapetThicknessMm: 500,
|
||||
seatWidthMm: 1600,
|
||||
};
|
||||
|
||||
describe('교대 벽체·흉벽 삽도 (시안 ② 벽체·흉벽 카드)', () => {
|
||||
it('단면 (① 구간): the width line and the levels of the generated model', () => {
|
||||
const svg = abutWallSectionSvg(d);
|
||||
for (const s of ['300 + 500 + 1600 = 2400', '교좌면 44.367', '벽체 2.372', '상면 41.995', '기초 하단 40.895', '교대축', '← 배면', '전면 →']) {
|
||||
expect(svg).toContain(s);
|
||||
}
|
||||
expect(svg).toMatch(/흉벽 상단 4[67]\.\d{3}/);
|
||||
expect(svg).toMatch(/브라켓 상면 4[56]\.\d{3}/);
|
||||
});
|
||||
|
||||
it('정면·배면도: five steps, five bearings, the sloped 흉벽 top (IP3 46.835 → IP1 47.180) and the 수축줄눈', () => {
|
||||
const svg = abutWallElevationSvg(d);
|
||||
for (const s of ['IP3', 'IP1', `${model.length.toFixed(3)} m`, '수축줄눈 1', '46.835', '47.180', '①', '⑤']) expect(svg).toContain(s);
|
||||
expect(svg).toContain('교좌면 계단 5구간');
|
||||
});
|
||||
|
||||
it('the 수축줄눈 count follows the input', () => {
|
||||
const svg = abutWallElevationSvg({ ...d, shrinkJointCount: 2 });
|
||||
expect(svg).toContain('수축줄눈 2');
|
||||
expect(svg.match(/stroke-dasharray="5 3"/g)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user