Merge branch 'dev' into dev-yj

This commit is contained in:
yjnoh 2024-08-02 15:20:20 +09:00
commit 896a0baa7e
12 changed files with 495 additions and 128 deletions

View File

@ -1 +1,3 @@
NEXT_PUBLIC_TEST="테스트변수입니다. development" NEXT_PUBLIC_TEST="테스트변수입니다. development"
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"

View File

@ -1 +1,3 @@
NEXT_PUBLIC_TEST="테스트변수입니다. production" NEXT_PUBLIC_TEST="테스트변수입니다. production"
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@nextui-org/react": "^2.4.2", "@nextui-org/react": "^2.4.2",
"@prisma/client": "^5.17.0", "@prisma/client": "^5.17.0",
"axios": "^1.7.3",
"fabric": "^5.3.0", "fabric": "^5.3.0",
"framer-motion": "^11.2.13", "framer-motion": "^11.2.13",
"mathjs": "^13.0.2", "mathjs": "^13.0.2",

View File

@ -1,12 +1,18 @@
'use client' 'use client'
import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react' import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react'
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import QSelect from '@/components/ui/QSelect' import QSelect from '@/components/ui/QSelect'
import styles from './changelog.module.css' import styles from './changelog.module.css'
import { get } from '@/lib/Axios'
export default function changelogPage() { export default function changelogPage() {
const testVar = process.env.NEXT_PUBLIC_TEST const testVar = process.env.NEXT_PUBLIC_TEST
const handleUsers = async () => {
const users = await get('/api/user/find-all')
console.log(users)
}
return ( return (
<> <>
<Hero title="Change log" /> <Hero title="Change log" />
@ -32,10 +38,15 @@ export default function changelogPage() {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
<div className="px-2 py-4"> <div className="m-2">
<QSelect /> <QSelect />
</div> </div>
<div className="w-full bg-orange-300 py-4">{testVar}</div> <div className="w-full bg-orange-300 m-2">{testVar}</div>
<div>
<div className="m-2">
<Button onClick={handleUsers}>Button</Button>
</div>
</div>
</> </>
) )
} }

View File

@ -6,11 +6,12 @@ import QRect from '@/components/fabric/QRect'
import RangeSlider from './ui/RangeSlider' import RangeSlider from './ui/RangeSlider'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { canvasSizeState, fontSizeState, roofState, sortedPolygonArray } from '@/store/canvasAtom' import { canvasSizeState, fontSizeState, roofMaterialState, roofState, sortedPolygonArray } from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine' import { QLine } from '@/components/fabric/QLine'
import { getCanvasState, insertCanvasState } from '@/lib/canvas' import { getCanvasState, insertCanvasState } from '@/lib/canvas'
import { calculateIntersection } from '@/util/canvas-util' import { calculateIntersection } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
import offsetPolygon from '@/util/qpolygon-utils'
export default function Roof2() { export default function Roof2() {
const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas') const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas')
@ -31,7 +32,8 @@ export default function Roof2() {
const [showControl, setShowControl] = useState(false) const [showControl, setShowControl] = useState(false)
const roof = useRecoilValue(roofState) //
const roofMaterial = useRecoilValue(roofMaterialState)
const { const {
mode, mode,
@ -138,12 +140,12 @@ export default function Roof2() {
{ x: 100, y: 400 }, { x: 100, y: 400 },
] ]
const type2 = [ const type2 = [
{ x: 100, y: 100 }, { x: 200, y: 100 },
{ x: 100, y: 1000 }, { x: 200, y: 1000 },
{ x: 1000, y: 1000 }, { x: 1100, y: 1000 },
{ x: 1000, y: 600 }, { x: 1100, y: 600 },
{ x: 550, y: 600 }, { x: 650, y: 600 },
{ x: 550, y: 100 }, { x: 650, y: 100 },
] ]
const type3 = [ const type3 = [
@ -263,58 +265,17 @@ export default function Roof2() {
{ x: 675, y: 275 }, { x: 675, y: 275 },
{ x: 450, y: 850 }, { x: 450, y: 850 },
] ]
const polygon = new QPolygon(type4, { const polygon = new QPolygon(type2, {
fill: 'transparent', fill: 'transparent',
stroke: 'black', stroke: 'black',
strokeWidth: 1, strokeWidth: 1,
selectable: false, selectable: false,
fontSize: fontSize, fontSize: fontSize,
name: 'QPolygon1', name: 'wall',
}) })
canvas?.add(polygon) canvas?.add(polygon)
polygon.set('strokeDashArray', [10, 5, 2, 5])
polygon.set('stroke', 'blue')
polygon.set('strokeWidth', 1)
// const newPolygon = fabric.util.clone(polygon)
handleOuterlinesTest2(polygon) handleOuterlinesTest2(polygon)
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof')
roofs.forEach((roof) => {
let maxLengthLine = roof.lines.reduce((acc, cur) => {
return acc.length > cur.length ? acc : cur
})
//
const patternSourceCanvas = document.createElement('canvas')
if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') {
patternSourceCanvas.width = 20 * window.devicePixelRatio || 1
patternSourceCanvas.height = 10 * window.devicePixelRatio || 1
} else {
patternSourceCanvas.width = 10 * window.devicePixelRatio || 1
patternSourceCanvas.height = 20 * window.devicePixelRatio || 1
}
const ctx = patternSourceCanvas.getContext('2d')
//
ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1)
ctx.strokeStyle = 'green'
ctx.lineWidth = 0.4
ctx.strokeRect(0, 0, 100, 100)
//
const pattern = new fabric.Pattern({
source: patternSourceCanvas,
repeat: 'repeat',
})
roof.set('fill', pattern)
})
// const lines = togglePolygonLine(polygon) // const lines = togglePolygonLine(polygon)
// togglePolygonLine(lines[0]) // togglePolygonLine(lines[0])
} }
@ -404,6 +365,70 @@ export default function Roof2() {
handleClear() handleClear()
} }
const drawRoofMaterial = () => {
const { width, height, roofStyle } = roofMaterial
const wallPolygon = canvas?.getObjects().find((obj) => obj.name === 'wall')
wallPolygon.set('strokeDashArray', [10, 5, 2, 5])
wallPolygon.set('stroke', 'blue')
wallPolygon.set('strokeWidth', 1)
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof')
roofs.forEach((roof) => {
let maxLengthLine = roof.lines.reduce((acc, cur) => {
return acc.length > cur.length ? acc : cur
})
const roofRatio = window.devicePixelRatio || 1
//
const patternSourceCanvas = document.createElement('canvas')
if (roofStyle === 1) {
if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') {
patternSourceCanvas.width = width * roofRatio
patternSourceCanvas.height = height * roofRatio
} else {
patternSourceCanvas.width = height * roofRatio
patternSourceCanvas.height = width * roofRatio
}
} else if (roofStyle === 2) {
if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') {
patternSourceCanvas.width = width * 2
patternSourceCanvas.height = height * 2
} else {
patternSourceCanvas.width = height * 2
patternSourceCanvas.height = width * 2
}
}
const ctx = patternSourceCanvas.getContext('2d')
ctx.scale(roofRatio, roofRatio)
ctx.strokeStyle = 'green'
ctx.lineWidth = 0.4
//
if (roofStyle === 1) {
ctx.strokeRect(0, 0, 50, 30)
} else if (roofStyle === 2) {
//
ctx.strokeRect(0, 0, 200, 100)
ctx.strokeRect(100, 100, 200, 100)
}
//
const pattern = new fabric.Pattern({
source: patternSourceCanvas,
repeat: 'repeat',
})
roof.set('fill', null)
roof.set('fill', pattern)
canvas?.renderAll()
})
}
/** /**
* canvas 내용 불러오기 * canvas 내용 불러오기
*/ */
@ -424,6 +449,34 @@ export default function Roof2() {
makeRoofPatternPolygon(roofStyle) makeRoofPatternPolygon(roofStyle)
} }
const createRoofRack = () => {
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof')
roofs.forEach((roof) => {
let maxLengthLine = roof.lines.reduce((acc, cur) => {
return acc.length > cur.length ? acc : cur
})
const offsetPolygonPoint = offsetPolygon(roof.points, -20, 0)
const newPoly = new QPolygon(offsetPolygonPoint, {
fill: 'transparent',
stroke: 'red',
strokeWidth: 1,
selectable: true,
fontSize: fontSize,
name: 'wall',
})
canvas?.add(newPoly)
if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') {
newPoly.fillCell({ width: 100, height: 50, padding: 10 })
} else {
newPoly.fillCell({ width: 50, height: 100, padding: 10 })
}
})
}
return ( return (
<> <>
{canvas && ( {canvas && (
@ -530,6 +583,12 @@ export default function Roof2() {
<Button className="m-1 p-2" onClick={addCanvas}> <Button className="m-1 p-2" onClick={addCanvas}>
캔버스 추가 캔버스 추가
</Button> </Button>
<Button className="m-1 p-2" onClick={drawRoofMaterial}>
지붕타입 지붕재
</Button>
<Button className="m-1 p-2" onClick={createRoofRack}>
지붕가대
</Button>
<Button className="m-1 p-2" color={`${showControl ? 'primary' : 'default'}`} onClick={handleShowController}> <Button className="m-1 p-2" color={`${showControl ? 'primary' : 'default'}`} onClick={handleShowController}>
canvas 컨트롤러 {`${showControl ? '숨기기' : '보이기'}`} canvas 컨트롤러 {`${showControl ? '숨기기' : '보이기'}`}
</Button> </Button>

View File

@ -13,6 +13,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
hips: [], hips: [],
ridges: [], ridges: [],
connectRidges: [], connectRidges: [],
cells: [],
initialize: function (points, options, canvas) { initialize: function (points, options, canvas) {
// 소수점 전부 제거 // 소수점 전부 제거
points.forEach((point) => { points.forEach((point) => {
@ -234,7 +235,6 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
{ x: rectLeft, y: rectTop + rectHeight }, { x: rectLeft, y: rectTop + rectHeight },
{ x: rectLeft + rectWidth, y: rectTop + rectHeight }, { x: rectLeft + rectWidth, y: rectTop + rectHeight },
] ]
const allPointsInside = rectPoints.every((point) => this.inPolygon(point)) const allPointsInside = rectPoints.every((point) => this.inPolygon(point))
if (allPointsInside) { if (allPointsInside) {
@ -244,6 +244,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
width: rectWidth, width: rectWidth,
height: rectHeight, height: rectHeight,
fill: '#BFFD9F', fill: '#BFFD9F',
stroke: 'black',
selectable: true, // 선택 가능하게 설정 selectable: true, // 선택 가능하게 설정
lockMovementX: true, // X 축 이동 잠금 lockMovementX: true, // X 축 이동 잠금
lockMovementY: true, // Y 축 이동 잠금 lockMovementY: true, // Y 축 이동 잠금
@ -251,17 +252,24 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
lockScalingX: true, // X 축 크기 조정 잠금 lockScalingX: true, // X 축 크기 조정 잠금
lockScalingY: true, // Y 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금
opacity: 0.8, opacity: 0.8,
name: 'cell',
}) })
rect.on('mousedown', () => {
rect.set({ fill: 'red' })
})
drawCellsArray.push(rect) //배열에 넣어서 반환한다 drawCellsArray.push(rect) //배열에 넣어서 반환한다
this.canvas.add(rect) this.canvas.add(rect)
} }
} }
} }
this.canvas?.renderAll() this.canvas?.renderAll()
this.cells = drawCellsArray
return drawCellsArray return drawCellsArray
}, },
inPolygon(point) { inPolygon(point) {
const vertices = this.getCurrentPoints() const vertices = this.points
let intersects = 0 let intersects = 0
for (let i = 0; i < vertices.length; i++) { for (let i = 0; i < vertices.length; i++) {

View File

@ -542,7 +542,24 @@ export function useCanvas(id) {
const addCanvas = () => { const addCanvas = () => {
// const canvasState = canvas // const canvasState = canvas
const objs = canvas?.toJSON(['selectable', 'name', 'parentId', 'id', 'length', 'idx', 'direction', 'lines', 'points']) const objs = canvas?.toJSON([
'selectable',
'name',
'parentId',
'id',
'length',
'idx',
'direction',
'lines',
'points',
'lockMovementX',
'lockMovementY',
'lockRotation',
'lockScalingX',
'lockScalingY',
'opacity',
'cells',
])
const str = JSON.stringify(objs) const str = JSON.stringify(objs)
@ -552,16 +569,12 @@ export function useCanvas(id) {
// 역직렬화하여 캔버스에 객체를 다시 추가합니다. // 역직렬화하여 캔버스에 객체를 다시 추가합니다.
canvas?.loadFromJSON(JSON.parse(str), function () { canvas?.loadFromJSON(JSON.parse(str), function () {
// 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다. // 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다.
console.log(canvas?.getObjects().filter((obj) => obj.name === 'roof'))
canvas?.renderAll() // 캔버스를 다시 그립니다. canvas?.renderAll() // 캔버스를 다시 그립니다.
}) })
}, 1000) }, 1000)
} }
const changeCanvas = (idx) => {
canvas?.clear()
const canvasState = JSON.parse(canvasList[idx])
}
return { return {
canvas, canvas,
addShape, addShape,
@ -577,6 +590,5 @@ export function useCanvas(id) {
handleFlip, handleFlip,
setCanvasBackgroundWithDots, setCanvasBackgroundWithDots,
addCanvas, addCanvas,
changeCanvas,
} }
} }

View File

@ -17,6 +17,7 @@ import {
import { QLine } from '@/components/fabric/QLine' import { QLine } from '@/components/fabric/QLine'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
import offsetPolygon from '@/util/qpolygon-utils'
export const Mode = { export const Mode = {
DRAW_LINE: 'drawLine', // 기준선 긋기모드` DRAW_LINE: 'drawLine', // 기준선 긋기모드`
@ -1113,66 +1114,13 @@ export function useMode() {
* 지붕 외곽선 생성 polygon을 입력받아 만들기 * 지붕 외곽선 생성 polygon을 입력받아 만들기
*/ */
const handleOuterlinesTest2 = (polygon, offset = 71) => { const handleOuterlinesTest2 = (polygon, offset = 71) => {
const offsetPoints = [] const offsetPoints = offsetPolygon(polygon.points, 50)
const sortedIndex = getStartIndex(polygon.lines)
let tmpArraySorted = rearrangeArray(polygon.lines, sortedIndex)
if (tmpArraySorted[0].direction === 'right') { const roof = makePolygon(
//시계방향 offsetPoints.map((point) => {
tmpArraySorted = tmpArraySorted.reverse() //그럼 배열을 거꾸로 만들어서 무조건 반시계방향으로 배열 보정 return { x1: point.x, y1: point.y }
} }),
)
setSortedArray(tmpArraySorted) //recoil에 넣음
const points = tmpArraySorted.map((line) => ({
x: line.x1,
y: line.y1,
}))
for (let i = 0; i < points.length; i++) {
const prev = points[(i - 1 + points.length) % points.length]
const current = points[i]
const next = points[(i + 1) % points.length]
// 두 벡터 계산 (prev -> current, current -> next)
const vector1 = { x: current.x - prev.x, y: current.y - prev.y }
const vector2 = { x: next.x - current.x, y: next.y - current.y }
// 벡터의 길이 계산
const length1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y)
const length2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y)
// 벡터를 단위 벡터로 정규화
const unitVector1 = { x: vector1.x / length1, y: vector1.y / length1 }
const unitVector2 = { x: vector2.x / length2, y: vector2.y / length2 }
// 법선 벡터 계산 (왼쪽 방향)
const normal1 = { x: -unitVector1.y, y: unitVector1.x }
const normal2 = { x: -unitVector2.y, y: unitVector2.x }
// 법선 벡터 평균 계산
const averageNormal = {
x: (normal1.x + normal2.x) / 2,
y: (normal1.y + normal2.y) / 2,
}
// 평균 법선 벡터를 단위 벡터로 정규화
const lengthNormal = Math.sqrt(averageNormal.x * averageNormal.x + averageNormal.y * averageNormal.y)
const unitNormal = {
x: averageNormal.x / lengthNormal,
y: averageNormal.y / lengthNormal,
}
// 오프셋 적용
const offsetPoint = {
x1: current.x + unitNormal.x * offset,
y1: current.y + unitNormal.y * offset,
}
offsetPoints.push(offsetPoint)
}
const roof = makePolygon(offsetPoints)
roof.setWall(polygon) roof.setWall(polygon)
setRoof(roof) setRoof(roof)

56
src/lib/Axios.js Normal file
View File

@ -0,0 +1,56 @@
'use client'
import axios from 'axios'
axios.defaults.baseURL = process.env.NEXT_PUBLIC_API_SERVER_PATH
const axiosInstance = axios.create({
// baseURL: process.env.API_SERVER_URL,
headers: {
Accept: 'application/json',
},
})
axiosInstance.interceptors.request.use((config) => {
// config['Authorization'] = localStorage.getItem('token')
//TODO: 인터셉터에서 추가 로직 구현
return config
})
axiosInstance.interceptors.request.use(undefined, (error) => {
//TODO: 인터셉터에서 에러 처리 로직 구현
// if (error.isAxiosError && e.response?.status === 401) {
// localStorage.removeItem('token')
// }
})
export const get = (url) =>
axiosInstance
.get(url)
.then((res) => res.data)
.catch(console.error)
export const post = (url, data) =>
axiosInstance
.post(url, data)
.then((res) => res.data)
.catch(console.error)
export const put = (url, data) =>
axiosInstance
.put(url, data)
.then((res) => res.data)
.catch(console.error)
export const patch = (url, data) =>
axiosInstance
.patch(url, data)
.then((res) => res.data)
.catch(console.error)
export const del = (url) =>
axiosInstance
.delete(url)
.then((res) => res.data)
.catch(console.error)

View File

@ -60,3 +60,10 @@ export const drewRoofCellsState = atom({
default: [], default: [],
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })
// 지붕재 width, height, rafter(서까래), roofStyle을 갖고있고 roofStyle 1은 정방향, 2는 지그재그
export const roofMaterialState = atom({
key: 'roofMaterial',
default: { width: 20, height: 10, rafter: 0, roofStyle: 2 },
dangerouslyAllowMutability: true,
})

View File

@ -3,6 +3,8 @@ import { QLine } from '@/components/fabric/QLine'
import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDirectionByPoint } from '@/util/canvas-util' import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDirectionByPoint } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
const TWO_PI = Math.PI * 2
export const defineQPloygon = () => { export const defineQPloygon = () => {
fabric.QPolygon.fromObject = function (object, callback) { fabric.QPolygon.fromObject = function (object, callback) {
fabric.Object._fromObject('QPolygon', object, callback, 'points') fabric.Object._fromObject('QPolygon', object, callback, 'points')
@ -499,6 +501,10 @@ export const dividePolygon = (polygon) => {
const startHip = hips.find((hip) => hip.startPoint.x === startPoint.x && hip.startPoint.y === startPoint.y) const startHip = hips.find((hip) => hip.startPoint.x === startPoint.x && hip.startPoint.y === startPoint.y)
const endHip = hips.find((hip) => hip.startPoint.x === endPoint.x && hip.startPoint.y === endPoint.y) const endHip = hips.find((hip) => hip.startPoint.x === endPoint.x && hip.startPoint.y === endPoint.y)
if (!startHip || !endHip) {
return
}
if (startHip && endHip && startHip.endPoint.x === endHip.endPoint.x && startHip.endPoint.y === endHip.endPoint.y) { if (startHip && endHip && startHip.endPoint.x === endHip.endPoint.x && startHip.endPoint.y === endHip.endPoint.y) {
polygonPoints.push(startHip.endPoint) polygonPoints.push(startHip.endPoint)
@ -506,7 +512,7 @@ export const dividePolygon = (polygon) => {
fontSize: polygon.fontSize, fontSize: polygon.fontSize,
id: polygon.id, id: polygon.id,
name: 'roof', name: 'roof',
selectable: true, selectable: false,
stroke: 'black', stroke: 'black',
fill: 'transparent', fill: 'transparent',
strokeWidth: 3, strokeWidth: 3,
@ -535,7 +541,7 @@ export const dividePolygon = (polygon) => {
fontSize: polygon.fontSize, fontSize: polygon.fontSize,
id: polygon.id, id: polygon.id,
name: 'roof', name: 'roof',
selectable: true, selectable: false,
stroke: 'black', stroke: 'black',
fill: 'transparent', fill: 'transparent',
strokeWidth: 3, strokeWidth: 3,
@ -553,7 +559,7 @@ export const dividePolygon = (polygon) => {
fontSize: polygon.fontSize, fontSize: polygon.fontSize,
id: polygon.id, id: polygon.id,
name: 'roof', name: 'roof',
selectable: true, selectable: false,
stroke: 'black', stroke: 'black',
fill: 'transparent', fill: 'transparent',
strokeWidth: 3, strokeWidth: 3,
@ -628,7 +634,7 @@ export const dividePolygon = (polygon) => {
fontSize: polygon.fontSize, fontSize: polygon.fontSize,
id: polygon.id, id: polygon.id,
name: 'roof', name: 'roof',
selectable: true, selectable: false,
stroke: 'black', stroke: 'black',
fill: 'transparent', fill: 'transparent',
strokeWidth: 3, strokeWidth: 3,
@ -672,3 +678,239 @@ const getOneSideLine = (line) => {
return newLine return newLine
} }
function inwardEdgeNormal(vertex1, vertex2) {
// Assuming that polygon vertices are in clockwise order
const dx = vertex2.x - vertex1.x
const dy = vertex2.y - vertex1.y
const edgeLength = Math.sqrt(dx * dx + dy * dy)
return {
x: -dy / edgeLength,
y: dx / edgeLength,
}
}
function outwardEdgeNormal(vertex1, vertex2) {
var n = inwardEdgeNormal(vertex1, vertex2)
return {
x: -n.x,
y: -n.y,
}
}
function createPolygon(vertices) {
const edges = []
let minX = vertices.length > 0 ? vertices[0].x : undefined
let minY = vertices.length > 0 ? vertices[0].y : undefined
let maxX = minX
let maxY = minY
for (let i = 0; i < vertices.length; i++) {
const vertex1 = vertices[i]
const vertex2 = vertices[(i + 1) % vertices.length]
const outwardNormal = outwardEdgeNormal(vertex1, vertex2)
const inwardNormal = inwardEdgeNormal(vertex1, vertex2)
const edge = {
vertex1,
vertex2,
index: i,
outwardNormal,
inwardNormal,
}
edges.push(edge)
const x = vertices[i].x
const y = vertices[i].y
minX = Math.min(x, minX)
minY = Math.min(y, minY)
maxX = Math.max(x, maxX)
maxY = Math.max(y, maxY)
}
return {
vertices,
edges,
minX,
minY,
maxX,
maxY,
}
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB) {
const den =
(edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) -
(edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y)
if (den == 0) {
return null // lines are parallel or coincident
}
const ua =
((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) -
(edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) /
den
const ub =
((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) -
(edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) /
den
// Edges are not intersecting but the lines defined by them are
const isIntersectionOutside = ua < 0 || ub < 0 || ua > 1 || ub > 1
return {
x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x),
y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y),
isIntersectionOutside,
}
}
function appendArc(arcSegments, vertices, center, radius, startVertex, endVertex, isPaddingBoundary) {
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x)
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x)
if (startAngle < 0) {
startAngle += TWO_PI
}
if (endAngle < 0) {
endAngle += TWO_PI
}
const angle = startAngle > endAngle ? startAngle - endAngle : startAngle + TWO_PI - endAngle
const angleStep = (isPaddingBoundary ? -angle : TWO_PI - angle) / arcSegments
vertices.push(startVertex)
for (let i = 1; i < arcSegments; ++i) {
const angle = startAngle + angleStep * i
const vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
}
vertices.push(vertex)
}
vertices.push(endVertex)
}
function createOffsetEdge(edge, dx, dy) {
return {
vertex1: {
x: edge.vertex1.x + dx,
y: edge.vertex1.y + dy,
},
vertex2: {
x: edge.vertex2.x + dx,
y: edge.vertex2.y + dy,
},
}
}
function createMarginPolygon(polygon, offset, arcSegments = 0) {
const offsetEdges = []
for (let i = 0; i < polygon.edges.length; i++) {
const edge = polygon.edges[i]
const dx = edge.outwardNormal.x * offset
const dy = edge.outwardNormal.y * offset
offsetEdges.push(createOffsetEdge(edge, dx, dy))
}
const vertices = []
for (let i = 0; i < offsetEdges.length; i++) {
const thisEdge = offsetEdges[i]
const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length]
const vertex = edgesIntersection(prevEdge, thisEdge)
if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) {
vertices.push({
x: vertex.x,
y: vertex.y,
})
} else {
const arcCenter = polygon.edges[i].vertex1
appendArc(arcSegments, vertices, arcCenter, offset, prevEdge.vertex2, thisEdge.vertex1, false)
}
}
const marginPolygon = createPolygon(vertices)
marginPolygon.offsetEdges = offsetEdges
return marginPolygon
}
function createPaddingPolygon(polygon, offset, arcSegments = 0) {
const offsetEdges = []
for (let i = 0; i < polygon.edges.length; i++) {
const edge = polygon.edges[i]
const dx = edge.inwardNormal.x * offset
const dy = edge.inwardNormal.y * offset
offsetEdges.push(createOffsetEdge(edge, dx, dy))
}
const vertices = []
for (let i = 0; i < offsetEdges.length; i++) {
const thisEdge = offsetEdges[i]
const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length]
const vertex = edgesIntersection(prevEdge, thisEdge)
if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) {
vertices.push({
x: vertex.x,
y: vertex.y,
})
} else {
const arcCenter = polygon.edges[i].vertex1
appendArc(arcSegments, vertices, arcCenter, offset, prevEdge.vertex2, thisEdge.vertex1, true)
}
}
const paddingPolygon = createPolygon(vertices)
paddingPolygon.offsetEdges = offsetEdges
return paddingPolygon
}
export default function offsetPolygon(vertices, offset, arcSegments = 0) {
const polygon = createPolygon(vertices)
const originPolygon = new QPolygon(vertices, { fontSize: 0 })
if (offset > 0) {
let result = createMarginPolygon(polygon, offset, arcSegments).vertices
const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point))
if (allPointsOutside) {
return createMarginPolygon(polygon, offset, arcSegments).vertices
} else {
return createPaddingPolygon(polygon, offset, arcSegments).vertices
}
} else {
let result = createPaddingPolygon(polygon, offset, arcSegments).vertices
const allPointsInside = result.every((point) => originPolygon.inPolygon(point))
if (allPointsInside) {
return createPaddingPolygon(polygon, offset, arcSegments).vertices
} else {
return createMarginPolygon(polygon, offset, arcSegments).vertices
}
}
}

View File

@ -2181,6 +2181,15 @@ asynckit@^0.4.0:
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.3.tgz#a1125f2faf702bc8e8f2104ec3a76fab40257d85"
integrity sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@ -2539,6 +2548,11 @@ flat@^5.0.2:
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
foreground-child@^3.1.0: foreground-child@^3.1.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz"
@ -3233,6 +3247,11 @@ prisma@^5.17.0:
dependencies: dependencies:
"@prisma/engines" "5.17.0" "@prisma/engines" "5.17.0"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
psl@^1.1.33: psl@^1.1.33:
version "1.9.0" version "1.9.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"