Merge pull request 'dev' (#760) from dev into prd-deploy
Reviewed-on: #760
This commit is contained in:
commit
680ea847e9
@ -13,8 +13,8 @@ SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
|
|||||||
NEXT_PUBLIC_CONVERTER_DWG_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_a0FLEK6M2oTpXInK"
|
NEXT_PUBLIC_CONVERTER_DWG_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_a0FLEK6M2oTpXInK"
|
||||||
NEXT_PUBLIC_CONVERTER_DXF_API_URL="https://v2.convertapi.com/convert/dxf/to/png?Secret=secret_a0FLEK6M2oTpXInK"
|
NEXT_PUBLIC_CONVERTER_DXF_API_URL="https://v2.convertapi.com/convert/dxf/to/png?Secret=secret_a0FLEK6M2oTpXInK"
|
||||||
|
|
||||||
NEXT_PUBLIC_Q_ORDER_AUTO_LOGIN_URL="http://q-order-stg.q-cells.jp:8120/eos/login/autoLogin"
|
NEXT_PUBLIC_Q_ORDER_AUTO_LOGIN_URL="http://q-order-dev.q-cells.jp:8120/eos/login/autoLogin"
|
||||||
NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="http://q-musubi-stg.q-cells.jp:8120/qm/login/autoLogin"
|
NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="http://q-musubi-dev.q-cells.jp:8120/qm/login/autoLogin"
|
||||||
|
|
||||||
# 테스트용
|
# 테스트용
|
||||||
# AWS_REGION="ap-northeast-2"
|
# AWS_REGION="ap-northeast-2"
|
||||||
|
|||||||
@ -1718,14 +1718,22 @@ export function useMode() {
|
|||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const next = (i + 1) % n
|
const next = (i + 1) % n
|
||||||
// 인접한 두 edge가 같은 방향이면 겹치는 선
|
// 인접한 두 edge가 같은 방향이면 두 edge 를 하나로 합칠 수 있다.
|
||||||
|
// 이 때 제거해야 하는 정점은 두 edge 사이의 "junction" 정점 = vertex[next].
|
||||||
|
// (vertex[i] 는 edge[i-1] 의 끝이자 edge[i] 의 시작으로, 실제 코너일 수 있으므로 제거하면 안 됨.)
|
||||||
if (isSameDirection(edges[i].angle, edges[next].angle)) {
|
if (isSameDirection(edges[i].angle, edges[next].angle)) {
|
||||||
// 짧은 쪽을 제거 대상으로 표시
|
removeIndices.add(next)
|
||||||
if (edges[i].length <= edges[next].length) {
|
}
|
||||||
removeIndices.add(i)
|
}
|
||||||
} else {
|
|
||||||
removeIndices.add(next)
|
// 절대 길이가 너무 짧은 edge 는 inset offset (~20) 대비 불안정하므로 제거.
|
||||||
}
|
// edge[i] 가 tiny 이면 시작 정점 vertex[i] 를 제거해 앞쪽 edge 와 병합한다.
|
||||||
|
// 이렇게 하면 남는 cleaned edge 의 line 매핑(cleanedLines[k]=lines[kept[k]]) 이
|
||||||
|
// edge[i+1] (실제 긴 edge) 의 line 을 쓰게 되므로 offset 계산이 안정해진다.
|
||||||
|
const TINY_EDGE_THRESHOLD = 30
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (edges[i].length < TINY_EDGE_THRESHOLD) {
|
||||||
|
removeIndices.add(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1747,6 +1755,25 @@ export function useMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createMarginPolygon(polygon, lines, arcSegments = 0, bevelJoin = false) {
|
function createMarginPolygon(polygon, lines, arcSegments = 0, bevelJoin = false) {
|
||||||
|
// 폴리곤의 winding 방향 보정: inwardEdgeNormal 이 CW (signedArea > 0) 가정이므로
|
||||||
|
// CCW 폴리곤은 vertex/lines 순서를 뒤집어 다시 createRoofPolygon 한 뒤 진행한다.
|
||||||
|
const rawSignedArea = polygon.edges.reduce(
|
||||||
|
(sum, edge) => sum + (edge.vertex1.x * edge.vertex2.y - edge.vertex2.x * edge.vertex1.y),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if (rawSignedArea < 0) {
|
||||||
|
const reversedVertices = [...polygon.vertices].reverse()
|
||||||
|
const reversedLines = []
|
||||||
|
const n = lines.length
|
||||||
|
// lines[i] 는 edge i (vertex[i] → vertex[i+1]) 에 해당. reverse 후의 edge j 는
|
||||||
|
// 원본 edge (n - 1 - j) 와 같은 선분이므로 lines 배열도 동일 매핑으로 재정렬.
|
||||||
|
for (let j = 0; j < n; j++) {
|
||||||
|
reversedLines.push(lines[(n - 1 - j) % n])
|
||||||
|
}
|
||||||
|
polygon = createRoofPolygon(reversedVertices)
|
||||||
|
lines = reversedLines
|
||||||
|
}
|
||||||
|
|
||||||
const offsetEdges = []
|
const offsetEdges = []
|
||||||
|
|
||||||
polygon.edges.forEach((edge, i) => {
|
polygon.edges.forEach((edge, i) => {
|
||||||
@ -1756,37 +1783,16 @@ export function useMode() {
|
|||||||
offsetEdges.push(createOffsetEdge(edge, dx, dy))
|
offsetEdges.push(createOffsetEdge(edge, dx, dy))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 폴리곤의 감김 방향(signed area)을 구한다
|
|
||||||
const signedArea = bevelJoin
|
|
||||||
? polygon.edges.reduce((sum, edge) => sum + (edge.vertex1.x * edge.vertex2.y - edge.vertex2.x * edge.vertex1.y), 0)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const vertices = []
|
const vertices = []
|
||||||
|
|
||||||
offsetEdges.forEach((thisEdge, i) => {
|
offsetEdges.forEach((thisEdge, i) => {
|
||||||
const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length]
|
const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length]
|
||||||
const vertex = edgesIntersection(prevEdge, thisEdge)
|
const vertex = edgesIntersection(prevEdge, thisEdge)
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
if (!vertex.isIntersectionOutside || !bevelJoin) {
|
// 볼록/오목 관계없이 두 offset edge 의 무한 직선 교점이 올바른 inset 꼭지점.
|
||||||
vertices.push({ x: vertex.x, y: vertex.y })
|
// isIntersectionOutside 는 offset "선분" 기준 flag 일 뿐이고, self-intersection(스파이크)
|
||||||
} else {
|
// 은 이후 cleanSelfIntersectingPolygon 과 clipOffsetToOriginal 에서 처리한다.
|
||||||
// 꼭짓점의 볼록/오목 판별 (cross product와 signed area 비교)
|
vertices.push({ x: vertex.x, y: vertex.y })
|
||||||
const origPrev = polygon.edges[(i + polygon.edges.length - 1) % polygon.edges.length]
|
|
||||||
const origThis = polygon.edges[i]
|
|
||||||
const cross =
|
|
||||||
(origPrev.vertex2.x - origPrev.vertex1.x) * (origThis.vertex2.y - origThis.vertex1.y) -
|
|
||||||
(origPrev.vertex2.y - origPrev.vertex1.y) * (origThis.vertex2.x - origThis.vertex1.x)
|
|
||||||
const isConvex = cross * signedArea > 0
|
|
||||||
|
|
||||||
if (isConvex) {
|
|
||||||
// 볼록 꼭짓점: bevel 적용 (offset edge 끝점 사용)
|
|
||||||
vertices.push({ x: prevEdge.vertex2.x, y: prevEdge.vertex2.y })
|
|
||||||
vertices.push({ x: thisEdge.vertex1.x, y: thisEdge.vertex1.y })
|
|
||||||
} else {
|
|
||||||
// 오목 꼭짓점: 교차점 사용 (90도 유지)
|
|
||||||
vertices.push({ x: vertex.x, y: vertex.y })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1803,6 +1809,23 @@ export function useMode() {
|
|||||||
* @returns {{vertices, edges: *[], minX: *, minY: *, maxX: *, maxY: *}}
|
* @returns {{vertices, edges: *[], minX: *, minY: *, maxX: *, maxY: *}}
|
||||||
*/
|
*/
|
||||||
function createPaddingPolygon(polygon, lines, arcSegments = 0, bevelJoin = false) {
|
function createPaddingPolygon(polygon, lines, arcSegments = 0, bevelJoin = false) {
|
||||||
|
// 폴리곤의 winding 방향 보정: inwardEdgeNormal 이 CW (signedArea > 0) 가정이므로
|
||||||
|
// CCW 폴리곤은 vertex/lines 순서를 뒤집어 다시 createRoofPolygon 한 뒤 진행한다.
|
||||||
|
const rawSignedArea = polygon.edges.reduce(
|
||||||
|
(sum, edge) => sum + (edge.vertex1.x * edge.vertex2.y - edge.vertex2.x * edge.vertex1.y),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if (rawSignedArea < 0) {
|
||||||
|
const reversedVertices = [...polygon.vertices].reverse()
|
||||||
|
const reversedLines = []
|
||||||
|
const n = lines.length
|
||||||
|
for (let j = 0; j < n; j++) {
|
||||||
|
reversedLines.push(lines[(n - 1 - j) % n])
|
||||||
|
}
|
||||||
|
polygon = createRoofPolygon(reversedVertices)
|
||||||
|
lines = reversedLines
|
||||||
|
}
|
||||||
|
|
||||||
const offsetEdges = []
|
const offsetEdges = []
|
||||||
|
|
||||||
polygon.edges.forEach((edge, i) => {
|
polygon.edges.forEach((edge, i) => {
|
||||||
@ -1813,37 +1836,16 @@ export function useMode() {
|
|||||||
offsetEdges.push(createOffsetEdge(edge, dx, dy))
|
offsetEdges.push(createOffsetEdge(edge, dx, dy))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 폴리곤의 감김 방향(signed area)을 구한다
|
|
||||||
const signedArea = bevelJoin
|
|
||||||
? polygon.edges.reduce((sum, edge) => sum + (edge.vertex1.x * edge.vertex2.y - edge.vertex2.x * edge.vertex1.y), 0)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const vertices = []
|
const vertices = []
|
||||||
|
|
||||||
offsetEdges.forEach((thisEdge, i) => {
|
offsetEdges.forEach((thisEdge, i) => {
|
||||||
const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length]
|
const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length]
|
||||||
const vertex = edgesIntersection(prevEdge, thisEdge)
|
const vertex = edgesIntersection(prevEdge, thisEdge)
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
if (!vertex.isIntersectionOutside || !bevelJoin) {
|
// 볼록/오목 관계없이 두 offset edge 의 무한 직선 교점이 올바른 inset 꼭지점.
|
||||||
vertices.push({ x: vertex.x, y: vertex.y })
|
// isIntersectionOutside 는 offset "선분" 기준 flag 일 뿐이고, self-intersection(스파이크)
|
||||||
} else {
|
// 은 이후 cleanSelfIntersectingPolygon 과 clipOffsetToOriginal 에서 처리한다.
|
||||||
// 꼭짓점의 볼록/오목 판별 (cross product와 signed area 비교)
|
vertices.push({ x: vertex.x, y: vertex.y })
|
||||||
const origPrev = polygon.edges[(i + polygon.edges.length - 1) % polygon.edges.length]
|
|
||||||
const origThis = polygon.edges[i]
|
|
||||||
const cross =
|
|
||||||
(origPrev.vertex2.x - origPrev.vertex1.x) * (origThis.vertex2.y - origThis.vertex1.y) -
|
|
||||||
(origPrev.vertex2.y - origPrev.vertex1.y) * (origThis.vertex2.x - origThis.vertex1.x)
|
|
||||||
const isConvex = cross * signedArea > 0
|
|
||||||
|
|
||||||
if (isConvex) {
|
|
||||||
// 볼록 꼭짓점: bevel 적용 (offset edge 끝점 사용)
|
|
||||||
vertices.push({ x: prevEdge.vertex2.x, y: prevEdge.vertex2.y })
|
|
||||||
vertices.push({ x: thisEdge.vertex1.x, y: thisEdge.vertex1.y })
|
|
||||||
} else {
|
|
||||||
// 오목 꼭짓점: 교차점 사용 (90도 유지)
|
|
||||||
vertices.push({ x: vertex.x, y: vertex.y })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -256,8 +256,11 @@ function createPaddingPolygon(polygon, offset, arcSegments = 0) {
|
|||||||
* @returns {Array} 정리된 점 배열
|
* @returns {Array} 정리된 점 배열
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* offset 폴리곤의 꼭짓점 중 원본 폴리곤 바깥에 나간 점을
|
* offset 폴리곤을 원본 폴리곤 경계 안으로 클리핑한다.
|
||||||
* 원본 폴리곤 경계 위의 가장 가까운 점으로 투영한다.
|
* 삼각형/예각 꼭짓점 등에서 bevel 때문에 offset 꼭짓점이 원본 바깥으로
|
||||||
|
* 튀어나가는 경우, 점별 투영이 아니라 turf.intersect 로 면 단위 클리핑을 수행한다.
|
||||||
|
* 점별 투영은 여러 점이 원본 바깥으로 나가면 각 점이 제각각 가까운 변으로
|
||||||
|
* 스냅되어 폴리곤이 직사각형처럼 찌그러지는 문제가 있었음.
|
||||||
* @param {Array} offsetVertices - offset된 점 배열 [{x, y}, ...]
|
* @param {Array} offsetVertices - offset된 점 배열 [{x, y}, ...]
|
||||||
* @param {Array} originalVertices - 원본 폴리곤 점 배열 [{x, y}, ...]
|
* @param {Array} originalVertices - 원본 폴리곤 점 배열 [{x, y}, ...]
|
||||||
* @returns {Array} 클리핑된 점 배열
|
* @returns {Array} 클리핑된 점 배열
|
||||||
@ -266,14 +269,15 @@ export function clipOffsetToOriginal(offsetVertices, originalVertices) {
|
|||||||
if (!offsetVertices || offsetVertices.length < 3) return offsetVertices
|
if (!offsetVertices || offsetVertices.length < 3) return offsetVertices
|
||||||
if (!originalVertices || originalVertices.length < 3) return offsetVertices
|
if (!originalVertices || originalVertices.length < 3) return offsetVertices
|
||||||
|
|
||||||
// 점이 폴리곤 내부에 있는지 판단 (ray casting)
|
// 점이 폴리곤 내부(또는 경계)에 있는지 판단 (ray casting)
|
||||||
function pointInPolygon(point, polygon) {
|
const _pointInPolygon = (point, polygon) => {
|
||||||
let inside = false
|
let inside = false
|
||||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
const xi = polygon[i].x, yi = polygon[i].y
|
const xi = polygon[i].x,
|
||||||
const xj = polygon[j].x, yj = polygon[j].y
|
yi = polygon[i].y
|
||||||
if (((yi > point.y) !== (yj > point.y)) &&
|
const xj = polygon[j].x,
|
||||||
(point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi)) {
|
yj = polygon[j].y
|
||||||
|
if (yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi) {
|
||||||
inside = !inside
|
inside = !inside
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,7 +285,7 @@ export function clipOffsetToOriginal(offsetVertices, originalVertices) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 점을 선분 위의 가장 가까운 점으로 투영
|
// 점을 선분 위의 가장 가까운 점으로 투영
|
||||||
function projectOnSegment(p, a, b) {
|
const _projectOnSegment = (p, a, b) => {
|
||||||
const dx = b.x - a.x
|
const dx = b.x - a.x
|
||||||
const dy = b.y - a.y
|
const dy = b.y - a.y
|
||||||
const lenSq = dx * dx + dy * dy
|
const lenSq = dx * dx + dy * dy
|
||||||
@ -291,24 +295,79 @@ export function clipOffsetToOriginal(offsetVertices, originalVertices) {
|
|||||||
return { x: a.x + t * dx, y: a.y + t * dy }
|
return { x: a.x + t * dx, y: a.y + t * dy }
|
||||||
}
|
}
|
||||||
|
|
||||||
return offsetVertices.map((p) => {
|
// 최종 결과 정점을 원본 폴리곤 경계 안으로 스냅하는 post-process.
|
||||||
if (pointInPolygon(p, originalVertices)) return p
|
// 부동소수점 누적/tiny edge 때문에 아주 미세하게 바깥에 있는 점만 복원한다.
|
||||||
|
const _snapToBoundary = (verts) => {
|
||||||
|
return verts.map((p) => {
|
||||||
|
if (_pointInPolygon(p, originalVertices)) return p
|
||||||
|
let minDist = Infinity
|
||||||
|
let nearest = p
|
||||||
|
for (let i = 0; i < originalVertices.length; i++) {
|
||||||
|
const a = originalVertices[i]
|
||||||
|
const b = originalVertices[(i + 1) % originalVertices.length]
|
||||||
|
const proj = _projectOnSegment(p, a, b)
|
||||||
|
const dd = (proj.x - p.x) * (proj.x - p.x) + (proj.y - p.y) * (proj.y - p.y)
|
||||||
|
if (dd < minDist) {
|
||||||
|
minDist = dd
|
||||||
|
nearest = proj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 바깥에 있으면 원본 폴리곤의 가장 가까운 변 위의 점으로 이동
|
try {
|
||||||
let minDist = Infinity
|
const offsetCoords = offsetVertices.map((p) => [p.x, p.y])
|
||||||
let nearest = p
|
offsetCoords.push([offsetVertices[0].x, offsetVertices[0].y])
|
||||||
for (let i = 0; i < originalVertices.length; i++) {
|
|
||||||
const a = originalVertices[i]
|
const origCoords = originalVertices.map((p) => [p.x, p.y])
|
||||||
const b = originalVertices[(i + 1) % originalVertices.length]
|
origCoords.push([originalVertices[0].x, originalVertices[0].y])
|
||||||
const proj = projectOnSegment(p, a, b)
|
|
||||||
const dist = (proj.x - p.x) * (proj.x - p.x) + (proj.y - p.y) * (proj.y - p.y)
|
// 자기교차가 있으면 intersect 가 실패하므로 먼저 unkink
|
||||||
if (dist < minDist) {
|
let offsetPoly = turf.polygon([offsetCoords])
|
||||||
minDist = dist
|
const kinked = turf.kinks(offsetPoly)
|
||||||
nearest = proj
|
if (kinked.features.length > 0) {
|
||||||
|
const unkinked = turf.unkinkPolygon(offsetPoly)
|
||||||
|
if (unkinked.features.length > 0) {
|
||||||
|
offsetPoly = unkinked.features.reduce((max, f) => (turf.area(f) > turf.area(max) ? f : max))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nearest
|
|
||||||
})
|
const origPoly = turf.polygon([origCoords])
|
||||||
|
|
||||||
|
const clipped = turf.intersect(turf.featureCollection([offsetPoly, origPoly]))
|
||||||
|
|
||||||
|
if (clipped) {
|
||||||
|
// MultiPolygon 인 경우 가장 큰 폴리곤 선택
|
||||||
|
let coords
|
||||||
|
if (clipped.geometry.type === 'MultiPolygon') {
|
||||||
|
let maxArea = -Infinity
|
||||||
|
let bestRing = null
|
||||||
|
clipped.geometry.coordinates.forEach((poly) => {
|
||||||
|
const ring = poly[0]
|
||||||
|
const feat = turf.polygon([ring])
|
||||||
|
const area = turf.area(feat)
|
||||||
|
if (area > maxArea) {
|
||||||
|
maxArea = area
|
||||||
|
bestRing = ring
|
||||||
|
}
|
||||||
|
})
|
||||||
|
coords = bestRing
|
||||||
|
} else {
|
||||||
|
coords = clipped.geometry.coordinates[0]
|
||||||
|
}
|
||||||
|
if (coords && coords.length > 0) {
|
||||||
|
const result = coords.slice(0, -1).map((c) => ({ x: c[0], y: c[1] }))
|
||||||
|
// 부동소수점 오차로 미세하게 경계 밖에 있는 점만 경계로 스냅
|
||||||
|
return _snapToBoundary(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to clip offset polygon to original:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// turf 실패 fallback: 바깥으로 나간 점만 원본 경계로 스냅
|
||||||
|
return _snapToBoundary(offsetVertices)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanSelfIntersectingPolygon(vertices) {
|
export function cleanSelfIntersectingPolygon(vertices) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user