feat(core): lateral coping step (Q1·Q2) and IP4 bend-point solve (spec §4.1)
- Front and back coping tops that differ by 5 mm or more in any segment are drawn as two coping parts split at the midline between the bearing rows. Tolerance 5 mm and midline position are the draft assumptions (Q1·Q2). - Scene extrudes each coping part across its own width. - solveIp4 computes IP4 as the intersection of the IP1→IP2 line with the line through IP3 turned by the bend angle (positive = counter-clockwise, sign convention to be confirmed). Bent coping geometry remains 미실시. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad59c0c74d
commit
d0fa9b94eb
4 files changed
+118
-26
No files matched your search
+47
-16
@@ -84,6 +84,13 @@ export interface PileModel {
|
||||
diameter: number;
|
||||
}
|
||||
|
||||
/** One coping slab across [nFrom, nTo]; profile is its (s, EL) outline, closed. */
|
||||
export interface CopingPart {
|
||||
nFrom: number;
|
||||
nTo: number;
|
||||
profile: [number, number][];
|
||||
}
|
||||
|
||||
export interface PierModel {
|
||||
name: string;
|
||||
lengthIp: number;
|
||||
@@ -106,8 +113,8 @@ export interface PierModel {
|
||||
h1: number;
|
||||
h2: number;
|
||||
segments: Segment[];
|
||||
/** Coping elevation outline as (s, EL) points, closed. */
|
||||
profile: [number, number][];
|
||||
/** Coping split across the width (n range) by the lateral step line; one part when there is no step. */
|
||||
copingParts: CopingPart[];
|
||||
levelBottom: number;
|
||||
endBottom: number;
|
||||
bearings: BearingLocal[];
|
||||
@@ -129,6 +136,22 @@ export interface PierModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bend point IP4 (spec §4.1-2): the point where the IP1→IP2 line meets the line through IP3 whose
|
||||
* direction is IP1→IP2 turned by the bend angle. Positive angle = counter-clockwise [확인 필요: 부호 규약].
|
||||
* Returns null for a zero angle, where IP4 is not defined.
|
||||
*/
|
||||
export function solveIp4(ip1: Vec2, ip2: Vec2, ip3: Vec2, angleDeg: number): Vec2 | null {
|
||||
if (angleDeg === 0) return null;
|
||||
const cross = (a: Vec2, b: Vec2) => a.e * b.n - a.n * b.e;
|
||||
const len = Math.hypot(ip2.e - ip1.e, ip2.n - ip1.n);
|
||||
const d1: Vec2 = { e: (ip2.e - ip1.e) / len, n: (ip2.n - ip1.n) / len };
|
||||
const rad = (angleDeg * Math.PI) / 180;
|
||||
const d2: Vec2 = { e: d1.e * Math.cos(rad) - d1.n * Math.sin(rad), n: d1.e * Math.sin(rad) + d1.n * Math.cos(rad) };
|
||||
const t = cross({ e: ip3.e - ip1.e, n: ip3.n - ip1.n }, d2) / cross(d1, d2);
|
||||
return { e: ip1.e + t * d1.e, n: ip1.n + t * d1.n };
|
||||
}
|
||||
|
||||
function toLocal(p: Vec2, origin: Vec2, u: Vec2, nrm: Vec2): { s: number; n: number } {
|
||||
const dx = p.e - origin.e;
|
||||
const dy = p.n - origin.n;
|
||||
@@ -150,7 +173,7 @@ export function buildPier(input: PierInput): PierModel {
|
||||
const ip3 = ip('IP3');
|
||||
const ip4 = ip('IP4');
|
||||
if (ip4.angleDeg !== 0) {
|
||||
throw new Error('미실시: 꺾인 코핑(IP4 꺾임각 ≠ 0)은 아직 지원하지 않습니다.');
|
||||
throw new Error('미실시: 꺾인 코핑(IP4 꺾임각 ≠ 0)의 형상은 아직 지원하지 않습니다. IP4 계산식(solveIp4)은 구현되어 있습니다.');
|
||||
}
|
||||
|
||||
const dx = ip3.x - ip1.x;
|
||||
@@ -166,6 +189,7 @@ export function buildPier(input: PierInput): PierModel {
|
||||
|
||||
const L = input.copingLengthMm / 1000;
|
||||
const B = input.copingWidthMm / 1000;
|
||||
const planHalfB = B / 2;
|
||||
const ip1v: Vec2 = { e: ip1.x, n: ip1.y };
|
||||
const bp1: Vec2 = { e: ip1v.e + u.e * (lengthIp / 2 - L / 2), n: ip1v.n + u.n * (lengthIp / 2 - L / 2) };
|
||||
const bp2: Vec2 = { e: bp1.e + u.e * L, n: bp1.n + u.n * L };
|
||||
@@ -190,13 +214,9 @@ export function buildPier(input: PierInput): PierModel {
|
||||
bounds.push(L);
|
||||
const segments: Segment[] = frontLocal.map((b, k) => ({ s0: bounds[k], s1: bounds[k + 1], top: b.z - h1 }));
|
||||
|
||||
// Lateral step: front and back coping tops must match segment by segment (not supported yet).
|
||||
frontLocal.forEach((f, k) => {
|
||||
const backTop = backLocal[k].z - h2;
|
||||
if (Math.abs(segments[k].top - backTop) > DIM.lateralTolerance) {
|
||||
throw new Error('미실시: 횡방향 단차(전열·후열 상면 차 ≥ 5 mm)는 아직 지원하지 않습니다.');
|
||||
}
|
||||
});
|
||||
// Lateral step (spec §4.3, Q1·Q2 추천안): front and back tops differ by 5 mm or more in some segment.
|
||||
const backTops = backLocal.map((b) => b.z - h2);
|
||||
const lateralStep = backTops.some((top, k) => Math.abs(segments[k].top - top) >= DIM.lateralTolerance);
|
||||
|
||||
// Columns (spec §4.5). Multi-column positions are placed; haunch and separate-footing checks are not.
|
||||
const colS = columnPositions(input.type, L);
|
||||
@@ -220,11 +240,22 @@ export function buildPier(input: PierInput): PierModel {
|
||||
underside.push([zoneStart - DIM.haunch, lowerEl], [next[1] + DIM.haunch, lowerEl], [next[1], levelBottom]);
|
||||
});
|
||||
|
||||
const profile: [number, number][] = [[0, endBottom]];
|
||||
for (const seg of segments) {
|
||||
profile.push([seg.s0, seg.top], [seg.s1, seg.top]);
|
||||
}
|
||||
profile.push(...underside);
|
||||
// Elevation outline for one row of tops, closed by the shared underside.
|
||||
const outline = (tops: number[]): [number, number][] => {
|
||||
const pts: [number, number][] = [[0, endBottom]];
|
||||
segments.forEach((seg, k) => pts.push([seg.s0, tops[k]], [seg.s1, tops[k]]));
|
||||
pts.push(...underside);
|
||||
return pts;
|
||||
};
|
||||
// The lateral step line lies midway between the front and back bearing rows.
|
||||
const meanN = (rows: BearingLocal[]) => rows.reduce((sum, b) => sum + b.n, 0) / rows.length;
|
||||
const frontTops = segments.map((seg) => seg.top);
|
||||
const copingParts: CopingPart[] = lateralStep
|
||||
? [
|
||||
{ nFrom: -planHalfB, nTo: (meanN(frontLocal) + meanN(backLocal)) / 2, profile: outline(frontTops) },
|
||||
{ nFrom: (meanN(frontLocal) + meanN(backLocal)) / 2, nTo: planHalfB, profile: outline(backTops) },
|
||||
]
|
||||
: [{ nFrom: -planHalfB, nTo: planHalfB, profile: outline(frontTops) }];
|
||||
|
||||
const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + DIM.footingH;
|
||||
const columns: ColumnModel[] = colS.map((s) => ({
|
||||
@@ -294,7 +325,7 @@ export function buildPier(input: PierInput): PierModel {
|
||||
h1,
|
||||
h2,
|
||||
segments,
|
||||
profile,
|
||||
copingParts,
|
||||
levelBottom,
|
||||
endBottom,
|
||||
bearings: [...frontLocal, ...backLocal].map((b) => ({ no: b.no, row: b.row, s: b.s, n: b.n, z: b.z })),
|
||||
|
||||
@@ -6,6 +6,7 @@ const EXPECTED_COLUMNS: Record<PierType, number> = { 'T형': 1, '2주형': 2, '3
|
||||
/** Rules from spec §10 that are not implemented in this stage. Shown to the user, never silently skipped. */
|
||||
export const NOT_IMPLEMENTED_RULES = [
|
||||
'E04 fck 미입력 (배근 단계, 3단계)',
|
||||
'꺾인 코핑 형상 (IP4 꺾임각 ≠ 0; IP4 계산식만 구현)',
|
||||
'W06 속성 복사 건너뜀 (2단계)',
|
||||
'W08 솔플레이트 T1−T2 검사 (2단계)',
|
||||
] as const;
|
||||
|
||||
+7
-5
@@ -65,11 +65,13 @@ export function buildPierScene(m: PierModel): PierScene {
|
||||
parts[part].add(new THREE.Mesh(geometry, material));
|
||||
};
|
||||
|
||||
// Coping: the elevation outline extruded across the width (no boolean operations).
|
||||
const shape = new THREE.Shape(m.profile.map(([s, z]) => new THREE.Vector2(s, z)));
|
||||
const coping = new THREE.ExtrudeGeometry(shape, { depth: m.copingB, bevelEnabled: false });
|
||||
coping.translate(0, 0, -m.copingB / 2);
|
||||
add('coping', coping);
|
||||
// Coping: each part's elevation outline extruded across its width (no boolean operations).
|
||||
for (const part of m.copingParts) {
|
||||
const shape = new THREE.Shape(part.profile.map(([s, z]) => new THREE.Vector2(s, z)));
|
||||
const coping = new THREE.ExtrudeGeometry(shape, { depth: part.nTo - part.nFrom, bevelEnabled: false });
|
||||
coping.translate(0, 0, part.nFrom);
|
||||
add('coping', coping);
|
||||
}
|
||||
|
||||
for (const c of m.columns) {
|
||||
const g = new THREE.CylinderGeometry(c.diameter / 2, c.diameter / 2, c.top - c.bottom, 48);
|
||||
|
||||
Reference in new issue
Block a user