fix(abut): 날개벽·헌치·접속슬래브가 벽체와 흉벽 안으로 들어가지 않도록
- 날개벽: 벽체 배면(t −0.8) 뒤로만 세움. 벽체·흉벽 앞쪽 부분은 잘라냄 (프로파일을 dM ≥ 벽체 배면 위치로 자름) - 위 헌치: 브라켓 상면이 s 방향으로 기울므로, 헌치 면적 안에서 가장 높은 브라켓 상면 위에 놓음 - 접속슬래브: 흉벽 배면 단차 위(t ≥ 벽체 배면)는 브라켓 상면에 평평하게 얹고, 벽체 뒤쪽에서만 4 % 하향 - 테스트: 날개벽 t ≤ −0.8, 벽체 배면 위치, 위 헌치와 슬래브가 배면 단차 아래로 내려가지 않음. 전체 638건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b425a6010b
commit
7fab9b6685
4 files changed
+54
-6
No files matched your search
@@ -110,14 +110,18 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
|
||||
const pointAt = (d: number) => ({ e: p0.e + w.e * d, n: p0.n + w.n * d });
|
||||
const shift = (q: AbutPoint, v: AbutPoint, f: number) => ({ e: q.e + v.e * f, n: q.n + v.n * f });
|
||||
const bracketTop = topStart - members.shape.bracketHeightM;
|
||||
// The 브라켓 상면 falls along s (the 흉벽 top slopes), so the upper haunch rests on the highest point of its own footprint.
|
||||
const sAt = (q: AbutPoint) => (q.e - model.ip1.e) * model.u.e + (q.n - model.ip1.n) * model.u.n;
|
||||
const backTopAt = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM;
|
||||
const haunchAt = (kind: 'upper' | 'lower', t: number): Haunch => {
|
||||
const d = Math.abs(t) / k;
|
||||
const c = pointAt(d);
|
||||
const inwardUnit = { e: model.u.e * a, n: model.u.n * a };
|
||||
const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)];
|
||||
return {
|
||||
kind,
|
||||
corners: [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)],
|
||||
zBottomEl: kind === 'upper' ? bracketTop : footingTop,
|
||||
corners,
|
||||
zBottomEl: kind === 'upper' ? Math.max(...corners.map((q) => backTopAt(sAt(q)))) : footingTop,
|
||||
zTopEl: kind === 'upper' ? topAt(d) : bracketTop,
|
||||
};
|
||||
};
|
||||
|
||||
+19
-2
@@ -79,6 +79,21 @@ function toAxis(model: AbutModel, p: AbutPoint): { s: number; t: number } {
|
||||
return { s: d.e * model.u.e + d.n * model.u.n, t: d.e * front.e + d.n * front.n };
|
||||
}
|
||||
|
||||
/** Keeps the part of a 2-D profile with x ≥ x0 (Sutherland–Hodgman against one line). */
|
||||
function clipBelowX(poly: [number, number][], x0: number): [number, number][] {
|
||||
const out: [number, number][] = [];
|
||||
for (let i = 0; i < poly.length; i++) {
|
||||
const p = poly[i];
|
||||
const q = poly[(i + 1) % poly.length];
|
||||
if (p[0] >= x0) out.push(p);
|
||||
if ((p[0] - x0) * (q[0] - x0) < 0) {
|
||||
const y = p[1] + ((q[1] - p[1]) * (x0 - p[0])) / (q[0] - p[0]);
|
||||
out.push([x0, y]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function wingDirectionAxis(model: AbutModel): { ws: number; wt: number } {
|
||||
const rad = ((model.wing?.lineDeg ?? 0) * Math.PI) / 180;
|
||||
const w = { e: Math.cos(rad), n: Math.sin(rad) };
|
||||
@@ -207,7 +222,8 @@ export function buildAbutGroup(
|
||||
[innerS(L, slabFar), slabFar],
|
||||
[innerS(0, slabFar), slabFar],
|
||||
],
|
||||
(s, t) => bracketTopEl(s) + SLAB_FALL * (t - slabNear),
|
||||
// Flat on the bracket where it lies over the back strip (t ≥ 벽체 배면), falling 4 % only behind the wall.
|
||||
(s, t) => bracketTopEl(s) - SLAB_FALL * Math.max(0, members.shape.wallBackM - t),
|
||||
),
|
||||
new THREE.MeshStandardMaterial({ color: 0xd8c8a0, roughness: 0.9, side: THREE.DoubleSide }),
|
||||
);
|
||||
@@ -239,7 +255,8 @@ export function buildAbutGroup(
|
||||
...[...wing.bottomLine].reverse().map((p) => [p.dM, p.el] as [number, number]),
|
||||
];
|
||||
const ax = new THREE.Vector3(dir.ws, 0, dir.wt);
|
||||
const geom = prism(profile, 0.45, new THREE.Vector3(sEnd, 0, 0), ax, ey, new THREE.Vector3(a, 0, 0));
|
||||
// The wing starts behind the 벽체 back face (t = 벽체 배면 −0.8): the part in front of it lies inside the 벽체 and 흉벽, so it is cut off.
|
||||
const geom = prism(clipBelowX(profile, members.shape.wallBackM / dir.wt), 0.45, new THREE.Vector3(sEnd, 0, 0), ax, ey, new THREE.Vector3(a, 0, 0));
|
||||
add('wings', geom, 0xc9a77c);
|
||||
for (const h of wing.haunches) {
|
||||
const shape: [number, number][] = h.corners.map((c) => {
|
||||
|
||||
@@ -68,4 +68,29 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () =>
|
||||
const backs = (named('wall') as THREE.Group).children.map((w) => new THREE.Box3().setFromObject(w).min.z);
|
||||
expect(Math.min(...backs)).toBeCloseTo(members.shape.wallBackM, 6);
|
||||
});
|
||||
|
||||
it('the 날개벽 stays behind the 벽체 back face, so it does not enter the 벽체 or the 흉벽', () => {
|
||||
// All 벽체 and 흉벽 parts lie at t ≥ 벽체 배면 (−0.8). A wing vertex with t ≤ −0.8 therefore has no volume in common with them.
|
||||
const z = (g: THREE.Object3D) => {
|
||||
const pos = (g as THREE.Mesh).geometry.getAttribute('position');
|
||||
let max = -Infinity;
|
||||
for (let i = 0; i < pos.count; i++) max = Math.max(max, pos.getZ(i));
|
||||
return max;
|
||||
};
|
||||
for (const w of (named('wings') as THREE.Group).children) expect(z(w)).toBeLessThanOrEqual(members.shape.wallBackM + 1e-6);
|
||||
for (const w of (named('wall') as THREE.Group).children) expect(new THREE.Box3().setFromObject(w).min.z).toBeGreaterThanOrEqual(members.shape.wallBackM - 1e-6);
|
||||
});
|
||||
|
||||
it('the upper 헌치 and the 접속슬래브 stay on the 브라켓 상면 over the back strip, not in it', () => {
|
||||
const backTop = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM;
|
||||
for (const h of (named('haunches') as THREE.Group).children) {
|
||||
const b = new THREE.Box3().setFromObject(h);
|
||||
if (b.max.z < members.shape.parapetBackM + 1e-6) continue; // lower 헌치, behind the 벽체
|
||||
expect(b.min.y).toBeGreaterThanOrEqual(Math.max(backTop(b.min.x), backTop(b.max.x)) - 1e-6);
|
||||
}
|
||||
const pos = ((named('slab') as THREE.Group).children[0] as THREE.Mesh).geometry.getAttribute('position');
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getZ(i) >= members.shape.wallBackM) expect(pos.getY(i)).toBeGreaterThanOrEqual(backTop(pos.getX(i)) - 1e-6);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -126,9 +126,11 @@ describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () =>
|
||||
|
||||
it('upper haunch runs from the bracket top (흉벽 상단 − 0.85) to the wing top; lower from the footing top to the bracket top', () => {
|
||||
const [up, low] = a2Left.haunches;
|
||||
expect(up.zBottomEl).toBeCloseTo(46.838 - 0.85, 2);
|
||||
// The upper haunch rests on the highest point of the 브라켓 top over its footprint (the top falls along s), never below it.
|
||||
expect(up.zBottomEl).toBeGreaterThanOrEqual(46.838 - 0.85 - 1e-6);
|
||||
expect(up.zBottomEl).toBeLessThan(46.838 - 0.85 + 0.05);
|
||||
expect(low.zBottomEl).toBeCloseTo(41.995, 3);
|
||||
expect(low.zTopEl).toBeCloseTo(up.zBottomEl, 6);
|
||||
expect(low.zTopEl).toBeCloseTo(46.838 - 0.85, 2);
|
||||
expect(up.zTopEl).toBeGreaterThan(up.zBottomEl);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user