Merge pull request '[1840]생성되지 않은 팔작지붕 도면 사례' (#751) from dev into dev-deploy

Reviewed-on: #751
This commit is contained in:
ysCha 2026-04-02 14:47:12 +09:00
commit b4bf1a8640
2 changed files with 42 additions and 8 deletions

View File

@ -253,13 +253,12 @@ export default class SkeletonBuilder {
} }
private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) { private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) {
const beginEdge2 = beginNextVertex.PreviousEdge; // Use the vertex's actual edges — chain edges can become stale
const endEdge2 = endPreviousVertex.NextEdge; // when previous iterations of MultiSplitEvent modify the LAV
const actualBeginEdge = beginNextVertex.PreviousEdge;
const actualEndEdge = endPreviousVertex.NextEdge;
if (beginEdge !== beginEdge2 || endEdge !== endEdge2) if (actualBeginEdge.Norm.Dot(actualEndEdge.Norm) < -0.97) {
throw new Error();
if (beginEdge.Norm.Dot(endEdge.Norm) < -0.97) {
const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized(); const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized();
const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized(); const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized();
const bisectorPrediction = this.CalcVectorBisector(n1, n2); const bisectorPrediction = this.CalcVectorBisector(n1, n2);
@ -887,7 +886,7 @@ export default class SkeletonBuilder {
private static GetEdgeInLav(lav: CircularList<Vertex>, oppositeEdge: Edge): Vertex { private static GetEdgeInLav(lav: CircularList<Vertex>, oppositeEdge: Edge): Vertex {
for (const node of lav.Iterate()) for (const node of lav.Iterate())
if (oppositeEdge === node.PreviousEdge || if (oppositeEdge === node.PreviousEdge ||
oppositeEdge === node.Previous.Next) oppositeEdge === (node.Previous as Vertex).NextEdge)
return node; return node;
return null; return null;

View File

@ -16,6 +16,39 @@ import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine
*/ */
const EPSILON = 0.1 const EPSILON = 0.1
/**
* 오목(concave) 폴리곤인지 판별합니다.
* cross product의 부호가 혼재하면 오목 폴리곤입니다.
*/
const isConcavePolygon = (points) => {
if (!points || points.length < 4) return false
let positive = 0, negative = 0
for (let i = 0; i < points.length; i++) {
const prev = points[(i - 1 + points.length) % points.length]
const curr = points[i]
const next = points[(i + 1) % points.length]
const cross = (curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x)
if (cross > 0) positive++
else if (cross < 0) negative++
}
return positive > 0 && negative > 0
}
/**
* 오목 폴리곤에만 미세한 perturbation을 적용하여 동시 이벤트(MultiSplitEvent) 방지합니다.
* 대칭적 길이(: 45.5, 45.5) 동일 거리의 이벤트를 만들어 LAV 불일치 오류를 유발하므로
* 꼭짓점에 인덱스 기반 미세 오프셋을 적용하면 이벤트가 개별 SplitEvent로 처리됩니다.
* 볼록 폴리곤은 문제가 없으므로 원본을 그대로 반환합니다.
*/
const perturbPolygonPoints = (points, eps = 1e-4) => {
if (!points || points.length < 3) return points
if (!isConcavePolygon(points)) return points
return points.map((p, i) => ({
x: p.x + (i + 1) * eps,
y: p.y + (i + 1) * eps
}))
}
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
// 2. 스켈레톤 생성 및 그리기 // 2. 스켈레톤 생성 및 그리기
@ -503,11 +536,13 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지) // changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y })) roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
const geoJSONPolygon = toGeoJSON(changRoofLinePoints) const perturbedPoints = perturbPolygonPoints(changRoofLinePoints)
const geoJSONPolygon = toGeoJSON(perturbedPoints)
try { try {
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop() geoJSONPolygon.pop()
console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성 // 스켈레톤 데이터를 기반으로 내부선 생성