diff --git a/Nextjs 14 컴포넌트에 대해서....pdf b/Nextjs 14 컴포넌트에 대해서....pdf new file mode 100644 index 00000000..fe9072f1 Binary files /dev/null and b/Nextjs 14 컴포넌트에 대해서....pdf differ diff --git a/Qcast coding convention.pdf b/Qcast coding convention.pdf new file mode 100644 index 00000000..0ab0e190 Binary files /dev/null and b/Qcast coding convention.pdf differ diff --git a/Qcast development guilde.pdf b/Qcast development guilde.pdf new file mode 100644 index 00000000..d991663d Binary files /dev/null and b/Qcast development guilde.pdf differ diff --git a/README.md b/README.md index 24eddc59..c4033664 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,36 @@ -# 점 갯수 별 타입 +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -## 점 6개 +## Getting Started -### type1 +First, run the development server: -![type1](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6+-+type1.png) +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` -### type2 +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -![type2](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-+type2.png) +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -### type3 +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. -![type3](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-type3.png) +## Learn More -### type4 +To learn more about Next.js, take a look at the following resources: -![type4](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-type4.png) +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/next.config.mjs b/next.config.mjs index 15466bd2..80649ef9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,6 +10,9 @@ const nextConfig = { // config.infrastructureLogging = { debug: /PackFileCache/ }; return config }, + sassOptions: { + includePaths: ['./src/styles'], + }, } export default nextConfig diff --git a/package.json b/package.json index d4cdd9bb..28845ef0 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "postcss": "^8", "prettier": "^3.3.3", "prisma": "^5.17.0", + "sass": "^1.77.8", "tailwindcss": "^3.4.1" } } diff --git a/shape-type.md b/shape-type.md new file mode 100644 index 00000000..24eddc59 --- /dev/null +++ b/shape-type.md @@ -0,0 +1,19 @@ +# 점 갯수 별 타입 + +## 점 6개 + +### type1 + +![type1](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6+-+type1.png) + +### type2 + +![type2](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-+type2.png) + +### type3 + +![type3](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-type3.png) + +### type4 + +![type4](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-type4.png) diff --git a/src/app/changelog/page.jsx b/src/app/changelog/page.jsx index 1dcc8d0e..52c207b8 100644 --- a/src/app/changelog/page.jsx +++ b/src/app/changelog/page.jsx @@ -47,6 +47,9 @@ export default function changelogPage() { +
+

Sass 테스트입니다.

+
) } diff --git a/src/app/layout.js b/src/app/layout.js index 55cb11f3..d9dd77d0 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,5 +1,6 @@ import { Inter } from 'next/font/google' import './globals.css' +import '../styles/style.scss' import Headers from '@/components/Headers' import RecoilRootWrapper from './RecoilWrapper' import UIProvider from './UIProvider' diff --git a/src/components/Roof2.jsx b/src/components/Roof2.jsx index da41280b..be84a135 100644 --- a/src/components/Roof2.jsx +++ b/src/components/Roof2.jsx @@ -2,15 +2,15 @@ import { useCanvas } from '@/hooks/useCanvas' import { useEffect, useState } from 'react' import { Mode, useMode } from '@/hooks/useMode' import { Button } from '@nextui-org/react' -import QRect from '@/components/fabric/QRect' import RangeSlider from './ui/RangeSlider' import { useRecoilState, useRecoilValue } from 'recoil' -import { canvasSizeState, fontSizeState, roofMaterialState, roofState, sortedPolygonArray } from '@/store/canvasAtom' +import { canvasSizeState, fontSizeState, roofMaterialState, sortedPolygonArray } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' import { getCanvasState, insertCanvasState } from '@/lib/canvas' import { calculateIntersection } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' +import offsetPolygon from '@/util/qpolygon-utils' export default function Roof2() { const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas') @@ -48,6 +48,7 @@ export default function Roof2() { handleOuterlinesTest2, applyTemplateB, makeRoofPatternPolygon, + createRoofRack, } = useMode() // const [canvasState, setCanvasState] = useRecoilState(canvasAtom) @@ -59,22 +60,6 @@ export default function Roof2() { changeMode(canvas, mode) }, [canvas, mode]) - const makeRect = () => { - if (canvas) { - const rect = new QRect({ - left: 100, - top: 100, - fill: 'transparent', - stroke: 'black', - width: 400, - height: 100, - fontSize: fontSize, - }) - - canvas?.add(rect) - } - } - const makeLine = () => { if (canvas) { const line = new QLine([50, 50, 200, 50], { @@ -264,7 +249,7 @@ export default function Roof2() { { x: 675, y: 275 }, { x: 450, y: 850 }, ] - const polygon = new QPolygon(type2, { + const polygon = new QPolygon(type1, { fill: 'transparent', stroke: 'black', strokeWidth: 1, @@ -448,21 +433,6 @@ export default function Roof2() { 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 - }) - - if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') { - roof.fillCell({ width: 50, height: 100, padding: 0 }) - } else { - roof.fillCell({ width: 100, height: 50, padding: 0 }) - } - }) - } - return ( <> {canvas && ( @@ -501,9 +471,6 @@ export default function Roof2() { - @@ -520,9 +487,6 @@ export default function Roof2() { 축소 현재 줌 : {zoom}% - diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 685d83ee..61a7928c 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -13,6 +13,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { hips: [], ridges: [], connectRidges: [], + cells: [], initialize: function (points, options, canvas) { // 소수점 전부 제거 points.forEach((point) => { @@ -223,6 +224,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { const drawCellsArray = [] //그려진 셀의 배열 + let idx = 1 + for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const rectLeft = minX + i * (rectWidth + cell.padding) + tmpWidth @@ -251,13 +254,18 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { lockScalingX: true, // X 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금 opacity: 0.8, + name: 'cell', + idx: idx, }) + + idx++ drawCellsArray.push(rect) //배열에 넣어서 반환한다 this.canvas.add(rect) } } } this.canvas?.renderAll() + this.cells = drawCellsArray return drawCellsArray }, inPolygon(point) { diff --git a/src/components/fabric/QRect.js b/src/components/fabric/QRect.js deleted file mode 100644 index ad6c8ae9..00000000 --- a/src/components/fabric/QRect.js +++ /dev/null @@ -1,98 +0,0 @@ -import { fabric } from 'fabric' -export default class QRect extends fabric.Rect { - #text = [] - #viewLengthText - #fontSize - type = 'QRect' - constructor(option) { - if (!option.fontSize) { - throw new Error('Font size is required.') - } - super(option) - this.#fontSize = option.fontSize - this.#init(option) - this.#addControl() - } - - #init(option) { - this.#viewLengthText = option.viewLengthText ?? true - } - - setViewLengthText(bool) { - this.#viewLengthText = bool - this.#addLengthText() - } - - setFontSize(fontSize) { - this.#fontSize = fontSize - this.#addLengthText() - } - - #addControl() { - this.on('removed', () => { - if (this.#text.length > 0) { - this.#text.forEach((text) => { - this.canvas.remove(text) - }) - this.#text = [] - } - }) - - this.on('added', () => { - this.#addLengthText() - }) - - this.on('modified', (e) => { - this.#addLengthText() - }) - - this.on('scaling', (e) => { - this.#addLengthText() - }) - - this.on('moving', () => { - this.#addLengthText() - }) - } - #addLengthText() { - if (this.#text.length > 0) { - this.#text.forEach((text) => { - this.canvas.remove(text) - }) - this.#text = [] - } - - if (!this.#viewLengthText) { - return - } - - const scaleX = this.scaleX - const scaleY = this.scaleY - - const lines = [ - { - start: { x: this.left, y: this.top }, - end: { x: this.left + this.width * scaleX, y: this.top }, - }, - { - start: { x: this.left, y: this.top + this.height * scaleY }, - end: { x: this.left, y: this.top }, - }, - ] - - lines.forEach((line) => { - const dx = line.end.x - line.start.x - const dy = line.end.y - line.start.y - const length = Math.sqrt(dx * dx + dy * dy) - - const text = new fabric.Text(length.toFixed(0), { - left: (line.start.x + line.end.x) / 2, - top: (line.start.y + line.end.y) / 2, - fontSize: this.#fontSize, - selectable: false, - }) - this.#text.push(text) - this.canvas.add(text) - }) - } -} diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index 31b223bf..77f261cc 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -5,7 +5,6 @@ import { actionHandler, anchorWrapper, polygonPositionHandler } from '@/util/can import { useRecoilState } from 'recoil' import { canvasSizeState, fontSizeState } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' -import QRect from '@/components/fabric/QRect' import { QPolygon } from '@/components/fabric/QPolygon' import { defineQLine } from '@/util/qline-utils' import { defineQPloygon } from '@/util/qpolygon-utils' @@ -65,6 +64,7 @@ export function useCanvas(id) { if (canvas) { initialize() canvas?.on('object:added', onChange) + canvas?.on('object:added', addEventOnObject) canvas?.on('object:modified', onChange) canvas?.on('object:removed', onChange) canvas?.on('mouse:move', drawMouseLines) @@ -93,6 +93,21 @@ export function useCanvas(id) { canvas?.off('mouse:down', handleMouseDown) } + const addEventOnObject = (e) => { + const target = e.target + if (target.name === 'cell') { + target.on('mousedown', () => { + target.set({ fill: 'red' }) + }) + } + + if (target.name === 'trestle') { + target.on('mousedown', () => { + target.set({ strokeWidth: 5 }) + }) + } + } + /** * 마우스 포인터의 가이드라인을 제거합니다. */ @@ -116,7 +131,6 @@ export function useCanvas(id) { fabric.QPolygon = QPolygon QPolygon.prototype.canvas = canvas QLine.prototype.canvas = canvas - QRect.prototype.canvas = canvas defineQLine() defineQPloygon() } @@ -542,7 +556,24 @@ export function useCanvas(id) { const addCanvas = () => { // 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) @@ -552,16 +583,12 @@ export function useCanvas(id) { // 역직렬화하여 캔버스에 객체를 다시 추가합니다. canvas?.loadFromJSON(JSON.parse(str), function () { // 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다. + console.log(canvas?.getObjects().filter((obj) => obj.name === 'roof')) canvas?.renderAll() // 캔버스를 다시 그립니다. }) }, 1000) } - const changeCanvas = (idx) => { - canvas?.clear() - const canvasState = JSON.parse(canvasList[idx]) - } - return { canvas, addShape, @@ -577,6 +604,5 @@ export function useCanvas(id) { handleFlip, setCanvasBackgroundWithDots, addCanvas, - changeCanvas, } } diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index c60fba2b..efa933e2 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from 'react' -import QRect from '@/components/fabric/QRect' import { findTopTwoIndexesByDistance, getCenterPoint, getDirection, getStartIndex, rearrangeArray } from '@/util/canvas-util' import { useRecoilState } from 'recoil' @@ -17,6 +16,7 @@ import { import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' import { QPolygon } from '@/components/fabric/QPolygon' +import offsetPolygon from '@/util/qpolygon-utils' export const Mode = { DRAW_LINE: 'drawLine', // 기준선 긋기모드` @@ -58,6 +58,7 @@ export function useMode() { const [selectedCellRoofArray, setSelectedCellRoofArray] = useState([]) const [drewRoofCells, setDrewRoofCells] = useRecoilState(drewRoofCellsState) + const [roofStyle, setRoofStyle] = useState(1) //기본 지붕 패턴 useEffect(() => { // 이벤트 리스너 추가 @@ -521,6 +522,7 @@ export function useMode() { // handleOuterlines() const wall = makePolygon() + wall.set({ name: 'wall' }) setWall(wall) return wall @@ -614,27 +616,6 @@ export function useMode() { rect.set({ width: Math.abs(origX - pointer.x) }) rect.set({ height: Math.abs(origY - pointer.y) }) }) - - canvas.on('mouse:up', function (o) { - const pointer = canvas.getPointer(o.e) - const qRect = new QRect({ - left: origX, - top: origY, - originX: 'left', - originY: 'top', - width: pointer.x - origX, - height: pointer.y - origY, - angle: 0, - viewLengthText: true, - fill: 'transparent', - stroke: 'black', - transparentCorners: false, - fontSize: fontSize, - }) - canvas.remove(rect) - canvas.add(qRect) - isDown = false - }) } /** @@ -1112,67 +1093,14 @@ export function useMode() { /** *벽 지붕 외곽선 생성 polygon을 입력받아 만들기 */ - const handleOuterlinesTest2 = (polygon, offset = 71) => { - const offsetPoints = [] - const sortedIndex = getStartIndex(polygon.lines) - let tmpArraySorted = rearrangeArray(polygon.lines, sortedIndex) + const handleOuterlinesTest2 = (polygon, offset = 50) => { + const offsetPoints = offsetPolygon(polygon.points, offset) - if (tmpArraySorted[0].direction === 'right') { - //시계방향 - tmpArraySorted = tmpArraySorted.reverse() //그럼 배열을 거꾸로 만들어서 무조건 반시계방향으로 배열 보정 - } - - 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) + const roof = makePolygon( + offsetPoints.map((point) => { + return { x1: point.x, y1: point.y } + }), + ) roof.setWall(polygon) setRoof(roof) @@ -1233,15 +1161,15 @@ export function useMode() { setTemplateType(2) } - const handleOuterLineTemplateA4Points = (polygon) => { - const edge = 20 - const eaves = 50 + const handleOuterLineTemplateA4Points = (polygon, offsetInputX = 20, offsetInputY = 50) => { + const edge = offsetInputX + const eaves = offsetInputY // 폴리곤의 각 변을 선으로 생성 const createLine = (start, end, stroke, property) => new QLine([start.x, start.y, end.x, end.y], { stroke, - strokeWidth: 5, + strokeWidth: 1, property, fontSize: 14, }) @@ -1279,7 +1207,7 @@ export function useMode() { strokeDashArray: dashArray, }) - const vertCenterLine = createCenterLine(centerPointX, lines[0].y1 - edge, centerPointX, lines[0].y2 + edge, 'blue', 4, 'center') + const vertCenterLine = createCenterLine(centerPointX, lines[0].y1 - edge, centerPointX, lines[0].y2 + edge, 'blue', 1, 'center') canvas.add(vertCenterLine) const horiCenterLineLeft = createCenterLine( @@ -1341,6 +1269,31 @@ export function useMode() { const outLine = createLine({ x: line.x1, y: line.y1 }, { x: line.x2, y: line.y2 }, 'blue', 'normal') canvas.add(outLine) }) + + const roofPatternPolygonArray = [] + + const leftLine = drawArray[0] + const rightLine = drawArray[3] + + //사각형 왼쪽 지붕 패턴 생성 배열 + const leftPolygon = [ + { x: leftLine.x1, y: leftLine.y1 }, + { x: leftLine.x2, y: leftLine.y2 }, + { x: vertCenterLine.x1, y: vertCenterLine.y1 }, + { x: vertCenterLine.x2, y: vertCenterLine.y2 }, + ] + roofPatternPolygonArray.push(leftPolygon) + + //사각형 오른쪽 지붕 패턴 생성 배열 + const rightPolygon = [ + { x: vertCenterLine.x1, y: vertCenterLine.y1 }, + { x: vertCenterLine.x2, y: vertCenterLine.y2 }, + { x: rightLine.x1, y: rightLine.y1 }, + { x: rightLine.x2, y: rightLine.y2 }, + ] + roofPatternPolygonArray.push(rightPolygon) + + setRoofPolygonPattern({ roofPatternPolygonArray, lines }) //모든 행을 저장 } //탬플릿A 적용 @@ -2379,8 +2332,6 @@ export function useMode() { let tmpArray = [] let tmpBigArray = [] - console.log('tmpVertCenterLine', tmpVertCenterLine) - const lastCenterLine = tmpVertCenterLine[tmpVertCenterLine.length - 1] //마지막 센터라인을 정의 for (let i = 0; i < tmpVertCenterLine.length - 1; i++) { @@ -2455,8 +2406,6 @@ export function useMode() { } } - console.log('roofPatternPolygonArray', roofPatternPolygonArray) - setRoofPolygonPattern({ roofPatternPolygonArray, lines }) } else { // 오목한 부분이 세로선일때 아래ㄷ, 위ㄷ @@ -3179,23 +3128,7 @@ export function useMode() { canvas?.renderAll() } - /** - * 지붕 패턴 생성 로직 - * @param roofStyle - */ - const makeRoofPatternPolygon = (roofStyle) => { - if (Object.keys(roofPolygonPattern).length === 0 && roofPolygonPattern.constructor === Object) { - alert('객체가 비어있습니다.') - return - } - - //내부 선 점선으로 변경 추후에 다시 되돌리는 로직 필요 - roofPolygonPattern.lines.forEach((line, index) => { - line.line.set('strokeDashArray', [10, 5, 2, 5]) - line.line.set('stroke', 'blue') - line.line.set('strokeWidth', 1) - }) - + const getRoofPattern = (roofStyle, mode = 'normal') => { const ratio = window.devicePixelRatio || 1 const inputPatternSize = { width: 30, height: 20 } //임시 사이즈 @@ -3214,6 +3147,12 @@ export function useMode() { // 벽돌 패턴 그리기 ctx.scale(ratio, ratio) + + if (mode === 'cell') { + ctx.fillStyle = 'rgba(0, 0, 0, 0.3)' + ctx.fillRect(0, 0, patternSize.width * 2, patternSize.height * 2) + } + ctx.strokeStyle = 'green' ctx.lineWidth = 0.4 @@ -3241,6 +3180,30 @@ export function useMode() { repeat: 'repeat', }) + return pattern + } + + /** + * 지붕 패턴 생성 로직 + * @param roofStyle + */ + const makeRoofPatternPolygon = (roofStyle) => { + if (Object.keys(roofPolygonPattern).length === 0 && roofPolygonPattern.constructor === Object) { + alert('객체가 비어있습니다.') + return + } + + setRoofStyle(roofStyle) //클릭한 지붕패턴을 저장 + + //내부 선 점선으로 변경 추후에 다시 되돌리는 로직 필요 + roofPolygonPattern.lines.forEach((line, index) => { + line.line.set('strokeDashArray', [10, 5, 2, 5]) + line.line.set('stroke', 'blue') + line.line.set('strokeWidth', 1) + }) + + const pattern = getRoofPattern(roofStyle) + const commonOption = { fill: pattern, selectable: false, @@ -3316,14 +3279,16 @@ export function useMode() { canvas.discardActiveObject() // 객체의 활성 상태 해제 //폴리곤에 커스텀 인덱스를 가지고 해당 배열 인덱스를 찾아 삭제함 - const removeIndex = polygon.customIndex - const removeArrayIndex = selectedAreaArray.findIndex((x) => x.customIndex === removeIndex) + const removeIndex = polygon.idx + const removeArrayIndex = selectedAreaArray.findIndex((x) => x.idx === removeIndex) selectedAreaArray.splice(removeArrayIndex, 1) } canvas?.renderAll() } - //외각선을 안쪽으로 그려 가대선을 그린다. + const pattern = getRoofPattern(roofStyle, 'cell') + + // 외각선을 안쪽으로 그려 가대선을 그린다. polygons.forEach((polygon, index) => { const trestlePolygon = handleOuterlinesTest(polygon, -12) trestlePolygon.setViewLengthText(false) //얘는 set으로 안먹는다... @@ -3337,7 +3302,8 @@ export function useMode() { lockScalingX: true, lockScalingY: true, bringToFront: true, - customIndex: polygon.customIndex, //가대 폴리곤의 임시 인덱스를 넣어줌 + idx: polygon.customIndex, //가대 폴리곤의 임시 인덱스를 넣어줌 + name: 'trestlePolygon', }) /** @@ -3346,8 +3312,15 @@ export function useMode() { trestlePolygon.on('mousedown', function () { toggleSelection(trestlePolygon) }) + + console.log('polygon', polygon) + + polygon.set({ fill: pattern }) }) + setSelectedCellRoofArray(selectedAreaArray) + canvas?.renderAll() + setMode(Mode.DEFAULT) //default 모드로 변경 } /** @@ -3392,6 +3365,51 @@ export function useMode() { setMode(Mode.DEFAULT) //default 모드로 변경 } + const createRoofRack = () => { + const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof') + let roofCells = [] // roof에 적재된 cell들 + roofs.forEach((roof, index) => { + let maxLengthLine = roof.lines.reduce((acc, cur) => { + return acc.length > cur.length ? acc : cur + }) + + const offsetPolygonPoint = offsetPolygon(roof.points, -20, 0) + + const trestlePoly = new QPolygon(offsetPolygonPoint, { + fill: 'transparent', + stroke: 'red', + strokeDashArray: [5, 5], + strokeWidth: 1, + selectable: true, + fontSize: fontSize, + name: 'trestle', + lockMovementX: true, // X 축 이동 잠금 + lockMovementY: true, // Y 축 이동 잠금 + lockRotation: true, // 회전 잠금 + lockScalingX: true, // X 축 크기 조정 잠금 + lockScalingY: true, // Y 축 크기 조정 잠금 + idx: index, + }) + + canvas?.add(trestlePoly) + + let drawRoofCells + if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') { + drawRoofCells = trestlePoly.fillCell({ width: 100, height: 50, padding: 10 }) + trestlePoly.direction = 'south' + } else { + drawRoofCells = trestlePoly.fillCell({ width: 50, height: 100, padding: 10 }) + trestlePoly.direction = 'east' + } + + drawRoofCells.forEach((cell) => { + roofCells.push(cell) + }) + }) + + setDrewRoofCells(roofCells) + } + return { mode, setMode, @@ -3406,5 +3424,6 @@ export function useMode() { handleOuterlinesTest2, makeRoofPatternPolygon, makeRoofTrestle, + createRoofRack, } } diff --git a/src/styles/_test.scss b/src/styles/_test.scss new file mode 100644 index 00000000..babec92c --- /dev/null +++ b/src/styles/_test.scss @@ -0,0 +1,3 @@ +.test { + background-color: #121212; +} diff --git a/src/styles/style.scss b/src/styles/style.scss new file mode 100644 index 00000000..e7f56590 --- /dev/null +++ b/src/styles/style.scss @@ -0,0 +1 @@ +@import '_test.scss'; \ No newline at end of file diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 36eadd4d..80693e04 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -3,6 +3,8 @@ import { QLine } from '@/components/fabric/QLine' import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDirectionByPoint } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' +const TWO_PI = Math.PI * 2 + export const defineQPloygon = () => { fabric.QPolygon.fromObject = function (object, callback) { fabric.Object._fromObject('QPolygon', object, callback, 'points') @@ -251,6 +253,10 @@ export const drawHelpLineInHexagon = (polygon, chon) => { name: 'hip', }) + if (line.length === 0) { + return + } + line.startPoint = ridgePoint line.endPoint = filteredCenterInterSectionPoints @@ -281,54 +287,74 @@ export const drawHelpLineInHexagon = (polygon, chon) => { const startPoint = remainingPoints.shift() const endPoint = remainingPoints.shift() - const line = new QLine([startPoint.x, startPoint.y, endPoint.x, endPoint.y], { - stroke: 'purple', - fontSize: polygon.fontSize, - name: 'connectRidge', - }) + if (!(startPoint.x === endPoint.x && startPoint.y === endPoint.y)) { + const line = new QLine([startPoint.x, startPoint.y, endPoint.x, endPoint.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'connectRidge', + }) - line.startPoint = startPoint - line.endPoint = endPoint + line.startPoint = startPoint + line.endPoint = endPoint - polygon.connectRidges.push(line) + polygon.connectRidges.push(line) - polygon.points.forEach((point) => { - const degree = calculateAngle(startPoint, point) + polygon.points.forEach((point) => { + const degree = calculateAngle(startPoint, point) - if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { - const line = new QLine([startPoint.x, startPoint.y, point.x, point.y], { - stroke: 'purple', - fontSize: polygon.fontSize, - name: 'hip', - }) + if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { + const line = new QLine([startPoint.x, startPoint.y, point.x, point.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'hip', + }) - line.startPoint = startPoint - line.endPoint = point + line.startPoint = startPoint + line.endPoint = point - polygon.hips.push(line) - polygon.canvas.add(line) - } - }) + polygon.hips.push(line) + polygon.canvas.add(line) + } + }) - polygon.points.forEach((point) => { - const degree = calculateAngle(endPoint, point) + polygon.points.forEach((point) => { + const degree = calculateAngle(endPoint, point) - if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { - const line = new QLine([endPoint.x, endPoint.y, point.x, point.y], { - stroke: 'purple', - fontSize: polygon.fontSize, - name: 'hip', - }) + if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { + const line = new QLine([endPoint.x, endPoint.y, point.x, point.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'hip', + }) - line.startPoint = endPoint - line.endPoint = point + line.startPoint = endPoint + line.endPoint = point - polygon.hips.push(line) - polygon.canvas.add(line) - } - }) + polygon.hips.push(line) + polygon.canvas.add(line) + } + }) - polygon.canvas.add(line) + polygon.canvas.add(line) + } else { + polygon.points.forEach((point) => { + const degree = calculateAngle(startPoint, point) + + if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { + const line = new QLine([startPoint.x, startPoint.y, point.x, point.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'hip', + }) + + line.startPoint = startPoint + line.endPoint = point + + polygon.hips.push(line) + polygon.canvas.add(line) + } + }) + } } } @@ -483,9 +509,8 @@ export const dividePolygon = (polygon) => { } }) hips = [...hips, ...connectRidges] - polygon.setViewLengthText(false) - polygonLines.forEach((line) => { + polygonLines.forEach((line, index) => { let ridge const startPoint = line.startPoint @@ -520,15 +545,31 @@ export const dividePolygon = (polygon) => { return } - let connectedRidge = ridges.find( - (ridge) => - (ridge.startPoint.x === startHip.endPoint.x && ridge.startPoint.y === startHip.endPoint.y) || - (ridge.endPoint.x === startHip.endPoint.x && ridge.endPoint.y === startHip.endPoint.y), - ) + let connectedRidge + const restRidgeConnection = connectRidges[0] + + if (!restRidgeConnection || restRidgeConnection.length === 0) { + connectedRidge = ridges.find( + (ridge) => + (ridge.startPoint.x === startHip.endPoint.x && + ridge.startPoint.y === startHip.endPoint.y && + ridge.endPoint.x === endHip.endPoint.x && + ridge.endPoint.y === endHip.endPoint.y) || + (ridge.startPoint.x === endHip.endPoint.x && + ridge.startPoint.y === endHip.endPoint.y && + ridge.endPoint.x === startHip.endPoint.x && + ridge.endPoint.y === startHip.endPoint.y), + ) + } else { + connectedRidge = ridges.find( + (ridge) => + (ridge.startPoint.x === startHip.endPoint.x && ridge.startPoint.y === startHip.endPoint.y) || + (ridge.endPoint.x === startHip.endPoint.x && ridge.endPoint.y === startHip.endPoint.y), + ) + } const hipStartPoint = startHip.endPoint const hipEndPoint = endHip.endPoint - const restRidgeConnection = connectRidges[0] if (connectedRidge.startPoint.x === hipStartPoint.x && connectedRidge.startPoint.y === hipStartPoint.y) { if (connectedRidge.endPoint.x === hipEndPoint.x && connectedRidge.endPoint.y === hipEndPoint.y) { @@ -676,3 +717,239 @@ const getOneSideLine = (line) => { 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 + } + } +} diff --git a/yarn.lock b/yarn.lock index 5eec9ca2..7c678f91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2258,7 +2258,7 @@ canvas@^2.8.0: nan "^2.17.0" simple-get "^3.0.3" -chokidar@^3.5.3: +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -2714,6 +2714,11 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +immutable@^4.0.0: + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3402,6 +3407,15 @@ safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sass@^1.77.8: + version "1.77.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.8.tgz#9f18b449ea401759ef7ec1752a16373e296b52bd" + integrity sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + saxes@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz" @@ -3486,7 +3500,7 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -source-map-js@^1.0.2, source-map-js@^1.2.0: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==