feat(abut): ② 날개벽 카드를 시안대로 작성 (좌우 같게 · 측별 치수 · 삽도 3개)
- 선택 줄 (타일): 좌우 같게(기본) / 따로 정하기, 편집할 측 (좌 · ① 쪽 / 우 · ⑤ 쪽), 형식 일반형(기본) / 단축형 / 없음 - 측별 치수 입력 (WingDims, 교대 기록 wingDims): 길이 6500, 두께 450, 끝 높이 1500(일반형), 헌치 300. 단축형 길이 기본 = 뒷굽 끝(1.807 m). 좌우 같게이면 양쪽에 함께 적용, 켤 때 좌측 값을 우측에 복사 - 연동: 두께 → 날개벽 3D · 접속슬래브 폭(geometry slabWidthMm) · 슬래브 평면, 헌치 → 연직 헌치 · 저판 상면 헌치 (0이면 없음) - 자동 값: 경사 (A1 = −i), 등단면 · 변화단면 경계(뒷굽 끝), 끝 EL 상단 / 하단 (가정), 단축형 하단 = 확대기초 상면 (뒷굽 밖은 AI02 안내) - 삽도 3개 (src/view/abut-wing-svg.ts): 평면(좌우 날개벽 · 접속슬래브, abutPlanOutline 공유), 측면(수평 단 · 뒷굽 끝), 날개벽–흉벽 모서리 모식도(헌치 · 브라켓 · 슬래브 모따기 · 간격) - "생성되는 날개벽" 줄과 시안 메모. 프로젝트 파일 검사에 wingDims · wingsLinked 추가 - 테스트 7건 추가 · 갱신. 전체 669건 통과, 타입 검사 · 빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a025a61209
commit
80e5302192
12 files changed
+610
-53
No files matched your search
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import type { AbutRecord } from './parse';
|
||||
import type { WingForm } from './geometry';
|
||||
import { WING_DIMS_DEFAULT, wingDimsIssues, type WingDims } from './wings';
|
||||
import { shapeIssues, type AbutShape } from './shape';
|
||||
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
||||
|
||||
@@ -32,11 +33,45 @@ export function applyAbutEdit(rec: AbutRecord, key: AbutEditKey, value: number):
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 좌우 같게 (기본): the form and dims apply to both sides. Off: each side keeps its own. */
|
||||
export function wingsLinked(rec: AbutRecord): boolean {
|
||||
return rec.wingsLinked !== false;
|
||||
}
|
||||
|
||||
export function setWingForm(rec: AbutRecord, side: 'left' | 'right', form: WingForm): AbutRecord {
|
||||
const current = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
if (wingsLinked(rec)) return { ...rec, wings: { left: form, right: form } };
|
||||
return { ...rec, wings: { ...current, [side]: form } };
|
||||
}
|
||||
|
||||
/** Turns 좌우 같게 on or off. Turning it on copies the left side (form and dims) to the right. */
|
||||
export function setWingsLinked(rec: AbutRecord, linked: boolean): AbutRecord {
|
||||
if (!linked) return { ...rec, wingsLinked: false };
|
||||
const left = rec.wings?.left ?? 'general';
|
||||
const dims = rec.wingDims?.left;
|
||||
return { ...rec, wingsLinked: true, wings: { left, right: left }, wingDims: dims ? { left: { ...dims }, right: { ...dims } } : undefined };
|
||||
}
|
||||
|
||||
/** 날개벽 치수 입력 칸 (② 날개벽 카드, 시안 순서). 단축형에는 끝 높이가 없다. */
|
||||
export const WING_DIM_FIELDS: { key: keyof WingDims; label: string; general: boolean; short: boolean }[] = [
|
||||
{ key: 'lengthMm', label: '길이 (mm)', general: true, short: true },
|
||||
{ key: 'thicknessMm', label: '두께 (mm)', general: true, short: true },
|
||||
{ key: 'endHeightMm', label: '끝 높이 (mm)', general: true, short: false },
|
||||
{ key: 'haunchMm', label: '헌치 (mm, 300 × 300의 한 변)', general: true, short: true },
|
||||
];
|
||||
|
||||
/** Changes one wing dim of a side (both sides when 좌우 같게). Length, thickness and end height must be > 0, the haunch ≥ 0. */
|
||||
export function applyWingDim(rec: AbutRecord, side: 'left' | 'right', key: keyof WingDims, value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
const probe = { ...WING_DIMS_DEFAULT, [key]: value };
|
||||
const issues = wingDimsIssues(probe);
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
const sides: ('left' | 'right')[] = wingsLinked(rec) ? ['left', 'right'] : [side];
|
||||
const next = { left: { ...(rec.wingDims?.left ?? {}) }, right: { ...(rec.wingDims?.right ?? {}) } };
|
||||
for (const s of sides) next[s] = { ...next[s], [key]: value };
|
||||
return { ...rec, wingDims: next };
|
||||
}
|
||||
|
||||
/** 받침 값 편집 (2026-11-03): z (패드 상면, m) or 교좌면 override (m, undefined = 자동 — 블럭 높이 규칙을 따른다). */
|
||||
export function applyBearingEdit(rec: AbutRecord, no: number, key: 'z' | 'seatEl', value: number | undefined): AbutRecord {
|
||||
const target = rec.bearings.find((b) => b.no === no);
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface AbutInput {
|
||||
girderAzimuthDeg?: number;
|
||||
/** 측별 날개벽 형식: 'left' = 받침 ① 쪽 끝, 'right' = 받침 n 쪽 끝(spec §4.7). 기본은 양측 일반형. */
|
||||
wings?: { left: WingForm; right: WingForm };
|
||||
/** 측별 날개벽 두께 (mm). 없으면 450. 접속슬래브 폭에 쓴다. */
|
||||
wingThicknessMm?: { left?: number; right?: number };
|
||||
/** 기초 하단 EL (m), 말뚝길이 (mm), 기초 형식 (AA3, 저판·말뚝). 없으면 저판·말뚝을 만들지 않는다. */
|
||||
foundationBottomEl?: number;
|
||||
pileLengthMm?: number;
|
||||
@@ -188,10 +190,11 @@ function buildWing(input: AbutInput, u: AbutPoint, nLeft: AbutPoint, frontSide:
|
||||
const pick = Math.cos(radLine) * back.e + Math.sin(radLine) * back.n > 0 ? line : norm360(line + 180);
|
||||
const axisCad = norm360(deg(Math.atan2(u.n, u.e)));
|
||||
const sinDelta = Math.abs(Math.sin(((pick - axisCad) * Math.PI) / 180));
|
||||
const wingAxisWidth = WING_THICKNESS_MM / sinDelta;
|
||||
const wings = input.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
const present = (f: WingForm) => (f === 'none' ? 0 : wingAxisWidth + SLAB_CLEARANCE_MM);
|
||||
const slabWidthMm = Math.round(length * 1000 - present(wings.left) - present(wings.right));
|
||||
// 날개벽 두께는 측별 입력 (2026-11-04, 없으면 450): 축 방향 폭 = 두께 / |sin Δ|.
|
||||
const thick = { left: input.wingThicknessMm?.left ?? WING_THICKNESS_MM, right: input.wingThicknessMm?.right ?? WING_THICKNESS_MM };
|
||||
const present = (f: WingForm, side: 'left' | 'right') => (f === 'none' ? 0 : thick[side] / sinDelta + SLAB_CLEARANCE_MM);
|
||||
const slabWidthMm = Math.round(length * 1000 - present(wings.left, 'left') - present(wings.right, 'right'));
|
||||
const girderDiffDeg = input.girderAzimuthDeg === undefined ? NaN : lineDiff(pick, input.girderAzimuthDeg);
|
||||
const slopeI = input.slopeI ?? 0;
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Issue } from '../types';
|
||||
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
|
||||
import type { AbutShape } from './shape';
|
||||
import type { AbutBearingRule } from './bearing-table';
|
||||
import type { WingDims } from './wings';
|
||||
|
||||
type Cell = string | number | null | undefined;
|
||||
type Grid = Cell[][];
|
||||
@@ -56,6 +57,10 @@ export interface AbutRecord {
|
||||
shape?: Partial<AbutShape>;
|
||||
/** 받침 규칙 (② 교량받침 카드: h3, 솔플레이트 T·여유, 받침 종류). Only the changed values. Absent = 교각 P1 가정값 (QA-15). */
|
||||
bearingRule?: Partial<AbutBearingRule>;
|
||||
/** 날개벽 치수 (② 날개벽 카드, 측별, mm). Only the changed values. */
|
||||
wingDims?: { left?: Partial<WingDims>; right?: Partial<WingDims> };
|
||||
/** 좌우 같게 (기본 true): 형식과 치수를 양쪽에 함께 적용한다. */
|
||||
wingsLinked?: boolean;
|
||||
}
|
||||
|
||||
export interface AbutParseResult {
|
||||
@@ -207,6 +212,7 @@ export function toAbutInput(rec: AbutRecord): AbutInput {
|
||||
skewDeg: rec.skewDeg,
|
||||
slopeI: rec.slopeI,
|
||||
wings: rec.wings,
|
||||
wingThicknessMm: { left: rec.wingDims?.left?.thicknessMm, right: rec.wingDims?.right?.thicknessMm },
|
||||
foundationBottomEl: rec.foundationBottomEl,
|
||||
pileLengthMm: rec.pileLengthMm,
|
||||
foundationType: rec.foundation,
|
||||
|
||||
@@ -14,7 +14,7 @@ const mm = (v: number) => `${Math.round(v)} mm`;
|
||||
export function abutSummaryRows(rec: AbutRecord): [string, string][] {
|
||||
const model: AbutModel = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
const wings = buildWings(model, members, rec.wings);
|
||||
const wings = buildWings(model, members, rec.wings, rec.wingDims);
|
||||
const shape = members.shape;
|
||||
const first = model.bearings[0];
|
||||
const L = model.length;
|
||||
|
||||
+65
-17
@@ -21,11 +21,43 @@ export const WING_THICKNESS_M = 0.45;
|
||||
/** 단부 높이 (끝점 상단에서 단부 하단까지, m). */
|
||||
export const WING_END_HEIGHT_M = 1.5;
|
||||
|
||||
/** 날개벽 치수 입력 (측별, mm, 2026-11-04 시안 ② 날개벽 카드). 단축형 길이의 기본값은 뒷굽 끝까지의 거리다. */
|
||||
export interface WingDims {
|
||||
lengthMm: number;
|
||||
thicknessMm: number;
|
||||
endHeightMm: number;
|
||||
haunchMm: number;
|
||||
}
|
||||
|
||||
export const WING_DIMS_DEFAULT: WingDims = { lengthMm: 6500, thicknessMm: 450, endHeightMm: 1500, haunchMm: 300 };
|
||||
|
||||
/** The dims of one side: the user's values over the defaults; 단축형 without a length input runs to the 뒷굽 끝. */
|
||||
export function resolveWingDims(form: WingForm, changes: Partial<WingDims> | undefined, heelDistanceM: number): WingDims {
|
||||
const base = { ...WING_DIMS_DEFAULT, ...(changes ?? {}) };
|
||||
if (form === 'short' && changes?.lengthMm === undefined) base.lengthMm = Math.round(heelDistanceM * 1000);
|
||||
return base;
|
||||
}
|
||||
|
||||
/** Input problems of one side's dims, in Korean; empty when the wing can be built. */
|
||||
export function wingDimsIssues(d: WingDims): string[] {
|
||||
const out: string[] = [];
|
||||
if (!(d.lengthMm > 0)) out.push('날개벽 길이는 0보다 커야 합니다.');
|
||||
if (!(d.thicknessMm > 0)) out.push('날개벽 두께는 0보다 커야 합니다.');
|
||||
if (!(d.endHeightMm > 0)) out.push('날개벽 끝 높이는 0보다 커야 합니다.');
|
||||
if (!(d.haunchMm >= 0)) out.push('헌치는 0 이상이어야 합니다.');
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface WingSolid {
|
||||
side: 'left' | 'right';
|
||||
form: Exclude<WingForm, 'none'>;
|
||||
/** 날개벽이 시작하는 축 끝: 0 (IP1 끝) 또는 L (IP3 끝). */
|
||||
sEnd: number;
|
||||
/** 이 측의 치수 (m): 길이, 두께, 끝 높이, 헌치 다리. */
|
||||
lengthM: number;
|
||||
thicknessM: number;
|
||||
endHeightM: number;
|
||||
haunchM: number;
|
||||
/** 평면 네 꼭짓점 (E, N), 순서: 시작, 끝, 끝 안쪽, 시작 안쪽. */
|
||||
corners: [AbutPoint, AbutPoint, AbutPoint, AbutPoint];
|
||||
topStartEl: number;
|
||||
@@ -66,7 +98,12 @@ export interface Haunch {
|
||||
|
||||
export const HAUNCH_LEG_M = 0.3;
|
||||
|
||||
export function buildWings(model: AbutModel, members: AbutMembers, forms?: { left: WingForm; right: WingForm }): WingSolid[] {
|
||||
export function buildWings(
|
||||
model: AbutModel,
|
||||
members: AbutMembers,
|
||||
forms?: { left: WingForm; right: WingForm },
|
||||
dims?: { left?: Partial<WingDims>; right?: Partial<WingDims> },
|
||||
): WingSolid[] {
|
||||
if (!model.wing) return [];
|
||||
const L = model.length;
|
||||
const wings = forms ?? model.wing.wings;
|
||||
@@ -89,26 +126,30 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
|
||||
|
||||
const build = (side: 'left' | 'right', sEnd: number, topStart: number, form: Exclude<WingForm, 'none'>): WingSolid => {
|
||||
const a = sEnd === 0 ? 1 : -1;
|
||||
const dm = resolveWingDims(form, dims?.[side], heelDistanceM);
|
||||
const length = dm.lengthMm / 1000;
|
||||
const thick = dm.thicknessMm / 1000;
|
||||
const leg = dm.haunchMm / 1000;
|
||||
const p0 = at(sEnd);
|
||||
const p1 = { e: p0.e + w.e * WING_LENGTH_M, n: p0.n + w.n * WING_LENGTH_M };
|
||||
const inward = { e: model.u.e * a * WING_THICKNESS_M, n: model.u.n * a * WING_THICKNESS_M };
|
||||
const topEnd = topStart + slope * WING_LENGTH_M;
|
||||
const bottomEnd = form === 'general' ? topEnd - WING_END_HEIGHT_M : footingTop;
|
||||
const p1 = { e: p0.e + w.e * length, n: p0.n + w.n * length };
|
||||
const inward = { e: model.u.e * a * thick, n: model.u.n * a * thick };
|
||||
const topEnd = topStart + slope * length;
|
||||
const bottomEnd = form === 'general' ? topEnd - dm.endHeightMm / 1000 : footingTop;
|
||||
const topAt = (d: number) => topStart + slope * d;
|
||||
const topLine = [
|
||||
{ dM: 0, el: topStart },
|
||||
{ dM: WING_LENGTH_M, el: topEnd },
|
||||
{ dM: length, el: topEnd },
|
||||
];
|
||||
const bottomLine =
|
||||
form === 'general' && heelDistanceM < WING_LENGTH_M
|
||||
form === 'general' && heelDistanceM < length
|
||||
? [
|
||||
{ dM: 0, el: footingTop },
|
||||
{ dM: heelDistanceM, el: footingTop },
|
||||
{ dM: WING_LENGTH_M, el: bottomEnd },
|
||||
{ dM: length, el: bottomEnd },
|
||||
]
|
||||
: [
|
||||
{ dM: 0, el: footingTop },
|
||||
{ dM: WING_LENGTH_M, el: form === 'general' ? bottomEnd : footingTop },
|
||||
{ dM: length, el: form === 'general' ? bottomEnd : footingTop },
|
||||
];
|
||||
|
||||
// 헌치: 위 헌치는 흉벽 배면(−0.5), 아래 헌치는 벽체 배면(−0.8)의 모서리에 둔다.
|
||||
@@ -122,8 +163,8 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
|
||||
const d = Math.abs(t) / k;
|
||||
// The corner lies on the 날개벽 내측면 (the face between the two wings), WING_THICKNESS_M in from the axis end.
|
||||
const inwardUnit = { e: model.u.e * a, n: model.u.n * a };
|
||||
const c = shift(pointAt(d), inwardUnit, WING_THICKNESS_M);
|
||||
const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)];
|
||||
const c = shift(pointAt(d), inwardUnit, thick);
|
||||
const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, leg), shift(c, inwardUnit, leg)];
|
||||
return {
|
||||
kind,
|
||||
corners,
|
||||
@@ -132,14 +173,17 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
|
||||
};
|
||||
};
|
||||
// 연직 헌치 (2026-11-03, 내측면 모서리): upper at the 흉벽 배면, lower at the 벽체 배면.
|
||||
const haunches = [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)];
|
||||
const haunches = leg > 0 ? [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)] : [];
|
||||
|
||||
// 저판 상면 헌치 (수평): from the 벽체 배면 to the 뒷굽, on the 저판 상면 next to the 날개벽 내측면.
|
||||
const footingHaunch = {
|
||||
dMFrom: Math.abs(members.shape.wallBackM) / k,
|
||||
dMTo: heelDistanceM,
|
||||
el: footingTop,
|
||||
};
|
||||
const footingHaunch =
|
||||
leg > 0
|
||||
? {
|
||||
dMFrom: Math.abs(members.shape.wallBackM) / k,
|
||||
dMTo: Math.min(heelDistanceM, length),
|
||||
el: footingTop,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// 수평 단 (QA-12): 등단면 = 끝 상단 − 1.5 m 위, 변화단면1 = 그 아래 ~ 교좌면 EL, 변화단면2 = 교좌면 EL ~ 저판 상면.
|
||||
// 교좌면 EL은 날개벽 끝에 가장 가까운 받침의 교좌면이다.
|
||||
@@ -157,6 +201,10 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
|
||||
side,
|
||||
form,
|
||||
sEnd,
|
||||
lengthM: length,
|
||||
thicknessM: thick,
|
||||
endHeightM: dm.endHeightMm / 1000,
|
||||
haunchM: leg,
|
||||
corners: [p0, p1, { e: p1.e + inward.e, n: p1.n + inward.n }, { e: p0.e + inward.e, n: p0.n + inward.n }],
|
||||
topStartEl: topStart,
|
||||
topEndEl: topEnd,
|
||||
|
||||
@@ -135,6 +135,14 @@ export function checkStoredAbutments(list: unknown): void {
|
||||
if (key === 'kind' ? typeof v !== 'string' : !finite(v)) throw new Error(`${who}: 받침 규칙 ${key}이(가) 맞지 않습니다.`);
|
||||
}
|
||||
}
|
||||
if (a.wingDims !== undefined) {
|
||||
for (const sideDims of [a.wingDims.left, a.wingDims.right]) {
|
||||
for (const [key, v] of Object.entries(sideDims ?? {})) {
|
||||
if (!finite(v)) throw new Error(`${who}: 날개벽 치수 ${key}이(가) 숫자가 아닙니다.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.wingsLinked !== undefined && typeof a.wingsLinked !== 'boolean') throw new Error(`${who}: 좌우 같게 값이 맞지 않습니다.`);
|
||||
if (a.wings !== undefined) {
|
||||
const forms = ['general', 'short', 'none'];
|
||||
if (!forms.includes(a.wings.left) || !forms.includes(a.wings.right)) throw new Error(`${who}: 날개벽 형식이 맞지 않습니다.`);
|
||||
|
||||
Reference in new issue
Block a user