feat(plan): ② 교대 카드를 3D 창 아래로 옮기고 교량받침 삽도 3개 추가
- 교대 카드를 왼쪽 목록 아래에서 가운데 3D 창 아래(교각 부재 카드 자리)로 옮김. 부재 · 교대 목록에서 고른 부재의 내용을 보여 주고, 머리줄에 부재 이름과 요약을 표시 - 왼쪽 편집 영역 정리: ②의 편집 버튼과 접히는 편집 영역을 없앰. 왼쪽에는 구조물 목록과 부재 · 교대 목록만 남김 - 교량받침 카드: 블럭 배치(분리 고정, 일체 비활성), 상세 보기(받침 ①–⑤), 삽도 3개(평면 · 단면 · 솔플레이트 상세, src/view/abut-bearing-svg.ts), ELEVATION TABLE, 솔플레이트 TABLE, 공통 · 규칙 입력, 검사 줄 - 삽도 테스트 3건 추가. 전체 650건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e3950eb67
commit
4b02dde153
5 files changed
+231
-32
No files matched your search
+10
-14
@@ -183,20 +183,6 @@
|
||||
<p id="plan-pier-title" class="lbl">교각·교대 목록</p>
|
||||
<div class="phead" aria-hidden="true"><span>교각</span><span>변경</span></div>
|
||||
<ul id="plan-pier-items" class="pier-items"></ul>
|
||||
<section id="abut-panel-plan" class="pier-input" hidden aria-labelledby="abut-panel-plan-title">
|
||||
<p id="abut-panel-plan-title" class="lbl"><span id="abut-panel-plan-name"></span>
|
||||
<button id="abut-panel-plan-edit-toggle" class="btn small" type="button" aria-expanded="false">편집</button></p>
|
||||
<div id="abut-panel-plan-editor" hidden>
|
||||
<div id="abut-panel-plan-basic" class="edit-row"></div>
|
||||
<details class="abut-group">
|
||||
<summary>교량받침 · 받침블럭 · 솔플레이트</summary>
|
||||
<div id="abut-panel-plan-bearings" class="edit-row"></div>
|
||||
</details>
|
||||
<div id="abut-panel-plan-wing" class="edit-row"></div>
|
||||
<p id="abut-panel-plan-msg" class="muted" aria-live="polite"></p>
|
||||
</div>
|
||||
<div id="abut-panel-plan-shape" class="edit-row abut-shape-view"></div>
|
||||
</section>
|
||||
<p class="lbl abut-members-title">부재 · 교대</p>
|
||||
<ul id="abut-members" class="members abut-members" aria-label="교대 부재"></ul>
|
||||
<p class="lbl members-title">부재 · 5개</p>
|
||||
@@ -205,6 +191,16 @@
|
||||
|
||||
<div class="center-col">
|
||||
<div class="viewer-slot" id="slot-plan"></div>
|
||||
<!-- ② 교대 부재 카드 (시안, 2026-11-03): the card of the row picked in 부재 · 교대, under the 3D view. -->
|
||||
<section id="abut-panel-plan" class="panel viewer abut-card" hidden aria-labelledby="abut-panel-plan-title">
|
||||
<div class="viewer-head">
|
||||
<h2 id="abut-panel-plan-title"><span id="abut-panel-plan-member">교량받침 · 받침블럭 · 솔플레이트</span> <span class="muted small" id="abut-panel-plan-name"></span></h2>
|
||||
</div>
|
||||
<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>
|
||||
<p id="abut-panel-plan-msg" class="muted" aria-live="polite"></p>
|
||||
</section>
|
||||
<section class="panel viewer" aria-labelledby="member-title">
|
||||
<div class="viewer-head">
|
||||
<h2 id="member-title">코핑</h2>
|
||||
|
||||
+75
-18
@@ -11,6 +11,7 @@ import { ABUT_DETAIL_LABELS, ABUT_SEGMENT_HEADER, abutInputRows, abutSegmentRows
|
||||
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 { abutShown, piersShown } from './core/abut/visibility';
|
||||
import { abutBearingPlanSvg, abutBearingSectionSvg, abutSolplateSvg, type AbutBearingDetail } from './view/abut-bearing-svg';
|
||||
import {
|
||||
ABUT_BEARING_RULE_DEFAULT,
|
||||
ABUT_BLOCK_MM,
|
||||
@@ -145,6 +146,8 @@ const state = {
|
||||
abutActive: null as string | null,
|
||||
/** The 부재 · 교대 row whose card is open in ② (2026-11-03). */
|
||||
abutMember: 'bearing' as AbutMemberId,
|
||||
/** The bearing the ② 교량받침 삽도 draw (상세 보기). */
|
||||
abutBearingNo: 1,
|
||||
/** The editable part of the 교대 panel is open (2026-11-03). */
|
||||
abutEditOpen: false,
|
||||
/** 교대 기본값 (가져온 그대로), for 기본값 복원. */
|
||||
@@ -713,7 +716,8 @@ 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' },
|
||||
plan: { wing: 'abut-panel-plan-wing', root: 'abut-panel-plan',name: 'abut-panel-plan-name', toggle: 'abut-panel-plan-edit-toggle', editor: 'abut-panel-plan-editor', basic: 'abut-panel-plan-basic', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
// ② 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' },
|
||||
} as const;
|
||||
|
||||
/** Shows the 교대 입력 in place of the pier info when an abutment is selected (2026-11-03). */
|
||||
@@ -779,7 +783,18 @@ function fillAbutBearingCard(
|
||||
}
|
||||
return tbl;
|
||||
};
|
||||
box.append(el('p', '블럭 배치: 분리 (교대 일체 블럭은 규칙 미정 — 교좌면이 계단이라 분리 고정)', 'muted small'));
|
||||
// 블럭 배치 (시안): 분리 고정, 일체는 규칙 미정이라 고를 수 없다.
|
||||
const layout = el('div', undefined, 'abut-bearing-pick');
|
||||
layout.append(el('span', '블럭 배치', 'muted small'));
|
||||
const sep = el('button', '분리', 'btn small');
|
||||
sep.type = 'button';
|
||||
sep.setAttribute('aria-pressed', 'true');
|
||||
const one = el('button', '일체', 'btn small');
|
||||
one.type = 'button';
|
||||
one.disabled = true;
|
||||
one.title = '교대 일체 블럭은 규칙 미정 (교좌면이 계단이라 분리 고정)';
|
||||
layout.append(sep, one, el('span', '교대 일체 블럭은 규칙 미정 (교좌면이 계단이라 분리 고정)', 'muted small'));
|
||||
box.append(layout);
|
||||
let model: AbutModel;
|
||||
let members: AbutMembers;
|
||||
try {
|
||||
@@ -790,11 +805,46 @@ function fillAbutBearingCard(
|
||||
return;
|
||||
}
|
||||
const m3 = (v: number) => v.toFixed(3);
|
||||
const elev = abutElevationRows(model, rule);
|
||||
const sol = abutSolplateRows(model, rule, rec.slopeI);
|
||||
// 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing.
|
||||
const pickNo = elev.some((r) => r.no === state.abutBearingNo) ? state.abutBearingNo : elev[0].no;
|
||||
const picker = el('div', undefined, 'abut-bearing-pick');
|
||||
picker.append(el('span', '상세 보기 (받침)', 'muted small'));
|
||||
for (const r of elev) {
|
||||
const b = el('button', r.seg, 'btn small');
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(r.no === pickNo));
|
||||
b.addEventListener('click', () => {
|
||||
state.abutBearingNo = r.no;
|
||||
renderAll();
|
||||
});
|
||||
picker.append(b);
|
||||
}
|
||||
box.append(picker);
|
||||
const er = elev.find((r) => r.no === pickNo)!;
|
||||
const sr = sol.find((r) => r.no === pickNo)!;
|
||||
const detail: AbutBearingDetail = {
|
||||
no: er.no, blockMm: ABUT_BLOCK_MM, padMm: ABUT_PAD_PLAN_MM, A: er.A, B: er.B, L: sr.L, W: sr.W,
|
||||
h1: er.h1, h2: er.h2, h3: er.h3, h4: er.h4, T1: sr.T1, T2: sr.T2, rotDeg: er.rotDeg, S: sr.S, seatEl: er.seatEl, z: er.z,
|
||||
};
|
||||
const figs = el('div', undefined, 'abut-bearing-figs');
|
||||
for (const [svg, caption] of [
|
||||
[abutBearingPlanSvg(detail), '평면 · 점선 = 몰탈패드, 주황 점선 = 솔플레이트'],
|
||||
[abutBearingSectionSvg(detail), '단면 · 교축 방향 · E = h1 + h2 + h3 + h4'],
|
||||
[abutSolplateSvg(detail), '솔플레이트 상세 · 가정'],
|
||||
]) {
|
||||
const fig = el('figure', undefined, 'abut-fig');
|
||||
fig.innerHTML = svg;
|
||||
fig.append(el('figcaption', caption, 'muted small'));
|
||||
figs.append(fig);
|
||||
}
|
||||
box.append(figs);
|
||||
box.append(el('p', '받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3·h4는 가정 (QA-15)', 'lbl'));
|
||||
box.append(
|
||||
table(
|
||||
['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'],
|
||||
abutElevationRows(model, rule).map((r) => [
|
||||
elev.map((r) => [
|
||||
String(r.no), m3(r.sFromIp3M), r.seg, m3(r.seatEl), String(r.h1), String(r.h2), m3(r.z), String(r.h3), String(r.h4),
|
||||
String(r.E), m3(r.solplateTopEl), String(r.A), String(r.B), r.stepGapM === null ? '—' : m3(r.stepGapM),
|
||||
r.azimuthCadDeg === null || r.azimuthNorthDeg === null ? '입력 없음' : `${r.azimuthCadDeg.toFixed(3)} / ${r.azimuthNorthDeg.toFixed(3)}`,
|
||||
@@ -806,7 +856,7 @@ function fillAbutBearingCard(
|
||||
box.append(
|
||||
table(
|
||||
['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 ≈ L·|S|'],
|
||||
abutSolplateRows(model, rule, rec.slopeI).map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]),
|
||||
sol.map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]),
|
||||
),
|
||||
);
|
||||
box.append(el('p', `공통 · 규칙 — 블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small'));
|
||||
@@ -846,11 +896,15 @@ function fillAbutBearingCard(
|
||||
/** 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;
|
||||
const group = (id: string) => $<HTMLElement>(id).closest<HTMLElement>('details') ?? $<HTMLElement>(id);
|
||||
group('abut-panel-plan-bearings').hidden = member !== 'bearing';
|
||||
group('abut-panel-plan-shape').hidden = member !== 'shape';
|
||||
$<HTMLElement>('abut-panel-plan-bearings').hidden = member !== 'bearing';
|
||||
$<HTMLElement>('abut-panel-plan-shape').hidden = member !== 'shape';
|
||||
$<HTMLElement>('abut-panel-plan-wing').hidden = member !== 'wing';
|
||||
$<HTMLElement>('abut-panel-plan-msg').textContent = ABUT_MEMBER_EDITABLE[member] ? '' : `${rec.name} 입력은 2단계에서 추가합니다. 현재 값만 보여 줍니다.`;
|
||||
// The card title is the picked row; its 요약 follows the name (시안 머리줄).
|
||||
const slabWidth = abutSummaryRows(rec).find(([label]) => label === '접속슬래브 폭')?.[1] ?? '—';
|
||||
const row = abutMemberRows(rec, slabWidth).find((r) => r.id === member);
|
||||
$<HTMLElement>('abut-panel-plan-member').textContent = row?.label ?? '';
|
||||
$<HTMLElement>('abut-panel-plan-name').textContent = `${rec.name} · ${row?.summary ?? ''}`;
|
||||
$<HTMLElement>('abut-panel-plan-msg').textContent = ABUT_MEMBER_EDITABLE[member] ? '' : `${rec.name}: 이 부재의 입력은 2단계에서 추가합니다. 현재 값만 보여 줍니다.`;
|
||||
}
|
||||
|
||||
/** Fills one 교대 입력 panel: the basic items, the 받침 values and the 형상 values. Each change is checked and applied. */
|
||||
@@ -865,16 +919,19 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
if ('input' in slot) fillList($<HTMLDListElement>(slot.input), abutInputRows(rec));
|
||||
// ② has no 상세 (계산값) block (2026-11-03); ① keeps it.
|
||||
if ('summary' in slot) fillList($<HTMLDListElement>(slot.summary), abutSummaryRows(rec).filter(([label]) => ABUT_DETAIL_LABELS.includes(label)));
|
||||
// 편집 opens the editable fields; they stay closed until asked for (2026-11-03).
|
||||
const toggle = $<HTMLButtonElement>(slot.toggle);
|
||||
toggle.textContent = state.abutEditOpen ? '닫기' : '편집';
|
||||
toggle.setAttribute('aria-expanded', String(state.abutEditOpen));
|
||||
toggle.onclick = () => {
|
||||
state.abutEditOpen = !state.abutEditOpen;
|
||||
renderAll();
|
||||
};
|
||||
$<HTMLElement>(slot.editor).hidden = !state.abutEditOpen;
|
||||
const basic = $<HTMLDivElement>(slot.basic);
|
||||
// 편집 opens the editable fields in ①; they stay closed until asked for (2026-11-03). ② has no toggle.
|
||||
if ('toggle' in slot) {
|
||||
const toggle = $<HTMLButtonElement>(slot.toggle);
|
||||
toggle.textContent = state.abutEditOpen ? '닫기' : '편집';
|
||||
toggle.setAttribute('aria-expanded', String(state.abutEditOpen));
|
||||
toggle.onclick = () => {
|
||||
state.abutEditOpen = !state.abutEditOpen;
|
||||
renderAll();
|
||||
};
|
||||
$<HTMLElement>(slot.editor).hidden = !state.abutEditOpen;
|
||||
}
|
||||
// ② has no basic items; a detached box keeps the shared code simple.
|
||||
const basic = 'basic' in slot ? $<HTMLDivElement>(slot.basic) : el('div');
|
||||
const bearings = $<HTMLDivElement>(slot.bearings);
|
||||
const shape = $<HTMLDivElement>(slot.shape);
|
||||
const msg = $<HTMLParagraphElement>(slot.msg);
|
||||
|
||||
@@ -901,3 +901,12 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
|
||||
#abut-panel-plan-bearings > .abut-seg { display: block; overflow-x: auto; border-collapse: collapse; font-size: 12px; }
|
||||
#abut-panel-plan-bearings > .abut-seg th, #abut-panel-plan-bearings > .abut-seg td { border-bottom: 1px solid #e6eaef; padding: 4px 6px; text-align: right; white-space: nowrap; font-family: 'JetBrains Mono', ui-monospace, monospace; }
|
||||
#abut-panel-plan-bearings > .abut-seg th { color: #55606e; font-weight: 500; font-family: inherit; }
|
||||
|
||||
/* ② 교대 카드 under the 3D view (2026-11-03): pickers, the three 받침 삽도 side by side, full-width rows. */
|
||||
#abut-panel-plan .abut-bearing-pick { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||
#abut-panel-plan .abut-bearing-pick .btn[aria-pressed='true'] { background: #1f5fa8; border-color: #1f5fa8; color: #ffffff; }
|
||||
#abut-panel-plan .abut-bearing-figs { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
||||
#abut-panel-plan .abut-fig { margin: 0; background: #f6f8fa; border: 1px solid #d5dbe2; border-radius: 6px; padding: 6px; }
|
||||
#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); } }
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 교대 받침 상세 삽도 (2026-11-03, 시안 ② 교량받침 카드): 평면, 교축 방향 단면, 솔플레이트 상세. 순수 문자열 함수.
|
||||
* 입력은 받침 표에서 이미 계산한 값뿐이다(교대 모델을 다시 읽지 않는다). 치수는 mm, EL은 m.
|
||||
*/
|
||||
|
||||
export interface AbutBearingDetail {
|
||||
no: number;
|
||||
/** 블럭 평면 (정사각), 몰탈패드 평면 (정사각), mm. */
|
||||
blockMm: number;
|
||||
padMm: number;
|
||||
/** 받침 A × B, 솔플레이트 L × W (mm). */
|
||||
A: number;
|
||||
B: number;
|
||||
L: number;
|
||||
W: number;
|
||||
/** 높이 (mm): 블럭 콘크리트 h1, 패드 h2, 받침 h3, 솔플레이트 h4 = T. */
|
||||
h1: number;
|
||||
h2: number;
|
||||
h3: number;
|
||||
h4: number;
|
||||
T1: number;
|
||||
T2: number;
|
||||
/** 블럭 평면 회전 (deg), 교대축 직각 기준. */
|
||||
rotDeg: number;
|
||||
/** 솔플레이트 경사 (%). */
|
||||
S: number;
|
||||
seatEl: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
const INK = '#18212b';
|
||||
const MUTED = '#55606e';
|
||||
const ORANGE = '#c46a1a';
|
||||
const text = (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 square = (cx: number, cy: number, w: number, h: number, style: string) =>
|
||||
`<rect x="${(cx - w / 2).toFixed(1)}" y="${(cy - h / 2).toFixed(1)}" width="${w.toFixed(1)}" height="${h.toFixed(1)}" ${style}/>`;
|
||||
|
||||
/** 평면: 블럭 1200, 몰탈패드 1000 (점선), 솔플레이트 L × W (주황 점선), 받침 A × B. 블럭은 받침 방위각만큼 돈다. */
|
||||
export function abutBearingPlanSvg(d: AbutBearingDetail): string {
|
||||
const k = 0.17; // px per mm
|
||||
const cx = 150;
|
||||
const cy = 130;
|
||||
const turn = `transform="rotate(${(-d.rotDeg).toFixed(3)} ${cx} ${cy})"`;
|
||||
return [
|
||||
`<svg viewBox="0 0 300 270" width="100%" role="img" aria-label="받침 ${d.no} 평면: 블럭 ${d.blockMm}, 몰탈패드 ${d.padMm}, 받침 ${d.A} × ${d.B}, 솔플레이트 ${d.L} × ${d.W}, 회전 ${d.rotDeg.toFixed(2)}°">`,
|
||||
`<g ${turn}>`,
|
||||
square(cx, cy, d.blockMm * k, d.blockMm * k, `fill="#dfe5ec" stroke="${MUTED}"`),
|
||||
square(cx, cy, d.padMm * k, d.padMm * k, `fill="none" stroke="${MUTED}" stroke-dasharray="4 3"`),
|
||||
square(cx, cy, d.L * k, d.W * k, `fill="none" stroke="${ORANGE}" stroke-dasharray="5 3"`),
|
||||
square(cx, cy, d.A * k, d.B * k, `fill="#b9c3ce" stroke="${INK}"`),
|
||||
`</g>`,
|
||||
text(cx, cy - (d.blockMm * k) / 2 - 8, `${d.blockMm}`),
|
||||
text(cx, cy + 4, `${d.A} × ${d.B}`),
|
||||
text(cx, cy + (d.blockMm * k) / 2 + 16, `솔플 ${d.L} × ${d.W} · 패드 ${d.padMm}`, 'middle', MUTED),
|
||||
text(cx, 262, `받침 ${d.no} · 회전 ${d.rotDeg.toFixed(2)}° (방위각)`, 'middle', MUTED),
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
/** 교축 방향 단면 (높이 2배 과장): 블럭 h1, 패드 h2, 받침 h3, 솔플레이트 h4. E = h1 + h2 + h3 + h4. */
|
||||
export function abutBearingSectionSvg(d: AbutBearingDetail): string {
|
||||
const kx = 0.17;
|
||||
const ky = 0.34; // 2× exaggerated heights
|
||||
const cx = 150;
|
||||
const base = 215;
|
||||
const E = d.h1 + d.h2 + d.h3 + d.h4;
|
||||
const layer = (bottom: number, h: number, w: number, style: string) =>
|
||||
`<rect x="${(cx - (w * kx) / 2).toFixed(1)}" y="${(base - (bottom + h) * ky).toFixed(1)}" width="${(w * kx).toFixed(1)}" height="${(h * ky).toFixed(1)}" ${style}/>`;
|
||||
const at = (mm: number) => base - mm * ky;
|
||||
const right = cx + (d.blockMm * kx) / 2 + 6;
|
||||
return [
|
||||
`<svg viewBox="0 0 330 250" width="100%" role="img" aria-label="받침 ${d.no} 단면: h1 ${d.h1}, h2 ${d.h2}, h3 ${d.h3}, h4 ${d.h4}, E ${E}">`,
|
||||
`<line x1="20" y1="${base}" x2="300" y2="${base}" stroke="${MUTED}"/>`,
|
||||
layer(0, d.h1, d.blockMm, `fill="#dfe5ec" stroke="${MUTED}"`),
|
||||
layer(d.h1, d.h2, d.padMm, `fill="#c9d1da" stroke="${MUTED}"`),
|
||||
layer(d.h1 + d.h2, d.h3, d.A, `fill="#b9c3ce" stroke="${INK}"`),
|
||||
layer(d.h1 + d.h2 + d.h3, d.h4, d.L, `fill="#f0c9a0" stroke="${ORANGE}"`),
|
||||
text(right, at(d.h1 / 2) + 4, `h1 ${d.h1}`, 'start'),
|
||||
text(right, at(d.h1 + d.h2 / 2) + 4, `h2 ${d.h2}`, 'start'),
|
||||
text(right, at(d.h1 + d.h2 + d.h3 / 2) + 4, `h3 ${d.h3}`, 'start'),
|
||||
text(right, at(E - d.h4 / 2) + 4, `h4 ${d.h4}`, 'start'),
|
||||
text(cx, at(E) - 10, `E = ${E}`),
|
||||
text(24, base + 16, `교좌면 ${d.seatEl.toFixed(3)}`, 'start', MUTED),
|
||||
text(24, at(d.h1 + d.h2) - 4, `z ${d.z.toFixed(3)}`, 'start', MUTED),
|
||||
text(cx, 244, '교축 방향 · 높이 2배 과장', 'middle', MUTED),
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
/** 솔플레이트 상세: 길이 L, 두께 T1 → T2 (경사 S). 두께는 과장해서 그린다. */
|
||||
export function abutSolplateSvg(d: AbutBearingDetail): string {
|
||||
const kx = 0.3;
|
||||
const ky = 2.2; // thickness exaggerated
|
||||
const x0 = 45;
|
||||
const x1 = x0 + d.L * kx;
|
||||
const base = 120;
|
||||
return [
|
||||
`<svg viewBox="0 0 330 190" width="100%" role="img" aria-label="받침 ${d.no} 솔플레이트: L ${d.L}, T1 ${d.T1}, T2 ${d.T2}, S ${d.S.toFixed(3)} %">`,
|
||||
`<polygon points="${x0},${base} ${x1.toFixed(1)},${base} ${x1.toFixed(1)},${(base - d.T2 * ky).toFixed(1)} ${x0},${(base - d.T1 * ky).toFixed(1)}" fill="#f0c9a0" stroke="${ORANGE}"/>`,
|
||||
text(x0 - 4, base - (d.T1 * ky) / 2 + 4, `T1 ${d.T1}`, 'end'),
|
||||
text(x1 + 4, base - (d.T2 * ky) / 2 + 4, `T2 ${d.T2}`, 'start'),
|
||||
text((x0 + x1) / 2, base + 18, `L ${d.L} · W ${d.W}`),
|
||||
text((x0 + x1) / 2, base - Math.max(d.T1, d.T2) * ky - 10, `S ${d.S.toFixed(3)} % · T ${d.h4}`),
|
||||
text(165, 180, '솔플레이트 상세 · 두께 과장 · 가정 (QA-15)', 'middle', MUTED),
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { abutBearingPlanSvg, abutBearingSectionSvg, abutSolplateSvg, type AbutBearingDetail } from '../src/view/abut-bearing-svg';
|
||||
|
||||
// 운곡교 A1 받침 ① (교대 받침 표의 값: 블럭 125 = 75 + 50, h3 181, T 30, L 800, T1 38 / T2 22, 회전 −4.42°).
|
||||
const d: AbutBearingDetail = {
|
||||
no: 1, blockMm: 1200, padMm: 1000, A: 700, B: 640, L: 800, W: 740,
|
||||
h1: 75, h2: 50, h3: 181, h4: 30, T1: 38, T2: 22, rotDeg: -4.42, S: -1.91, seatEl: 44.367, z: 44.492,
|
||||
};
|
||||
|
||||
describe('교대 받침 삽도 (시안 ② 교량받침 카드)', () => {
|
||||
it('평면: block, pad, solplate and bearing sizes, turned by the bearing azimuth', () => {
|
||||
const svg = abutBearingPlanSvg(d);
|
||||
expect(svg).toContain('700 × 640');
|
||||
expect(svg).toContain('솔플 800 × 740 · 패드 1000');
|
||||
expect(svg).toContain('rotate(4.420 150 130)');
|
||||
expect(svg.match(/<rect /g)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('단면: the four layers and E = h1 + h2 + h3 + h4 = 336', () => {
|
||||
const svg = abutBearingSectionSvg(d);
|
||||
for (const s of ['h1 75', 'h2 50', 'h3 181', 'h4 30', 'E = 336', '교좌면 44.367', 'z 44.492']) expect(svg).toContain(s);
|
||||
expect(svg.match(/<rect /g)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('솔플레이트: L, W, T1 → T2 and the slope', () => {
|
||||
const svg = abutSolplateSvg(d);
|
||||
for (const s of ['T1 38', 'T2 22', 'L 800 · W 740', 'S -1.910 %']) expect(svg).toContain(s);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user