feat(abut): 접속슬래브 경사는 형식이 정함, 단면 · 접합 상세를 파단선 단면 하나로 합침, 흉벽 높이 입력
- 경사 선택 삭제: 매립형 = 하향 4 %, 노출형 = 종단경사 따라 (노트 표기). 예전 파일의 slope 값은 읽고 무시 - 단면 + 접합 상세: 흉벽 쪽과 먼 끝만 그리고 중간 생략, 부속재 · 치수(7000 · 450 · 850 · 295 · 헌치) · EL을 한 그림에 - 흉벽 높이 (브라켓 상면 ~ 흉벽 상단) 입력 = 벽체·흉벽 카드 브라켓 높이와 같은 값 - 자동 값: 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (295) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b8b80ae97
commit
9835fb42b3
7 files changed
+157
-199
No files matched your search
+5
-12
@@ -6,7 +6,7 @@ import type { AbutRecord } from './parse';
|
||||
import type { WingForm } from './geometry';
|
||||
import { WING_DIMS_DEFAULT, wingDimsIssues, type WingDims } from './wings';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, shapeIssues, type AbutShape } from './shape';
|
||||
import { resolveSlab, slabDefaultSlope, slabIssues, type AbutSlab, type SlabForm, type SlabSlope } from './slab';
|
||||
import { resolveSlab, slabIssues, type AbutSlab, type SlabForm } from './slab';
|
||||
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
||||
|
||||
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
|
||||
@@ -131,22 +131,15 @@ function slabFitIssues(slab: Partial<AbutSlab> | undefined, shape: Partial<AbutS
|
||||
return s.thicknessMm > h ? [`브라켓 높이(${h})는 접속슬래브 두께(${s.thicknessMm}) 이상이어야 합니다.`] : [];
|
||||
}
|
||||
|
||||
/** 접속슬래브 형식 (2026-11-04): picking a form also sets its slope (매립형 하향 4 %, 노출형 종단경사). */
|
||||
/** 접속슬래브 형식 (2026-11-04); the form also sets the slope (매립형 하향 4 %, 노출형 종단경사). */
|
||||
export function setSlabForm(rec: AbutRecord, form: SlabForm): AbutRecord {
|
||||
const slab: Partial<AbutSlab> = { ...(rec.slab ?? {}), form };
|
||||
delete slab.slope;
|
||||
const { slope: _old, ...kept } = (rec.slab ?? {}) as Partial<AbutSlab> & { slope?: unknown };
|
||||
const slab: Partial<AbutSlab> = { ...kept, form };
|
||||
const issues = slabIssues(resolveSlab(slab), bracketHeightMm(rec.shape));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, slab };
|
||||
}
|
||||
|
||||
/** 접속슬래브 경사; the form's own slope is stored as "not set". */
|
||||
export function setSlabSlope(rec: AbutRecord, slope: SlabSlope): AbutRecord {
|
||||
const slab: Partial<AbutSlab> = { ...(rec.slab ?? {}), slope };
|
||||
if (slope === slabDefaultSlope(resolveSlab(rec.slab).form)) delete slab.slope;
|
||||
return { ...rec, slab };
|
||||
}
|
||||
|
||||
/** 접속슬래브 입력칸 (② 접속슬래브 카드, 시안 순서). 헌치 길이는 노출형만. */
|
||||
export const SLAB_EDIT_FIELDS: { key: 'lengthMm' | 'thicknessMm' | 'parapetGapMm' | 'wingGapMm' | 'haunchLengthMm'; label: string; exposedOnly?: boolean; note?: string }[] = [
|
||||
{ key: 'lengthMm', label: '길이 (mm)' },
|
||||
@@ -172,7 +165,7 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'w
|
||||
{ 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: 'bracketHeightMm', label: '브라켓 높이 (mm)', group: 'wall', note: '= 접속슬래브 카드 흉벽 높이' },
|
||||
{ key: 'bracketWidthMm', label: '브라켓 폭 (mm)', group: 'wall' },
|
||||
{ key: 'shrinkJointCount', label: '수축줄눈 개수', group: 'wall' },
|
||||
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||||
|
||||
+8
-16
@@ -4,17 +4,15 @@
|
||||
* - 매립형 (type-1, 기본): 하면 = 브라켓 상면, 두께 일정. 브라켓 위(벽체 배면까지)는 수평이고 그 뒤로 경사(기본 하향 4 %).
|
||||
* - 노출형 (type-2, 2026-11-04 삽도 기준, QA-8): 상면 = 흉벽 상단에서 경사(기본 종단경사)를 따른다. 끝은 브라켓 상면에
|
||||
* 얹히고(끝 두께 = 브라켓 높이), 벽체 배면부터 헌치 길이 동안 두께가 줄어 그 뒤로는 두께가 일정하다.
|
||||
* - 경사: 형식을 고르면 그 형식의 기본값(매립형 하향 4 %, 노출형 종단경사)으로 바뀌고, 따로 바꿀 수 있다.
|
||||
* - 경사는 형식이 정한다 (2026-11-04): 매립형 = 하향 4 %, 노출형 = 종단경사 따라 (엑셀 A555 "type-1 = 하향경사 4 %, type-2 = 종단경사").
|
||||
* - t: 교대축 직각 (전면 +, 배면 −, m). 슬래브는 흉벽 배면에서 "흉벽과 간격"만큼 띄워 시작하고 길이만큼 배면 쪽으로 간다.
|
||||
* - 단면의 EL은 그 s의 흉벽 상단 EL에서 잰 값이다 (흉벽 상단 경사는 호출하는 쪽에서 더한다).
|
||||
*/
|
||||
|
||||
export type SlabForm = 'buried' | 'exposed';
|
||||
export type SlabSlope = 'down4' | 'grade';
|
||||
|
||||
export interface AbutSlab {
|
||||
form: SlabForm;
|
||||
slope: SlabSlope;
|
||||
lengthMm: number;
|
||||
thicknessMm: number;
|
||||
parapetGapMm: number;
|
||||
@@ -25,7 +23,6 @@ export interface AbutSlab {
|
||||
|
||||
export const ABUT_SLAB_DEFAULT: AbutSlab = {
|
||||
form: 'buried',
|
||||
slope: 'down4',
|
||||
lengthMm: 7000,
|
||||
thicknessMm: 450,
|
||||
parapetGapMm: 5,
|
||||
@@ -38,31 +35,26 @@ export const SLAB_FALL_PCT = 4;
|
||||
|
||||
export const SLAB_FORM_LABEL: Record<SlabForm, string> = { buried: '매립형', exposed: '노출형' };
|
||||
|
||||
/** The slope a form starts with: 매립형 하향 4 %, 노출형 종단경사 (엑셀 A555 "type-1 = 하향경사 4 %, type-2 = 종단경사"). */
|
||||
export function slabDefaultSlope(form: SlabForm): SlabSlope {
|
||||
return form === 'exposed' ? 'grade' : 'down4';
|
||||
}
|
||||
|
||||
/** The user's values over the defaults; the slope follows the form unless it was set. */
|
||||
/** The user's values over the defaults. A `slope` stored by an earlier version (2026-11-04 오전) is dropped: the form sets it. */
|
||||
export function resolveSlab(changes?: Partial<AbutSlab>): AbutSlab {
|
||||
const form = changes?.form ?? ABUT_SLAB_DEFAULT.form;
|
||||
return { ...ABUT_SLAB_DEFAULT, slope: slabDefaultSlope(form), ...(changes ?? {}) };
|
||||
const { slope: _old, ...rest } = (changes ?? {}) as Partial<AbutSlab> & { slope?: unknown };
|
||||
return { ...ABUT_SLAB_DEFAULT, ...rest };
|
||||
}
|
||||
|
||||
/** Keys whose value differs from the default (형식, 경사 and the dims), for the change marks. */
|
||||
/** Keys whose value differs from the default (형식 and the dims), for the change marks. */
|
||||
export function slabChangedKeys(changes?: Partial<AbutSlab>): (keyof AbutSlab)[] {
|
||||
const s = resolveSlab(changes);
|
||||
return (Object.keys(ABUT_SLAB_DEFAULT) as (keyof AbutSlab)[]).filter((k) => s[k] !== ABUT_SLAB_DEFAULT[k]);
|
||||
}
|
||||
|
||||
/** Slope (%), + = rises away from the wall: 하향 4 % → −4, 종단경사 → the wing slope (A1 = −i, A2 = +i). */
|
||||
/** Slope (%), + = rises away from the wall: 매립형 하향 4 % → −4, 노출형 종단경사 → the wing slope (A1 = −i, A2 = +i). */
|
||||
export function slabGradePct(slab: AbutSlab, wingSlopePct: number): number {
|
||||
return slab.slope === 'down4' ? -SLAB_FALL_PCT : wingSlopePct;
|
||||
return slab.form === 'buried' ? -SLAB_FALL_PCT : wingSlopePct;
|
||||
}
|
||||
|
||||
/** Slope text as on the card: "하향 4 %" or "종단경사 +1.91 %". */
|
||||
export function slabSlopeLabel(slab: AbutSlab, wingSlopePct?: number): string {
|
||||
if (slab.slope === 'down4') return `하향 ${SLAB_FALL_PCT} %`;
|
||||
if (slab.form === 'buried') return `하향 ${SLAB_FALL_PCT} %`;
|
||||
if (wingSlopePct === undefined) return '종단경사';
|
||||
return `종단경사 ${wingSlopePct >= 0 ? '+' : ''}${wingSlopePct.toFixed(2)} %`;
|
||||
}
|
||||
|
||||
+3
-1
@@ -144,7 +144,9 @@ export function checkStoredAbutments(list: unknown): void {
|
||||
}
|
||||
if (a.slab !== undefined) {
|
||||
for (const [key, v] of Object.entries(a.slab)) {
|
||||
const ok = key === 'form' ? v === 'buried' || v === 'exposed' : key === 'slope' ? v === 'down4' || v === 'grade' : finite(v);
|
||||
// 'slope' was stored by an earlier version (2026-11-04 오전) and is ignored now: the form sets the slope.
|
||||
const x: unknown = v;
|
||||
const ok = key === 'form' ? x === 'buried' || x === 'exposed' : key === 'slope' ? x === 'down4' || x === 'grade' : finite(x);
|
||||
if (!ok) throw new Error(`${who}: 접속슬래브 값 ${key}이(가) 맞지 않습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-50
@@ -24,11 +24,10 @@ import {
|
||||
setWingForm,
|
||||
applySlabEdit,
|
||||
setSlabForm,
|
||||
setSlabSlope,
|
||||
SLAB_EDIT_FIELDS,
|
||||
} from './core/abut/edit';
|
||||
import { ABUT_SLAB_DEFAULT, resolveSlab, SLAB_FORM_LABEL, slabDefaultSlope, slabGradePct, slabSection, slabSlopeLabel, type SlabForm, type SlabSlope } from './core/abut/slab';
|
||||
import { abutSlabJointSvg, abutSlabPlanSvg, abutSlabSectionSvg } from './view/abut-slab-svg';
|
||||
import { ABUT_SLAB_DEFAULT, resolveSlab, SLAB_FORM_LABEL, slabGradePct, slabSection, slabSlopeLabel, type SlabForm } from './core/abut/slab';
|
||||
import { abutSlabPlanSvg, abutSlabSectionSvg } from './view/abut-slab-svg';
|
||||
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';
|
||||
@@ -1190,21 +1189,17 @@ function fillAbutWingCard(
|
||||
|
||||
/**
|
||||
* 접속슬래브 타일 그림 (2026-11-04, 48 × 24, 사용자 삽도 기준): 오른쪽이 흉벽 · 브라켓 단차, 왼쪽으로 슬래브가 뻗는다.
|
||||
* 매립형 = 흉벽 상단 아래 일정 두께, 노출형 = 상면이 흉벽 상단과 같고 끝에 헌치. 경사는 하향(왼쪽이 낮음) / 종단경사(높음).
|
||||
* 매립형 = 흉벽 상단 아래 일정 두께, 하향 4 % (왼쪽이 낮음). 노출형 = 상면이 흉벽 상단과 같고 끝에 헌치, 종단경사 (왼쪽이 높음).
|
||||
*/
|
||||
const SLAB_ICON = {
|
||||
section(form: SlabForm, slope: SlabSlope): string {
|
||||
section(form: SlabForm): string {
|
||||
const wall = '<path d="M31 22V12h3V2h4v20z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>';
|
||||
const fall = slope === 'down4';
|
||||
const slab =
|
||||
form === 'buried'
|
||||
? fall ? 'M4 11L30 8v4L4 15z' : 'M4 6L30 8v4L4 10z'
|
||||
: fall ? 'M4 4.5L30 2v10L23 6.5L4 8.5z' : 'M4 0.8L30 2v10L23 5.5L4 4.8z';
|
||||
const slab = form === 'buried' ? 'M4 11L30 8v4L4 15z' : 'M4 0.8L30 2v10L23 5.5L4 4.8z';
|
||||
return `${wall}<path d="${slab}" fill="currentColor" fill-opacity="0.18" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>`;
|
||||
},
|
||||
};
|
||||
|
||||
/** ② 접속슬래브 카드 (시안, 2026-11-04): 형식 · 경사 타일 → 삽도 3개 → 입력 → 자동 값 → 메모. */
|
||||
/** ② 접속슬래브 카드 (시안, 2026-11-04): 형식 타일 (경사는 노트) → 평면 · 단면(접합 상세 합침) → 입력 → 자동 값 → 메모. */
|
||||
function fillAbutSlabCard(
|
||||
box: HTMLDivElement,
|
||||
rec: AbutRecord,
|
||||
@@ -1215,13 +1210,24 @@ function fillAbutSlabCard(
|
||||
): void {
|
||||
const slab = resolveSlab(rec.slab);
|
||||
const say = (err: unknown) => ($<HTMLElement>('abut-panel-plan-msg').textContent = (err as Error).message);
|
||||
const tiles = el('div', undefined, 'abut-tile-row');
|
||||
tiles.append(
|
||||
let model: AbutModel | null = null;
|
||||
let members: AbutMembers | null = null;
|
||||
let wings: WingSolid[] = [];
|
||||
try {
|
||||
model = buildAbut(toAbutInput(rec));
|
||||
members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
wings = buildWings(model, members, rec.wings, rec.wingDims);
|
||||
} catch {
|
||||
// shown below the tiles
|
||||
}
|
||||
// 형식 (경사는 형식이 정한다, 2026-11-04): 매립형 = 하향 4 %, 노출형 = 종단경사 따라.
|
||||
const grade = slabSlopeLabel(resolveSlab({ form: 'exposed' }), model?.wing?.slopePct);
|
||||
box.append(
|
||||
optionTiles(
|
||||
'형식',
|
||||
(['buried', 'exposed'] as const).map((f) => ({
|
||||
label: SLAB_FORM_LABEL[f],
|
||||
icon: SLAB_ICON.section(f, slabDefaultSlope(f)),
|
||||
icon: SLAB_ICON.section(f),
|
||||
on: slab.form === f,
|
||||
isDefault: f === 'buried',
|
||||
pick: () => {
|
||||
@@ -1233,29 +1239,10 @@ function fillAbutSlabCard(
|
||||
}
|
||||
},
|
||||
})),
|
||||
),
|
||||
optionTiles(
|
||||
'경사',
|
||||
(['down4', 'grade'] as const).map((s) => ({
|
||||
label: s === 'down4' ? '하향 4 %' : '종단경사 따라',
|
||||
icon: SLAB_ICON.section(slab.form, s),
|
||||
on: slab.slope === s,
|
||||
isDefault: s === slabDefaultSlope(slab.form),
|
||||
pick: () => {
|
||||
if (slab.slope !== s) apply(setSlabSlope(rec, s));
|
||||
},
|
||||
})),
|
||||
`경사 · 매립형 = 하향 4 %, 노출형 = ${grade.replace('종단경사', '종단경사 따라')}`,
|
||||
),
|
||||
);
|
||||
box.append(tiles);
|
||||
let model: AbutModel;
|
||||
let members: AbutMembers;
|
||||
let wings: WingSolid[];
|
||||
try {
|
||||
model = buildAbut(toAbutInput(rec));
|
||||
members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
wings = buildWings(model, members, rec.wings, rec.wingDims);
|
||||
} catch {
|
||||
if (!model || !members) {
|
||||
box.append(el('p', '교대 모델을 만들 수 없어 접속슬래브를 그리지 않습니다.', 'muted small'));
|
||||
return;
|
||||
}
|
||||
@@ -1269,7 +1256,7 @@ function fillAbutSlabCard(
|
||||
// 단면은 교대축 가운데의 흉벽 상단에서 잰다.
|
||||
const parapetTopMid = (members.parapetTopStartEl + members.parapetTopEndEl) / 2;
|
||||
const sh = members.shape;
|
||||
const figs = el('div', undefined, 'abut-wall-figs abut-wing-figs');
|
||||
const figs = el('div', undefined, 'abut-wall-figs abut-slab-figs');
|
||||
const addFig = (svg: string, caption: string) => {
|
||||
const fig = el('figure', undefined, 'abut-fig');
|
||||
fig.innerHTML = svg;
|
||||
@@ -1300,20 +1287,26 @@ function fillAbutSlabCard(
|
||||
parapetGapMm: slab.parapetGapMm,
|
||||
haunchLengthMm: slab.haunchLengthMm,
|
||||
}),
|
||||
`단면 · 교대축 직각 (교대축 가운데) · ${SLAB_FORM_LABEL[slab.form]} · 경사 ${slopeText} · 축척 같음`,
|
||||
);
|
||||
addFig(
|
||||
abutSlabJointSvg({
|
||||
form: slab.form,
|
||||
gradePct,
|
||||
parapetTopEl: parapetTopMid,
|
||||
bracketHeightM: sh.bracketHeightM,
|
||||
parapetGapMm: slab.parapetGapMm,
|
||||
thicknessMm: slab.thicknessMm,
|
||||
}),
|
||||
'흉벽–접속슬래브 접합 상세 · 모식도 (부속재는 표시용)',
|
||||
`단면 · 접합 상세 · 교대축 직각 (교대축 가운데) · ${SLAB_FORM_LABEL[slab.form]} · 경사 ${slopeText} · 축척 같음, 중간 생략 · 간격 · 고무판은 키워 그림, 부속재는 표시용`,
|
||||
);
|
||||
box.append(figs);
|
||||
// 흉벽 높이 = 브라켓 상면 ~ 흉벽 상단 (2026-11-04): 벽체·흉벽 카드의 "브라켓 높이"와 같은 값.
|
||||
{
|
||||
const id = idOf('slab-parapet');
|
||||
const lab = el('label', '흉벽 높이 · 브라켓 상면 ~ 흉벽 상단 (mm)');
|
||||
lab.htmlFor = id;
|
||||
const now = Math.round(sh.bracketHeightM * 1000);
|
||||
const input = numberInput(id, String(now));
|
||||
if (now !== ABUT_SHAPE_DEFAULT.bracketHeightMm) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => {
|
||||
try {
|
||||
apply(applyShapeEdit(rec, 'bracketHeightMm', Number(input.value)));
|
||||
} catch (err) {
|
||||
fail(err, input, String(now));
|
||||
}
|
||||
});
|
||||
box.append(lab, input, el('span', `기본 ${ABUT_SHAPE_DEFAULT.bracketHeightMm} · = 벽체·흉벽 카드 브라켓 높이`, 'muted small'));
|
||||
}
|
||||
for (const f of SLAB_EDIT_FIELDS) {
|
||||
const id = idOf(`slab-${f.key}`);
|
||||
const lab = el('label', f.label);
|
||||
@@ -1335,12 +1328,13 @@ function fillAbutSlabCard(
|
||||
const side = (mm: number, w: WingSolid | undefined) => (w ? (mm > 0 ? `${mm} × ${mm}` : '없음') : '날개벽 없음');
|
||||
auto('모따기 (자동)', `좌 ${side(chamfer.left, leftWing)} · 우 ${side(chamfer.right, rightWing)}`, '= 그 측 날개벽 헌치');
|
||||
auto('폭 (자동)', `${widthM.toFixed(3)} m`, '도면 11.670 · QA-6');
|
||||
auto('경사', slopeText, slab.slope === slabDefaultSlope(slab.form) ? `${SLAB_FORM_LABEL[slab.form]} 기본` : `변경 (${SLAB_FORM_LABEL[slab.form]} 기본 = ${slabDefaultSlope(slab.form) === 'down4' ? '하향 4 %' : '종단경사'})`);
|
||||
const bearing = Math.round((sh.parapetBackM - sh.wallBackM) * 1000) - slab.parapetGapMm;
|
||||
auto('브라켓 얹힘 길이 (자동)', `${bearing} mm`, '= 브라켓 폭 − 흉벽과 간격');
|
||||
if (slab.form === 'exposed') auto('끝 두께 (자동)', `${Math.round(sh.bracketHeightM * 1000)} mm`, '= 흉벽 상단 − 브라켓 상면');
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
'시안 메모 · 매립형은 브라켓 상면에 얹혀 두께가 일정하고, 벽체 배면 뒤에서 경사를 따릅니다. 노출형(2026-11-04 삽도 기준, QA-8 가정)은 상면이 흉벽 상단과 같고 끝은 브라켓 위에 얹히며, 벽체 배면부터 헌치 길이 동안 두께가 끝 두께에서 슬래브 두께로 줄어듭니다. 형식을 바꾸면 경사가 그 형식의 기본값으로 바뀝니다. 접합 부속재(고무판, 타르페이퍼, 다웰, 강관)는 표시용이고, 배근은 철근 단계에서 정합니다(노출형 배근 규칙 미정).',
|
||||
'시안 메모 · 매립형은 브라켓 상면에 얹혀 두께가 일정하고, 벽체 배면 뒤에서 경사를 따릅니다. 노출형(2026-11-04 삽도 기준, QA-8 가정)은 상면이 흉벽 상단과 같고 끝은 브라켓 위에 얹히며, 벽체 배면부터 헌치 길이 동안 두께가 끝 두께에서 슬래브 두께로 줄어듭니다. 경사는 형식이 정합니다(매립형 하향 4 %, 노출형 종단경사). 흉벽 높이는 벽체·흉벽 카드의 브라켓 높이와 같은 값입니다. 접합 부속재(고무판, 타르페이퍼, 다웰, 강관)는 표시용이고, 배근은 철근 단계에서 정합니다(노출형 배근 규칙 미정).',
|
||||
'muted small abut-memo',
|
||||
),
|
||||
);
|
||||
|
||||
+1
-2
@@ -948,5 +948,4 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
|
||||
#abut-panel-plan .abut-wing-forms .abut-grp.editing { border-color: #1f5fa8; background: #f8fbfe; }
|
||||
#abut-panel-plan .abut-wing-forms .abut-grp .abut-grp-title { cursor: pointer; font-weight: 600; color: #18212b; }
|
||||
#abut-panel-plan .abut-grp.compact .abut-opt { width: 64px; min-height: 48px; padding: 4px 2px; }
|
||||
#abut-panel-plan .abut-tile-row { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 28px; }
|
||||
#abut-panel-plan .abut-tile-row .abut-grp { margin: 0; }
|
||||
#abut-panel-plan-slab > p, #abut-panel-plan-slab > .abut-wall-figs { grid-column: 1 / -1; }
|
||||
+79
-95
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 교대 접속슬래브 삽도 (2026-11-04, 시안 ② 접속슬래브 카드): 평면(날개벽 사이), 단면(교대축 직각, 매립형 / 노출형), 흉벽–슬래브 접합 상세.
|
||||
* 순수 문자열 함수. 평면·단면은 생성본 값으로 그리고 가로·세로 축척이 같다. 접합 상세는 부속재를 보여 주는 모식도다(표시용).
|
||||
* 교대 접속슬래브 삽도 (2026-11-04, 시안 ② 접속슬래브 카드): 평면(날개벽 사이)과 단면 + 접합 상세(매립형 / 노출형, 중간 생략).
|
||||
* 순수 문자열 함수. 생성본 값으로 그리고 가로·세로 축척이 같다(단면은 파단선으로 중간을 생략). 접합 부속재는 표시용이다.
|
||||
*/
|
||||
import type { AbutPlanOutline } from './abut-scene';
|
||||
import type { SlabForm, SlabStation } from '../core/abut/slab';
|
||||
import { slabLevelAt, type SlabForm, type SlabStation } from '../core/abut/slab';
|
||||
import { dimLine } from './abut-wing-svg';
|
||||
|
||||
const INK = '#18212b';
|
||||
@@ -92,108 +92,92 @@ export interface AbutSlabSectionDrawing {
|
||||
haunchLengthMm: number;
|
||||
}
|
||||
|
||||
/** 단면: 날개벽 축 방향(교대축 직각), 왼쪽이 배면. 흉벽 · 브라켓 단차, 슬래브(매립형 일정 두께 / 노출형 헌치), 치수와 EL. */
|
||||
/**
|
||||
* 단면 + 접합 상세 (2026-11-04, 그림 합침): 교대축 직각, 왼쪽이 배면. 흉벽 쪽(헌치 끝 너머까지)과 먼 끝 0.8 m만 같은 축척으로
|
||||
* 그리고 그 사이는 파단선으로 생략한다. 간격(5)과 고무판 두께는 보이도록 키워 그리고, 부속재(충진재 · 스티로폼 · 고무판 ·
|
||||
* 타르페이퍼 · 다웰 · 강관)는 표시용이다. 치수: 7000 (중간 생략), 450, 흉벽 높이(브라켓 상면 ~ 흉벽 상단), 헌치, 브라켓 얹힘 길이.
|
||||
*/
|
||||
export function abutSlabSectionSvg(d: AbutSlabSectionDrawing): string {
|
||||
const k = 56;
|
||||
const k = 150; // px per m in both zones
|
||||
const st = d.section;
|
||||
const t0 = st[0].t;
|
||||
const t1 = st[st.length - 1].t;
|
||||
const low = -d.bracketHeightM - 0.9;
|
||||
const hi = Math.max(0, ...st.map((p) => p.top)) + 0.5;
|
||||
const minBottom = Math.min(...st.map((p) => p.bottom));
|
||||
const left = t1 - 1.0;
|
||||
const X = (t: number) => 20 + (t - left) * k;
|
||||
const Y = (y: number) => 12 + (hi - y) * k;
|
||||
const bH = d.bracketHeightM;
|
||||
const wb = d.wallBackM;
|
||||
const pb = d.parapetBackM;
|
||||
const he = wb - d.haunchLengthMm / 1000;
|
||||
const farLen = 0.8;
|
||||
const tz = Math.max(d.form === 'exposed' ? he - 0.5 : wb - 1.5, t1 + farLen); // left end of the near zone
|
||||
const breakW = 34;
|
||||
const xL = 70;
|
||||
const xNear0 = xL + farLen * k + breakW;
|
||||
const X = (t: number) => (t <= t1 + farLen + 1e-9 ? xL + (t - t1) * k : xNear0 + (t - tz) * k);
|
||||
const gapPx = 4; // 간격 drawn 4 px wide whatever its size
|
||||
const Xs = (t: number) => X(t) - (t >= t0 - 1e-9 ? gapPx - (d.parapetGapMm / 1000) * k : 0);
|
||||
const low = -bH - 0.75;
|
||||
const hi = Math.max(0, ...st.map((p) => p.top)) + 0.15;
|
||||
const Y = (y: number) => 26 + (hi - y) * k;
|
||||
const lv = (t: number) => slabLevelAt(st, t);
|
||||
// Slab pieces: far (t1 … t1 + 0.8) and near (tz … t0), each closed by a zigzag at the cut.
|
||||
const zig = (x: number, top: number, bottom: number, dir: 1 | -1): [number, number][] => {
|
||||
const h = bottom - top;
|
||||
return [[x, top - 4], [x, top + h * 0.3], [x + dir * 6, top + h * 0.45], [x - dir * 6, top + h * 0.6], [x, top + h * 0.72], [x, bottom + 4]];
|
||||
};
|
||||
const piece = (from: number, to: number, cutLeft: boolean) => {
|
||||
const ts = [from, ...st.map((p) => p.t).filter((t) => t > from + 1e-9 && t < to - 1e-9), to].sort((a, b) => a - b);
|
||||
const top = ts.map((t) => [Xs(t), Y(lv(t).top)] as [number, number]);
|
||||
const bottom = ts.map((t) => [Xs(t), Y(lv(t).bottom)] as [number, number]).reverse();
|
||||
const cutT = cutLeft ? from : to;
|
||||
const cut = zig(Xs(cutT), Y(lv(cutT).top), Y(lv(cutT).bottom), cutLeft ? 1 : -1);
|
||||
return cutLeft ? [...top, ...bottom, ...cut.reverse()] : [...top, ...cut, ...bottom];
|
||||
};
|
||||
const slabStyle = `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`;
|
||||
const parts = [
|
||||
// 흉벽 (t 흉벽 배면 … 0) and the 브라켓 단차 (벽체 배면 … 흉벽 배면), cut below.
|
||||
poly(
|
||||
[[X(d.wallBackM), Y(low)], [X(0), Y(low)], [X(0), Y(0)], [X(d.parapetBackM), Y(0)], [X(d.parapetBackM), Y(-bH)], [X(d.wallBackM), Y(-bH)]],
|
||||
`fill="${WALL}" stroke="${MUTED}"`,
|
||||
),
|
||||
poly([...st.map((p) => [X(p.t), Y(p.top)] as [number, number]), ...[...st].reverse().map((p) => [X(p.t), Y(p.bottom)] as [number, number])], `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`),
|
||||
];
|
||||
// 다웰 D25 at the middle of the 브라켓 width.
|
||||
const dowelT = (d.wallBackM + d.parapetBackM) / 2;
|
||||
parts.push(line(X(dowelT), Y(-bH - 0.35), X(dowelT), Y(-bH + 0.2), `stroke="${INK}" stroke-width="1.6"`));
|
||||
parts.push(text(X(d.wallBackM) - 6, Y(-bH - 0.45), '다웰 D25 @400', 'end', MUTED));
|
||||
// 치수: 길이 (흉벽 · 벽체 아래), 끝 두께 (왼쪽 끝), 노출형은 흉벽 쪽 끝 두께와 헌치 길이.
|
||||
const yDim = Math.max(Y(minBottom), Y(low)) + 18;
|
||||
parts.push(dimLine([X(t1), Y(st[st.length - 1].bottom)], [X(t0), Y(st[0].bottom)], yDim, 'h', `${d.lengthMm}`, true));
|
||||
const far = st[st.length - 1];
|
||||
parts.push(dimLine([X(t1), Y(far.top)], [X(t1), Y(far.bottom)], X(t1) - 10, 'v', `${d.thicknessMm}`));
|
||||
if (d.form === 'exposed') {
|
||||
const he = d.wallBackM - d.haunchLengthMm / 1000;
|
||||
const heP = st.find((p) => Math.abs(p.t - he) < 1e-6);
|
||||
if (heP) parts.push(dimLine([X(he), Y(heP.bottom)], [X(d.wallBackM), Y(-bH)], yDim + 22, 'h', `헌치 ${d.haunchLengthMm}`, true));
|
||||
parts.push(text(X(dowelT) - 6, Y((st[0].top + st[0].bottom) / 2) + 4, `끝 ${Math.round(bH * 1000)}`, 'end'));
|
||||
}
|
||||
// 경사 (상면 가운데 위), 간격, EL.
|
||||
const mid = st[Math.floor((st.length - 1) / 2)];
|
||||
parts.push(text((X(t1) + X(mid.t)) / 2, Y(Math.max(far.top, mid.top)) - 8, `경사 ${d.slopeLabel}`, 'middle'));
|
||||
parts.push(text(X(t0), Y(st[0].top) - 8, `간격 ${d.parapetGapMm}`, 'middle', MUTED));
|
||||
parts.push(text(X(0) + 6, Y(0) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||||
parts.push(text(X(0) + 6, Y(-bH) + 4, `브라켓 상면 ${el(d.parapetTopEl - bH)}`, 'start'));
|
||||
parts.push(text(X(0) + 6, Y(low) - 4, '흉벽 · 벽체', 'start', MUTED));
|
||||
const W = X(0) + 190;
|
||||
const H = yDim + (d.form === 'exposed' ? 44 : 22);
|
||||
return [
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="접속슬래브 단면 · ${d.form === 'exposed' ? '노출형' : '매립형'} · 경사 ${d.slopeLabel} · 길이 ${d.lengthMm} · 두께 ${d.thicknessMm}">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
export interface AbutSlabJointDrawing {
|
||||
form: SlabForm;
|
||||
/** 경사 (%, + = 배면 쪽으로 올라감). "S = 4.00 % (하향)"처럼 표시한다. */
|
||||
gradePct: number;
|
||||
parapetTopEl: number;
|
||||
bracketHeightM: number;
|
||||
parapetGapMm: number;
|
||||
thicknessMm: number;
|
||||
}
|
||||
|
||||
/** 흉벽–접속슬래브 접합 상세 (삽도 기준, 모식도): 간격 · 아스팔트 충진재 / 스티로폼, 고무판, 타르페이퍼, 다웰 · 강관. */
|
||||
export function abutSlabJointSvg(d: AbutSlabJointDrawing): string {
|
||||
// Fixed schematic (px): 흉벽 x 300–360 from y 20, 브라켓 상면 y 150 from x 260.
|
||||
const bracketY = 150;
|
||||
const padH = 4;
|
||||
const slabEnd = 296;
|
||||
const bottomY = bracketY - padH;
|
||||
const exposed = d.form === 'exposed';
|
||||
const topY = exposed ? 20 : 86;
|
||||
const drop = d.gradePct < 0 ? 10 : -6; // left end lower for a fall, higher for a rise
|
||||
const slab: [number, number][] = exposed
|
||||
? [[20, topY + drop + 60], [20, topY + drop], [slabEnd, topY], [slabEnd, bottomY], [150, topY + drop * 0.5 + 60]]
|
||||
: [[20, bottomY + drop], [20, topY + drop], [slabEnd, topY], [slabEnd, bottomY]];
|
||||
const gapMid = topY + (bottomY - topY) * 0.3;
|
||||
const parts = [
|
||||
poly([[260, 262], [360, 262], [360, 20], [300, 20], [300, bracketY], [260, bracketY]], `fill="${WALL}" stroke="${MUTED}"`),
|
||||
poly(slab, `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`),
|
||||
// 간격: 아스팔트 충진재 (위), 스티로폼 (아래).
|
||||
`<rect x="${slabEnd}" y="${topY}" width="4" height="${(gapMid - topY).toFixed(1)}" fill="#3b3f45"/>`,
|
||||
`<rect x="${slabEnd}" y="${gapMid.toFixed(1)}" width="4" height="${(bottomY - gapMid).toFixed(1)}" fill="#ffffff" stroke="${MUTED}" stroke-width="0.6"/>`,
|
||||
// 고무판 150 × 150 × 15, 타르페이퍼 T15, 다웰 D25 in 강관 Φ60 L300.
|
||||
`<rect x="268" y="${bottomY}" width="24" height="${padH}" fill="#2b2b2b"/>`,
|
||||
line(260, bracketY, 300, bracketY, 'stroke="#8a5a2b" stroke-width="2.4"'),
|
||||
`<rect x="276" y="${bottomY - 40}" width="8" height="40" fill="none" stroke="${INK}" stroke-width="0.9"/>`,
|
||||
line(280, bottomY - 46, 280, bracketY + 48, `stroke="${INK}" stroke-width="1.8"`),
|
||||
poly([[X(wb), Y(low)], [X(0), Y(low)], [X(0), Y(0)], [X(pb), Y(0)], [X(pb), Y(-bH)], [X(wb), Y(-bH)]], `fill="${WALL}" stroke="${MUTED}"`),
|
||||
poly(piece(t1, t1 + farLen, false), slabStyle),
|
||||
poly(piece(tz, t0, true), slabStyle),
|
||||
];
|
||||
// 간격: 아스팔트 충진재 (위 30 %), 스티로폼 (아래).
|
||||
const gTop = Y(st[0].top);
|
||||
const gBot = Y(-bH);
|
||||
const gMid = gTop + (gBot - gTop) * 0.3;
|
||||
const gx = Xs(t0);
|
||||
parts.push(`<rect x="${gx.toFixed(1)}" y="${gTop.toFixed(1)}" width="${(X(pb) - gx).toFixed(1)}" height="${(gMid - gTop).toFixed(1)}" fill="#3b3f45"/>`);
|
||||
parts.push(`<rect x="${gx.toFixed(1)}" y="${gMid.toFixed(1)}" width="${(X(pb) - gx).toFixed(1)}" height="${(gBot - gMid).toFixed(1)}" fill="#ffffff" stroke="${MUTED}" stroke-width="0.5"/>`);
|
||||
// 타르페이퍼 (브라켓 상면), 고무판 150 (가운데), 다웰 D25 in 강관 Φ60 L300.
|
||||
const xc = (X(wb) + Xs(t0)) / 2;
|
||||
parts.push(line(X(wb), Y(-bH), X(pb), Y(-bH), 'stroke="#8a5a2b" stroke-width="2.4"'));
|
||||
parts.push(`<rect x="${(xc - 0.075 * k).toFixed(1)}" y="${(Y(-bH) - 3).toFixed(1)}" width="${(0.15 * k).toFixed(1)}" height="3" fill="#2b2b2b"/>`);
|
||||
parts.push(`<rect x="${(xc - 0.03 * k).toFixed(1)}" y="${(Y(-bH) - 3 - 0.3 * k).toFixed(1)}" width="${(0.06 * k).toFixed(1)}" height="${(0.3 * k).toFixed(1)}" fill="none" stroke="${INK}" stroke-width="0.9"/>`);
|
||||
parts.push(line(xc, Y(-bH) - 0.34 * k, xc, Y(-bH) + 0.3 * k, `stroke="${INK}" stroke-width="1.8"`));
|
||||
// 치수.
|
||||
const yRow = Y(-bH) + 18;
|
||||
if (d.form === 'exposed') parts.push(dimLine([X(he), Y(lv(he).bottom)], [X(wb), Y(-bH)], yRow, 'h', `헌치 ${d.haunchLengthMm}`, true));
|
||||
// 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (예: 300 − 5 = 295).
|
||||
parts.push(dimLine([X(wb), Y(-bH)], [Xs(t0), Y(-bH)], yRow, 'h', `${Math.round((t0 - wb) * 1000)}`, true));
|
||||
const yLen = Y(low) + 22;
|
||||
parts.push(dimLine([X(t1), Y(lv(t1).bottom)], [Xs(t0), Y(st[0].bottom)], yLen, 'h', `${d.lengthMm} (중간 생략)`, true));
|
||||
parts.push(dimLine([X(t1), Y(lv(t1).top)], [X(t1), Y(lv(t1).bottom)], X(t1) - 12, 'v', `${d.thicknessMm}`));
|
||||
parts.push(dimLine([X(0), Y(0)], [X(0), Y(-bH)], X(0) + 16, 'v', `${Math.round(bH * 1000)}`, true));
|
||||
// 글자: 경사, EL, 부속재 (오른쪽 · 왼쪽 아래로 끌어냄).
|
||||
const xMidNear = (X(tz) + Xs(t0)) / 2;
|
||||
parts.push(text(xMidNear, Y(lv(tz).top) - 12, `경사 ${d.slopeLabel}`, 'middle'));
|
||||
const xr = X(0) + 58;
|
||||
const lead = (x1: number, y1: number, x2: number, y2: number, s: string, anchor: 'start' | 'end') =>
|
||||
line(x1, y1, x2, y2, `stroke="${MUTED}" stroke-width="0.7"`) + text(x2 + (anchor === 'start' ? 3 : -3), y2 + 4, s, anchor);
|
||||
parts.push(lead(298, topY + 6, 372, 58, `간격 ${d.parapetGapMm} · 아스팔트 충진재`, 'start'));
|
||||
parts.push(lead(298, gapMid + 12, 372, 100, '스티로폼', 'start'));
|
||||
parts.push(lead(272, bottomY + 2, 190, 196, '고무판 150 × 150 × 15', 'end'));
|
||||
parts.push(lead(264, bracketY + 1, 190, 214, '타르페이퍼 T = 15', 'end'));
|
||||
parts.push(lead(280, bracketY + 40, 190, 232, '다웰 D25 · 강관 Φ60 L300', 'end'));
|
||||
parts.push(text(130, exposed ? 120 : (topY + bottomY) / 2 + 4, exposed ? `노출형 · 끝 ${Math.round(d.bracketHeightM * 1000)}` : `접속슬래브 ${d.thicknessMm}`, 'middle'));
|
||||
parts.push(text(40, topY + drop - 6, `S = ${Math.abs(d.gradePct).toFixed(2)} % (${d.gradePct < 0 ? '하향' : '상향'})`, 'start'));
|
||||
parts.push(text(368, 24, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||||
parts.push(text(368, bracketY + 4, `브라켓 상면 ${el(d.parapetTopEl - d.bracketHeightM)}`, 'start'));
|
||||
parts.push(text(330, 252, '흉벽 · 벽체', 'middle', MUTED));
|
||||
parts.push(text(xr, Y(0) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||||
parts.push(lead(gx + 2, gTop + 4, xr - 4, Y(0) + 26, `간격 ${d.parapetGapMm} · 아스팔트 충진재`, 'start'));
|
||||
parts.push(lead(gx + 2, gMid + 8, xr - 4, Y(0) + 48, '스티로폼', 'start'));
|
||||
parts.push(text(xr, Y(-bH) + 4, `브라켓 상면 ${el(d.parapetTopEl - bH)}`, 'start'));
|
||||
const xl = X(wb) - 24;
|
||||
parts.push(lead(xc - 6, Y(-bH) - 1, xl, Y(-bH) + 46, '고무판 150 × 150 × 15', 'end'));
|
||||
parts.push(lead(X(wb) + 4, Y(-bH) + 1, xl, Y(-bH) + 64, '타르페이퍼 T = 15', 'end'));
|
||||
parts.push(lead(xc, Y(-bH) + 0.25 * k, xl, Y(-bH) + 82, '다웰 D25 @400 · 강관 Φ60 L300', 'end'));
|
||||
parts.push(text(X(0) + 6, Y(low) - 4, '흉벽 · 벽체', 'start', MUTED));
|
||||
const W = xr + 200;
|
||||
const H = yLen + 24;
|
||||
return [
|
||||
`<svg viewBox="0 -22 560 292" width="100%" role="img" aria-label="흉벽–접속슬래브 접합 상세 (모식도): 간격 ${d.parapetGapMm}, 고무판, 타르페이퍼, 다웰 D25">`,
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="접속슬래브 단면 · 접합 상세 · ${d.form === 'exposed' ? '노출형' : '매립형'} · 경사 ${d.slopeLabel} · 길이 ${d.lengthMm} (중간 생략) · 두께 ${d.thicknessMm} · 흉벽 높이 ${Math.round(bH * 1000)}">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
|
||||
+17
-23
@@ -7,12 +7,12 @@ 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 { applyShapeEdit, applySlabEdit, setSlabForm, setSlabSlope } from '../src/core/abut/edit';
|
||||
import { applyShapeEdit, applySlabEdit, setSlabForm } from '../src/core/abut/edit';
|
||||
import { abutMemberRows } from '../src/core/abut/member-list';
|
||||
import { resolveSlab, slabChangedKeys, slabGradePct, slabIssues, slabSection, slabSlopeLabel } from '../src/core/abut/slab';
|
||||
import { checkStoredAbutments } from '../src/core/project';
|
||||
import { abutPlanOutline, buildAbutGroup } from '../src/view/abut-scene';
|
||||
import { abutSlabJointSvg, abutSlabPlanSvg, abutSlabSectionSvg } from '../src/view/abut-slab-svg';
|
||||
import { abutSlabPlanSvg, abutSlabSectionSvg } from '../src/view/abut-slab-svg';
|
||||
|
||||
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
|
||||
@@ -25,14 +25,13 @@ const build = (r = rec) => {
|
||||
const SHAPE = { parapetBackM: -0.5, wallBackM: -0.8, bracketHeightM: 0.85 };
|
||||
|
||||
describe('접속슬래브 입력 (2026-11-04, spec_abut §4.8, 시안 ② 접속슬래브 카드)', () => {
|
||||
it('defaults: 매립형 · 하향 4 % · 7000 × 450 · 간격 5 / 20; the slope follows the form unless set', () => {
|
||||
expect(resolveSlab()).toEqual({ form: 'buried', slope: 'down4', lengthMm: 7000, thicknessMm: 450, parapetGapMm: 5, wingGapMm: 20, haunchLengthMm: 2000 });
|
||||
expect(resolveSlab({ form: 'exposed' }).slope).toBe('grade');
|
||||
const exposed = setSlabForm({ ...rec, slab: { slope: 'down4' } }, 'exposed');
|
||||
expect(exposed.slab).toEqual({ form: 'exposed' }); // picking a form resets the slope to the form's own
|
||||
expect(setSlabSlope(exposed, 'down4').slab).toEqual({ form: 'exposed', slope: 'down4' });
|
||||
expect(setSlabSlope(exposed, 'grade').slab).toEqual({ form: 'exposed' });
|
||||
expect(slabChangedKeys(exposed.slab)).toEqual(['form', 'slope']);
|
||||
it('defaults: 매립형 · 7000 × 450 · 간격 5 / 20; the form sets the slope, an old stored slope is dropped', () => {
|
||||
expect(resolveSlab()).toEqual({ form: 'buried', lengthMm: 7000, thicknessMm: 450, parapetGapMm: 5, wingGapMm: 20, haunchLengthMm: 2000 });
|
||||
const old = { form: 'exposed', slope: 'down4' } as unknown as Parameters<typeof resolveSlab>[0];
|
||||
expect(resolveSlab(old)).toEqual({ ...resolveSlab(), form: 'exposed' });
|
||||
expect(setSlabForm({ ...rec, slab: old }, 'exposed').slab).toEqual({ form: 'exposed' });
|
||||
expect(slabGradePct(resolveSlab(old), 1.91)).toBe(1.91); // 노출형 = 종단경사, whatever was stored
|
||||
expect(slabChangedKeys({ form: 'exposed' })).toEqual(['form']);
|
||||
expect(slabChangedKeys(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -43,7 +42,7 @@ describe('접속슬래브 입력 (2026-11-04, spec_abut §4.8, 시안 ② 접속
|
||||
expect(() => applyShapeEdit({ ...rec, slab: { thicknessMm: 600 } }, 'bracketHeightMm', 500)).toThrow('접속슬래브 두께');
|
||||
});
|
||||
|
||||
it('slope text and value: 하향 4 % = −4; 종단경사 = the wing slope (A1 = −i)', () => {
|
||||
it('slope text and value: 매립형 하향 4 % = −4; 노출형 종단경사 = the wing slope (A1 = −i)', () => {
|
||||
const { model } = build();
|
||||
expect(slabGradePct(resolveSlab(), model.wing!.slopePct)).toBe(-4);
|
||||
expect(slabGradePct(resolveSlab({ form: 'exposed' }), 1.91)).toBe(1.91);
|
||||
@@ -112,12 +111,13 @@ describe('접속슬래브 입력 (2026-11-04, spec_abut §4.8, 시안 ② 접속
|
||||
|
||||
it('the project file keeps the slab input and refuses a wrong form', () => {
|
||||
expect(() => checkStoredAbutments([{ ...rec, slab: { form: 'exposed', lengthMm: 8000 } }])).not.toThrow();
|
||||
expect(() => checkStoredAbutments([{ ...rec, slab: { form: 'exposed', slope: 'down4' } as never }])).not.toThrow(); // earlier files
|
||||
expect(() => checkStoredAbutments([{ ...rec, slab: { form: 'open' } }])).toThrow('접속슬래브');
|
||||
expect(() => checkStoredAbutments([{ ...rec, slab: { lengthMm: 'x' } }])).toThrow('접속슬래브');
|
||||
});
|
||||
});
|
||||
|
||||
describe('접속슬래브 삽도 (평면 · 단면 · 접합 상세)', () => {
|
||||
describe('접속슬래브 삽도 (평면, 단면 + 접합 상세)', () => {
|
||||
const { model, members, wings } = build();
|
||||
const sh = members.shape;
|
||||
const sectionOf = (form: 'buried' | 'exposed') => {
|
||||
@@ -137,19 +137,13 @@ describe('접속슬래브 삽도 (평면 · 단면 · 접합 상세)', () => {
|
||||
for (const s of [`폭 ${(model.wing!.slabWidthMm / 1000).toFixed(3)} m`, '>7000<', '모따기 300 × 300', '접속슬래브']) expect(svg).toContain(s);
|
||||
});
|
||||
|
||||
it('단면 매립형: 7000, 450, 하향 4 %, EL labels; 노출형 adds 헌치 2000 and 끝 850', () => {
|
||||
it('단면 + 접합 상세 (중간 생략): 7000, 450, 흉벽 높이 850, 얹힘 295, accessories and EL; 노출형 adds 헌치 2000', () => {
|
||||
const buried = sectionOf('buried');
|
||||
for (const s of ['>7000<', '>450<', '경사 하향 4 %', '흉벽 상단 EL.46.867', '브라켓 상면 EL.46.017', '간격 5', '다웰 D25 @400']) expect(buried).toContain(s);
|
||||
for (const s of ['7000 (중간 생략)', '>450<', '>850<', '>295<', '경사 하향 4 %', '흉벽 상단 EL.46.867', '브라켓 상면 EL.46.017',
|
||||
'간격 5 · 아스팔트 충진재', '스티로폼', '고무판 150 × 150 × 15', '타르페이퍼 T = 15', '다웰 D25 @400 · 강관 Φ60 L300']) expect(buried).toContain(s);
|
||||
expect(buried).not.toContain('헌치');
|
||||
expect(buried.match(/<polygon /g)).toHaveLength(3); // 흉벽 · 벽체, slab far piece, slab near piece
|
||||
const exposed = sectionOf('exposed');
|
||||
for (const s of ['헌치 2000', '끝 850', '경사 종단경사 +']) expect(exposed).toContain(s);
|
||||
});
|
||||
|
||||
it('접합 상세: the accessories and the EL labels', () => {
|
||||
const svg = abutSlabJointSvg({ form: 'buried', gradePct: -4, parapetTopEl: 46.867, bracketHeightM: 0.85, parapetGapMm: 5, thicknessMm: 450 });
|
||||
for (const s of ['간격 5 · 아스팔트 충진재', '스티로폼', '고무판 150 × 150 × 15', '타르페이퍼 T = 15', '다웰 D25 · 강관 Φ60 L300', 'S = 4.00 % (하향)', '흉벽 상단 EL.46.867', '브라켓 상면 EL.46.017', '접속슬래브 450']) {
|
||||
expect(svg).toContain(s);
|
||||
}
|
||||
expect(abutSlabJointSvg({ form: 'exposed', gradePct: 1.91, parapetTopEl: 46.867, bracketHeightM: 0.85, parapetGapMm: 5, thicknessMm: 450 })).toContain('노출형 · 끝 850');
|
||||
for (const s of ['헌치 2000', '경사 종단경사 +', '>295<']) expect(exposed).toContain(s);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user