Merge branch 'feature/test'

This commit is contained in:
yoosangwook 2024-07-17 14:09:18 +09:00
commit 663ec38bd5
27 changed files with 5719 additions and 225 deletions

View File

@ -1,36 +1,19 @@
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).
# 점 갯수 별 타입
## Getting Started
## 점 6개
First, run the development server:
### type1
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
![type1](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6+-+type1.png)
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
### type2
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
![type2](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-+type2.png)
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
### type3
## Learn More
![type3](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-type3.png)
To learn more about Next.js, take a look at the following resources:
### type4
- [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.
![type4](https://devgrr-bucket.s3.ap-northeast-2.amazonaws.com/qcast-type/point6-type4.png)

View File

@ -9,10 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"@nextui-org/react": "^2.4.2",
"fabric": "^5.3.0",
"framer-motion": "^11.2.13",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"recoil": "^0.7.7",
"uuid": "^9.0.1"
},
"devDependencies": {

BIN
public/assets/img/check.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

6
src/app/RecoilWrapper.js Normal file
View File

@ -0,0 +1,6 @@
'use client'
import { RecoilRoot } from 'recoil'
export default function RecoilRootWrapper({ children }) {
return <RecoilRoot>{children}</RecoilRoot>
}

5
src/app/UIProvider.js Normal file
View File

@ -0,0 +1,5 @@
import { NextUIProvider } from '@nextui-org/react'
export default function UIProvider({ children }) {
return <NextUIProvider>{children}</NextUIProvider>
}

View File

@ -0,0 +1,40 @@
'use client'
import Hero from '@/components/Hero'
import {
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from '@nextui-org/react'
export default function changelogPage() {
return (
<>
<Hero title="Change log" />
<div>
<Table isStriped>
<TableHeader>
<TableColumn>DATE</TableColumn>
<TableColumn>NAME</TableColumn>
<TableColumn>CONTENTS</TableColumn>
</TableHeader>
<TableBody>
<TableRow key="2">
<TableCell>2024.07.17</TableCell>
<TableCell>SWYOO</TableCell>
<TableCell>` * README.md 파일 이미지 경로 수정 `</TableCell>
</TableRow>
<TableRow key="1">
<TableCell>2024.07.16</TableCell>
<TableCell>SWYOO</TableCell>
<TableCell>` * 버튼 정리(템플릿 적용) `</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</>
)
}

View File

@ -31,3 +31,9 @@ body {
text-wrap: balance;
}
} */
.archivo-black-regular {
font-family: 'Archivo Black', sans-serif;
font-weight: 400;
font-style: normal;
}

View File

@ -1,6 +1,9 @@
import { Inter } from 'next/font/google'
import './globals.css'
import Headers from '@/components/Headers'
import RecoilRootWrapper from './RecoilWrapper'
import UIProvider from './UIProvider'
import { headers } from 'next/headers'
const inter = Inter({ subsets: ['latin'] })
@ -10,11 +13,17 @@ export const metadata = {
}
export default function RootLayout({ children }) {
const headersList = headers()
const headerPathname = headersList.get('x-pathname') || ''
// console.log('headerPathname', headerPathname)
return (
<html lang="en">
<body className={inter.className}>
<Headers />
{children}
{headerPathname !== '/login' && <Headers />}
<RecoilRootWrapper>
<UIProvider>{children}</UIProvider>
</RecoilRootWrapper>
</body>
</html>
)

91
src/app/login/page.jsx Normal file
View File

@ -0,0 +1,91 @@
export default function page() {
return (
<>
<div className="flex flex-col align-center h-screen">
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img
alt="Your Company"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
className="mx-auto h-10 w-auto"
/>
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Sign in to your account
</h2>
</div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form action="#" method="POST" className="space-y-6">
<div>
<label
htmlFor="email"
className="block text-sm font-medium leading-6 text-gray-900"
>
Email address
</label>
<div className="mt-2">
<input
id="email"
name="email"
type="email"
required
autoComplete="email"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<label
htmlFor="password"
className="block text-sm font-medium leading-6 text-gray-900"
>
Password
</label>
<div className="text-sm">
<a
href="#"
className="font-semibold text-indigo-600 hover:text-indigo-500"
>
Forgot password?
</a>
</div>
</div>
<div className="mt-2">
<input
id="password"
name="password"
type="password"
required
autoComplete="current-password"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<button
type="submit"
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Sign in
</button>
</div>
</form>
<p className="mt-10 text-center text-sm text-gray-500">
Not a member?{' '}
<a
href="#"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
>
Start a 14 day free trial
</a>
</p>
</div>
</div>
</div>
</>
)
}

23
src/app/roof2/page.jsx Normal file
View File

@ -0,0 +1,23 @@
'use client'
import Hero from '@/components/Hero'
import Roof2 from '@/components/Roof2'
import { textState } from '@/store/canvasAtom'
import { useEffect } from 'react'
import { useRecoilState } from 'recoil'
export default function Roof2Page() {
const [text, setText] = useRecoilState(textState)
useEffect(() => {
console.log(text)
}, [])
return (
<>
<div className="flex flex-col justify-center my-8 pt-20">
<Roof2 />
</div>
</>
)
}

View File

@ -1,118 +0,0 @@
/**
* Collection of function to use on canvas
*/
// define a function that can locate the controls
export function polygonPositionHandler(dim, finalMatrix, fabricObject) {
// @ts-ignore
let x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x
// @ts-ignore
let y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y
return fabric.util.transformPoint(
{ x, y },
fabric.util.multiplyTransformMatrices(
fabricObject.canvas.viewportTransform,
fabricObject.calcTransformMatrix(),
),
)
}
function getObjectSizeWithStroke(object) {
let stroke = new fabric.Point(
object.strokeUniform ? 1 / object.scaleX : 1,
object.strokeUniform ? 1 / object.scaleY : 1,
).multiply(object.strokeWidth)
return new fabric.Point(object.width + stroke.x, object.height + stroke.y)
}
// define a function that will define what the control does
export function actionHandler(eventData, transform, x, y) {
let polygon = transform.target,
currentControl = polygon.controls[polygon.__corner],
mouseLocalPosition = polygon.toLocalPoint(
new fabric.Point(x, y),
'center',
'center',
),
polygonBaseSize = getObjectSizeWithStroke(polygon),
size = polygon._getTransformedDimensions(0, 0)
polygon.points[currentControl.pointIndex] = {
x:
(mouseLocalPosition.x * polygonBaseSize.x) / size.x +
polygon.pathOffset.x,
y:
(mouseLocalPosition.y * polygonBaseSize.y) / size.y +
polygon.pathOffset.y,
}
return true
}
// define a function that can keep the polygon in the same position when we change its width/height/top/left
export function anchorWrapper(anchorIndex, fn) {
return function (eventData, transform, x, y) {
let fabricObject = transform.target
let originX =
fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
let absolutePoint = fabric.util.transformPoint(
{
x: originX,
y: originY,
},
fabricObject.calcTransformMatrix(),
)
let actionPerformed = fn(eventData, transform, x, y)
let newDim = fabricObject._setPositionDimensions({})
let polygonBaseSize = getObjectSizeWithStroke(fabricObject)
let newX =
(fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) /
polygonBaseSize.x
let newY =
(fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) /
polygonBaseSize.y
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5)
return actionPerformed
}
}
export const getDistance = (x1, y1, x2, y2) => {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
// 선의 길이를 계산하는 함수
export const calculateLineLength = (x1, y1, x2, y2) => {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
// 선과 텍스트를 그룹으로 묶는 함수
export const createGroupWithLineAndText = (line, text) => {
return new fabric.Group([line, text])
}
export const calculateShapeLength = (shape) => {
// 도형의 원래 길이를 가져옵니다.
const originalLength = shape.width
// 도형의 scaleX 값을 가져옵니다.
const scaleX = shape.scaleX
// 도형의 현재 길이를 계산합니다.
return originalLength * scaleX
}
/**
*
* @param {number} value
* @param {boolean} useDefault
* @param {string} delimeter
* @returns
* ex) 1,100 mm
*/
export const formattedWithComma = (value, unit = 'mm') => {
let formatterdData = value.toLocaleString('ko-KR')
if (unit === 'cm') {
formatterdData = value.toLocaleString('ko-KR') / 10
} else if (unit === 'm') {
formatterdData = value.toLocaleString('ko-KR') / 1000
}
return `${formatterdData} ${unit}`
}

View File

@ -9,7 +9,9 @@ export default function Headers() {
</Link>
<div className="space-x-4 text-xl">
<Link href="/intro">Intro</Link>
<Link href="/changelog">Changelog</Link>
<Link href="/roof">Roof</Link>
<Link href="/roof2">Roof2</Link>
</div>
</nav>
</div>

View File

@ -1,7 +1,7 @@
export default function Hero(props) {
return (
<div className="pt-48 flex justify-center">
<h1 className="text-6xl">{props.title}</h1>
<h1 className="text-6xl archivo-black-regular">{props.title}</h1>
</div>
)
}

View File

@ -1,7 +1,8 @@
import { createGroupWithLineAndText, getDistance } from '@/app/util/canvas-util'
import { addDistanceTextToPolygon, getDistance } from '@/util/canvas-util'
import { useCanvas } from '@/hooks/useCanvas'
import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid'
import { useEffect } from 'react'
export default function Roof() {
const {
@ -18,9 +19,88 @@ export default function Roof() {
attachCustomControlOnPolygon,
saveImage,
handleFlip,
updateTextOnLineChange,
} = useCanvas('canvas')
useEffect(() => {
let circle = new fabric.Circle({
radius: 40,
fill: 'rgba(200, 0, 0, 0.3)',
originX: 'center',
originY: 'center',
})
let text = new fabric.Textbox('AJLoveChina', {
originX: 'center',
originY: 'center',
textAlign: 'center',
fontSize: 12,
})
let group = new fabric.Group([circle, text], {
left: 100,
top: 100,
originX: 'center',
originY: 'center',
})
group.on('mousedblclick', () => {
// textForEditing is temporary obj,
// and will be removed after editing
console.log(text.type)
let textForEditing = new fabric.Textbox(text.text, {
originX: 'center',
originY: 'center',
textAlign: text.textAlign,
fontSize: text.fontSize,
left: group.left,
top: group.top,
})
// hide group inside text
text.visible = false
// note important, text cannot be hidden without this
group.addWithUpdate()
textForEditing.visible = true
// do not give controls, do not allow move/resize/rotation on this
textForEditing.hasConstrols = false
// now add this temporary obj to canvas
canvas.add(textForEditing)
canvas.setActiveObject(textForEditing)
// make the cursor showing
textForEditing.enterEditing()
textForEditing.selectAll()
// editing:exited means you click outside of the textForEditing
textForEditing.on('editing:exited', () => {
let newVal = textForEditing.text
let oldVal = text.text
// then we check if text is changed
if (newVal !== oldVal) {
text.set({
text: newVal,
visible: true,
})
// comment before, you must call this
group.addWithUpdate()
// we do not need textForEditing anymore
textForEditing.visible = false
canvas?.remove(textForEditing)
// optional, buf for better user experience
canvas?.setActiveObject(group)
}
})
})
canvas?.add(group)
}, [canvas])
const addRect = () => {
const rect = new fabric.Rect({
height: 200,
@ -92,9 +172,9 @@ export default function Roof() {
const trapezoid = new fabric.Polygon(
[
{ x: 100, y: 100 }, //
{ x: 300, y: 100 }, //
{ x: 250, y: 200 }, //
{ x: 150, y: 200 }, //
{ x: 500, y: 100 }, //
{ x: 750, y: 700 }, //
{ x: 250, y: 400 }, //
],
{
name: uuidv4(),
@ -102,35 +182,96 @@ export default function Roof() {
opacity: 0.4,
strokeWidth: 3,
selectable: true,
objectCaching: false,
},
)
attachCustomControlOnPolygon(trapezoid)
addShape(trapezoid)
const group = addDistanceTextToPolygon(trapezoid)
addGroupClickEvent(group)
canvas?.add(group)
canvas?.renderAll()
}
const addTextWithLine = () => {
const { x1, y1, x2, y2 } = { x1: 20, y1: 100, x2: 220, y2: 100 }
/**
* 시작X,시작Y,도착X,도착Y 좌표
*/
const horizontalLine = new fabric.Line([x1, y1, x2, y2], {
name: uuidv4(),
stroke: 'red',
strokeWidth: 3,
selectable: true,
// group group object
function addGroupClickEvent(group) {
group.on('selected', (e) => {
console.log(e)
})
group.on('mousedblclick', (e) => {
// textForEditing is temporary obj,
// and will be removed after editing
const pointer = canvas?.getPointer(e.e) //
let minDistance = Infinity
let closestTextbox = null
const groupPoint = group.getCenterPoint()
group.getObjects().forEach(function (object) {
if (object.type === 'textbox') {
// TextBox
const text = new fabric.Text(getDistance(x1, y1, x2, y2).toString(), {
fontSize: 20,
left: (x2 - x1) / 2,
top: y1 - 20,
const objectCenter = object.getCenterPoint() // TextBox
const dx = objectCenter.x + groupPoint.x - pointer.x
const dy = objectCenter.y + groupPoint.y - pointer.y
const distance = Math.sqrt(dx * dx + dy * dy) // TextBox
if (distance < minDistance) {
// TextBox
minDistance = distance
closestTextbox = object
}
}
})
let textForEditing = new fabric.Textbox(closestTextbox.text, {
originX: 'center',
originY: 'center',
textAlign: closestTextbox.textAlign,
fontSize: closestTextbox.fontSize,
left: closestTextbox.left + groupPoint.x,
top: closestTextbox.top + groupPoint.y,
})
// hide group inside text
closestTextbox.visible = false
// note important, text cannot be hidden without this
group.addWithUpdate()
textForEditing.visible = true
// do not give controls, do not allow move/resize/rotation on this
textForEditing.hasConstrols = false
// now add this temporary obj to canvas
canvas?.add(textForEditing)
canvas?.setActiveObject(textForEditing)
// make the cursor showing
textForEditing?.enterEditing()
textForEditing?.selectAll()
// editing:exited means you click outside of the textForEditing
textForEditing?.on('editing:exited', () => {
let newVal = textForEditing.text
// then we check if text is changed
closestTextbox.set({
text: newVal,
visible: true,
})
// comment before, you must call this
group.addWithUpdate()
// we do not need textForEditing anymore
textForEditing.visible = false
canvas?.remove(textForEditing)
// optional, buf for better user experience
canvas?.setActiveObject(group)
})
})
}
const group = createGroupWithLineAndText(horizontalLine, text)
addShape(group)
// .
group.on('modified', () => updateTextOnLineChange(group, text))
// IText polygon
function addTextModifiedEvent(text, polygon, index) {
text.on('editing:exited', function () {})
}
const randomColor = () => {
@ -232,12 +373,6 @@ export default function Roof() {
>
도형반전
</button>
<button
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={addTextWithLine}
>
숫자가 있는
</button>
</div>
<div

426
src/components/Roof2.jsx Normal file
View File

@ -0,0 +1,426 @@
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 QPolygon from '@/components/fabric/QPolygon'
import RangeSlider from './ui/RangeSlider'
import { useRecoilState, useRecoilValue } from 'recoil'
import {
canvasSizeState,
fontSizeState,
sortedPolygonArray,
} from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
export default function Roof2() {
const {
canvas,
handleRedo,
handleUndo,
setCanvasBackgroundWithDots,
saveImage,
} = useCanvas('canvas')
//canvas
const [canvasSize, setCanvasSize] = useRecoilState(canvasSizeState)
//canvas
const [verticalSize, setVerticalSize] = useState(canvasSize.vertical)
//canvas
const [horizontalSize, setHorizontalSize] = useState(canvasSize.horizontal)
//
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
const [sortedArray] = useRecoilState(sortedPolygonArray)
const [angle, setAngle] = useState(0)
const [showControl, setShowControl] = useState(false)
const {
mode,
changeMode,
handleClear,
fillCellInPolygon,
zoomIn,
zoomOut,
zoom,
togglePolygonLine,
handleOuterlinesTest2,
applyTemplateB,
} = useMode()
useEffect(() => {
if (!canvas) {
return
}
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], {
stroke: 'black',
strokeWidth: 2,
fontSize: fontSize,
})
canvas?.add(line)
}
}
const makePolygon = () => {
if (canvas) {
const polygon = new QPolygon(
[
{ x: 100, y: 100 },
{ x: 600, y: 200 },
{ x: 700, y: 800 },
{ x: 100, y: 800 },
],
{
fill: 'transparent',
stroke: 'black',
strokeWidth: 2,
selectable: true,
fontSize: fontSize,
},
canvas,
)
canvas?.add(polygon)
polygon.fillCell({ width: 50, height: 30, padding: 10 })
}
}
/**
* canvas 사이즈 변경 함수
*/
const canvasSizeMode = () => {
if (canvas) {
canvas.setWidth(horizontalSize)
canvas.setHeight(verticalSize)
canvas.renderAll()
setCanvasSize(() => ({
vertical: verticalSize,
horizontal: horizontalSize,
}))
}
}
/**
* 변경시
*/
useEffect(() => {
canvasSizeMode()
}, [verticalSize, horizontalSize])
const makeQPolygon = () => {
const type1 = [
{ x: 100, y: 100 },
{ x: 850, y: 100 },
{ x: 850, y: 800 },
{ x: 500, y: 800 },
{ x: 500, y: 400 },
{ x: 100, y: 400 },
]
const type2 = [
{ x: 100, y: 100 },
{ x: 100, y: 1000 },
{ x: 1000, y: 1000 },
{ x: 1000, y: 600 },
{ x: 550, y: 600 },
{ x: 550, y: 100 },
]
const type3 = [
{ x: 200, y: 100 },
{ x: 200, y: 800 },
{ x: 500, y: 800 },
{ x: 500, y: 300 },
{ x: 800, y: 300 },
{ x: 800, y: 100 },
]
const type4 = [
{ x: 150, y: 450 },
{ x: 150, y: 800 },
{ x: 750, y: 800 },
{ x: 750, y: 300 },
{ x: 550, y: 300 },
{ x: 550, y: 450 },
]
if (canvas) {
const polygon = new QPolygon(
type4,
{
fill: 'transparent',
stroke: 'black',
strokeWidth: 1,
selectable: true,
fontSize: fontSize,
name: 'QPolygon1',
},
canvas, //
)
canvas?.add(polygon)
handleOuterlinesTest2(polygon)
// const lines = togglePolygonLine(polygon)
// togglePolygonLine(lines[0])
}
}
const rotateShape = () => {
if (canvas) {
const activeObject = canvas?.getActiveObject()
if (activeObject) {
activeObject.rotate(angle)
canvas?.renderAll()
}
}
}
const makeQLine = () => {
if (canvas) {
const line = new QLine(
[50, 50, 200, 50],
{
stroke: 'black',
strokeWidth: 1,
fontSize: fontSize,
},
50,
)
canvas?.add(line)
}
}
const addBackgroundInPolygon = (polygon) => {
fabric.Image.fromURL('assets/img/check2.png', function (img) {
// .
const pattern = new fabric.Pattern({
source: img.getElement(),
repeat: 'repeat',
})
polygon.fillBackground(pattern)
})
}
function PolygonToLine() {
const polygon = canvas?.getActiveObject()
if (polygon.type !== 'QPolygon') {
return
}
const lines = togglePolygonLine(polygon)
}
const handleShowController = () => {
setShowControl(!showControl)
}
return (
<>
{canvas && (
<>
<div className=" my-8 w-full text:pretty">
<Button
className="m-1 p-2"
color={`${mode === Mode.DEFAULT ? 'primary' : 'default'}`}
onClick={fillCellInPolygon}
>
모드 DEFAULT
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.DRAW_LINE ? 'primary' : 'default'}`}
onClick={() => changeMode(canvas, Mode.DRAW_LINE)}
>
기준선 긋기 모드
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.EDIT ? 'primary' : 'default'}`}
onClick={() => changeMode(canvas, Mode.EDIT)}
>
에디팅모드
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`}
onClick={() => changeMode(canvas, Mode.TEMPLATE)}
>
템플릿(기둥)
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`}
onClick={() => {}}
>
템플릿(A 패턴)
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`}
onClick={applyTemplateB}
>
템플릿(B 패턴)
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.TEXTBOX ? 'primary' : 'default'}`}
onClick={() => changeMode(canvas, Mode.TEXTBOX)}
>
텍스트박스 모드
</Button>
<Button
className="m-1 p-2"
color={`${mode === Mode.DRAW_RECT ? 'primary' : 'default'}`}
onClick={() => changeMode(canvas, Mode.DRAW_RECT)}
>
사각형 생성 모드
</Button>
<Button className="m-1 p-2" onClick={handleUndo}>
Undo
</Button>
<Button className="m-1 p-2" onClick={handleRedo}>
Redo
</Button>
<Button className="m-1 p-2" onClick={handleClear}>
clear
</Button>
<Button className="m-1 p-2" onClick={zoomIn}>
확대
</Button>
<Button className="m-1 p-2" onClick={zoomOut}>
축소
</Button>
현재 : {zoom}%
<Button className="m-1 p-2" onClick={makeRect}>
사각형만들기
</Button>
<Button className="m-1 p-2" onClick={makeLine}>
추가
</Button>
<Button className="m-1 p-2" onClick={makePolygon}>
다각형 추가
</Button>
<Button
className="m-1 p-2"
onClick={() => {
setCanvasBackgroundWithDots(canvas, 10)
}}
>
점선 추가
</Button>
<Button
className="m-1 p-2"
onClick={() => {
setCanvasBackgroundWithDots(canvas, 20)
}}
>
점선 추가
</Button>
<Button className="m-1 p-2" onClick={saveImage}>
저장
</Button>
<Button className="m-1 p-2" onClick={makeQPolygon}>
QPolygon
</Button>
<Button className="m-1 p-2" onClick={rotateShape}>
회전
</Button>
<Button className="m-1 p-2" onClick={makeQLine}>
QLine
</Button>
<Button className="m-1 p-2" onClick={PolygonToLine}>
PolygonToLine
</Button>
<Button
className="m-1 p-2"
color={`${showControl ? 'primary' : 'default'}`}
onClick={handleShowController}
>
canvas 컨트롤러 {`${showControl ? '숨기기' : '보이기'}`}
</Button>
</div>
<div
className={
showControl
? `flex justify-center flex-col items-center`
: `hidden`
}
>
<div className="m-2 p-2 w-80">
<RangeSlider
title={`각도${angle}`}
initValue={angle}
min="0"
step="1"
max="360"
onchange={setAngle}
/>
</div>
<div className="m-2 p-2 w-80">
<RangeSlider
title={`canvas 가로 사이즈${horizontalSize}`}
initValue={horizontalSize}
min="500"
step="100"
max="2000"
onchange={setHorizontalSize}
/>
</div>
<div className="m-2 p-2 w-80">
<RangeSlider
title={`canvas 세로 사이즈${verticalSize}`}
initValue={verticalSize}
min="500"
step="100"
max="2000"
onchange={setVerticalSize}
/>
</div>
<div className="m-2 p-2 w-80">
<RangeSlider
title={`글자 크기${fontSize}`}
initValue={fontSize}
onchange={setFontSize}
/>
</div>
</div>
</>
)}
<div className="flex justify-start my-8 mx-2 w-full">
<canvas id="canvas" style={{ border: '1px solid black' }} />
</div>
</>
)
}

View File

@ -0,0 +1,108 @@
import { fabric } from 'fabric'
export class QLine extends fabric.Group {
line
text
fontSize
length = 0
x1
y1
x2
y2
direction
type = 'QLine'
parent
#lengthTxt = 0
constructor(points, option, lengthTxt) {
const [x1, y1, x2, y2] = points
if (!option.fontSize) {
throw new Error('Font size is required.')
}
const line = new fabric.Line(points, { ...option, strokeWidth: 1 })
super([line], {})
this.x1 = x1
this.y1 = y1
this.x2 = x2
this.y2 = y2
this.line = line
this.fontSize = option.fontSize
this.direction = option.direction
this.parent = option.parent
if (lengthTxt > 0) {
this.#lengthTxt = Number(lengthTxt)
}
this.#init()
this.#addControl()
}
#init() {
this.#addLengthText(true)
}
#addControl() {
this.on('moving', () => {
this.#addLengthText(false)
})
this.on('modified', (e) => {
this.#addLengthText(false)
})
this.on('selected', () => {
Object.keys(this.controls).forEach((controlKey) => {
if (controlKey !== 'ml' && controlKey !== 'mr') {
this.setControlVisible(controlKey, false)
}
})
})
}
#addLengthText(isFirst) {
if (this.text) {
this.removeWithUpdate(this.text)
this.text = null
}
if (isFirst && this.#lengthTxt > 0) {
const text = new fabric.Textbox(this.#lengthTxt.toFixed(0).toString(), {
left: (this.x1 + this.x2) / 2,
top: (this.y1 + this.y2) / 2,
fontSize: this.fontSize,
})
this.length = this.#lengthTxt
this.text = text
this.addWithUpdate(text)
return
}
const scaleX = this.scaleX
const scaleY = this.scaleY
const x1 = this.left
const y1 = this.top
const x2 = this.left + this.width * scaleX
const y2 = this.top + this.height * scaleY
const dx = x2 - x1
const dy = y2 - y1
this.length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(0))
const text = new fabric.Textbox(this.length.toFixed(0).toString(), {
left: (x1 + x2) / 2,
top: (y1 + y2) / 2,
fontSize: this.fontSize,
})
this.text = text
this.addWithUpdate(text)
}
setFontSize(fontSize) {
this.fontSize = fontSize
this.text.set({ fontSize })
this.addWithUpdate()
}
}

View File

@ -0,0 +1,924 @@
import { fabric } from 'fabric'
import {
distanceBetweenPoints,
findTopTwoIndexesByDistance,
getDegreeByChon,
getDirectionByPoint,
getRoofHeight,
getRoofHypotenuse,
sortedPoints,
} from '@/util/canvas-util'
import { QLine } from '@/components/fabric/QLine'
export default class QPolygon extends fabric.Group {
type = 'QPolygon'
polygon
points
texts = []
lines = []
canvas
fontSize
qCells = []
name
shape = 0 // 점 6개일때의 shape 모양
helpPoints = []
helpLines = []
constructor(points, options, canvas) {
if (points.length !== 4 && points.length !== 6) {
throw new Error('Points must be 4 or 6.')
}
if (!options.fontSize) {
throw new Error('Font size is required.')
}
const sortPoints = sortedPoints(points)
const polygon = new fabric.Polygon(sortPoints, options)
super([polygon], {})
this.fontSize = options.fontSize
this.points = sortPoints
this.polygon = polygon
this.name = options.name
this.#init()
this.#addEvent()
this.#initLines()
this.setShape()
}
#initLines() {
this.points.forEach((point, i) => {
const nextPoint = this.points[(i + 1) % this.points.length]
const line = new QLine([point.x, point.y, nextPoint.x, nextPoint.y], {
stroke: this.stroke,
strokeWidth: this.strokeWidth,
fontSize: this.fontSize,
direction: getDirectionByPoint(point, nextPoint),
})
this.lines.push(line)
})
}
#init() {
this.#addLengthText()
}
#addEvent() {
this.on('scaling', (e) => {
this.#updateLengthText()
})
this.on('selected', function () {
// 모든 컨트롤 떼기
Object.keys(this.controls).forEach((controlKey) => {
if (controlKey !== 'mtr') {
this.setControlVisible(controlKey, false)
}
})
})
}
setFontSize(fontSize) {
this.fontSize = fontSize
this.texts.forEach((text) => {
text.set({ fontSize })
})
this.getObjects().forEach((obj) => {
if (obj.type[0] === 'Q') {
obj.setFontSize(fontSize)
}
})
this.addWithUpdate()
}
#addLengthText() {
if (this.texts.length > 0) {
this.texts.forEach((text) => {
this.canvas.remove(text)
})
this.texts = []
}
const points = this.points
points.forEach((start, i) => {
const end = points[(i + 1) % points.length]
const dx = end.x - start.x
const dy = end.y - start.y
const length = Math.sqrt(dx * dx + dy * dy)
const midPoint = new fabric.Point(
(start.x + end.x) / 2,
(start.y + end.y) / 2,
)
// Create new text object if it doesn't exist
const text = new fabric.Text(length.toFixed(0), {
left: midPoint.x,
top: midPoint.y,
fontSize: this.fontSize,
selectable: false,
})
this.texts.push(text)
this.addWithUpdate(text)
})
this.canvas.renderAll()
}
#updateLengthText() {
const points = this.getCurrentPoints()
points.forEach((start, i) => {
const end = points[(i + 1) % points.length]
const dx = end.x - start.x
const dy = end.y - start.y
const length = Math.sqrt(dx * dx + dy * dy)
// Update the text object with the new length
this.texts[i].set({ text: length.toFixed(0) })
})
this.canvas.renderAll()
}
fillCell(cell = { width: 50, height: 100, padding: 10 }) {
const points = this.getCurrentPoints()
let bounds
try {
bounds = fabric.util.makeBoundingBoxFromPoints(points)
} catch (error) {
alert('다각형의 꼭지점이 4개 이상이어야 합니다.')
return
}
for (
let x = bounds.left;
x < bounds.left + bounds.width;
x += cell.width + cell.padding
) {
for (
let y = bounds.top;
y < bounds.top + bounds.height;
y += cell.height + cell.padding
) {
const rect = new fabric.Rect({
left: x,
top: y,
width: cell.width,
height: cell.height,
fill: 'transparent',
stroke: 'black',
selectable: false,
})
const rectPoints = [
new fabric.Point(rect.left, rect.top),
new fabric.Point(rect.left + rect.width, rect.top),
new fabric.Point(rect.left, rect.top + rect.height),
new fabric.Point(rect.left + rect.width, rect.top + rect.height),
]
const isInside = rectPoints.every(
(rectPoint) =>
this.inPolygon(rectPoint) &&
this.#distanceFromEdge(rectPoint) >= cell.padding,
)
if (isInside) {
this.addWithUpdate(rect)
}
}
}
this.canvas.renderAll()
}
/**
* this.lines의 direction이 top line의 모든 합이 bottom line의 모든 합과 같은지 확인
* this.lines의 direction이 left line의 모든 합이 right line의 모든 합과 같은지 확인
* return {boolean}
*/
isValid() {
const leftLinesLengthSum = this.lines
.filter((line) => line.direction === 'left')
.reduce((sum, line) => sum + line.length, 0)
const rightLinesLengthSum = this.lines
.filter((line) => line.direction === 'right')
.reduce((sum, line) => sum + line.length, 0)
const topLinesLengthSum = this.lines
.filter((line) => line.direction === 'top')
.reduce((sum, line) => sum + line.length, 0)
const bottomLinesLengthSum = this.lines
.filter((line) => line.direction === 'bottom')
.reduce((sum, line) => sum + line.length, 0)
return (
leftLinesLengthSum === rightLinesLengthSum &&
topLinesLengthSum === bottomLinesLengthSum
)
}
inPolygon(point) {
const vertices = this.getCurrentPoints()
let intersects = 0
for (let i = 0; i < vertices.length; i++) {
let vertex1 = vertices[i]
let vertex2 = vertices[(i + 1) % vertices.length]
if (vertex1.y > vertex2.y) {
let tmp = vertex1
vertex1 = vertex2
vertex2 = tmp
}
if (point.y === vertex1.y || point.y === vertex2.y) {
point.y += 0.01
}
if (point.y <= vertex1.y || point.y > vertex2.y) {
continue
}
let xInt =
((point.y - vertex1.y) * (vertex2.x - vertex1.x)) /
(vertex2.y - vertex1.y) +
vertex1.x
if (xInt < point.x) {
intersects++
}
}
return intersects % 2 === 1
}
#distanceFromEdge(point) {
const vertices = this.getCurrentPoints()
let minDistance = Infinity
for (let i = 0; i < vertices.length; i++) {
let vertex1 = vertices[i]
let vertex2 = vertices[(i + 1) % vertices.length]
const dx = vertex2.x - vertex1.x
const dy = vertex2.y - vertex1.y
const t =
((point.x - vertex1.x) * dx + (point.y - vertex1.y) * dy) /
(dx * dx + dy * dy)
let closestPoint
if (t < 0) {
closestPoint = vertex1
} else if (t > 1) {
closestPoint = vertex2
} else {
closestPoint = new fabric.Point(vertex1.x + t * dx, vertex1.y + t * dy)
}
const distance = distanceBetweenPoints(point, closestPoint)
if (distance < minDistance) {
minDistance = distance
}
}
return minDistance
}
setViewLengthText(boolean) {
this.texts.forEach((text) => {
text.visible = boolean
})
this.canvas.renderAll()
}
getCurrentPoints() {
const scaleX = this.scaleX
const scaleY = this.scaleY
const left = this.left
const top = this.top
// 시작점
const point = this.points[0]
const movingX = left - point.x * scaleX
const movingY = top - point.y * scaleY
return this.points.map((point) => {
return {
x: point.x * scaleX + movingX,
y: point.y * scaleY + movingY,
}
})
}
fillBackground(pattern) {
this.polygon.set({ fill: pattern })
this.canvas.requestRenderAll()
}
// 보조선 그리기
drawHelpLine(chon = 4) {
if (!this.isValid()) {
return
}
if (this.lines.length === 4) {
this.#drawHelpLineInRect(chon)
} else if (this.lines.length === 6) {
// TODO : 6각형
this.#drawHelpLineInHexagon(chon)
} else if (this.lines.length === 8) {
// TODO : 8각형
this.#drawHelpLineInOctagon(chon)
}
}
/**
* 현재 6개만 가능
*/
setShape() {
let shape = 0
if (this.lines.length !== 6) {
return
}
//외각선 기준
const topIndex = findTopTwoIndexesByDistance(this.lines).sort(
(a, b) => a - b,
) //배열중에 큰 2값을 가져옴 TODO: 나중에는 인자로 받아서 다각으로 수정 해야됨
//일단 배열 6개 짜리 기준의 선 번호
if (topIndex[0] === 4) {
if (topIndex[1] === 5) {
//1번
shape = 1
}
} else if (topIndex[0] === 1) {
//4번
if (topIndex[1] === 2) {
shape = 4
}
} else if (topIndex[0] === 0) {
if (topIndex[1] === 1) {
//2번
shape = 2
} else if (topIndex[1] === 5) {
//3번
shape = 3
}
}
this.shape = shape
}
/**
* 현재 6개만 가능
* @returns {number}
*/
getShape() {
return this.shape
}
#drawHelpLineInRect(chon) {
let type = 1
let smallestLength = Infinity
let maxLength = 0
this.lines.forEach((line) => {
if (line.length < smallestLength) {
smallestLength = line.length
}
if (line.length > maxLength) {
maxLength = line.length
}
})
// QPolygon 객체의 모든 선들을 가져옵니다.
const lines = [...this.lines]
// 이 선들을 길이에 따라 정렬합니다.
lines.sort((a, b) => a.length - b.length)
// 정렬된 배열에서 가장 작은 두 선을 선택합니다.
let smallestLines
if (smallestLength === maxLength) {
// 정사각형인 경우 0, 2번째 라인이 가장 짧은 라인
smallestLines = [lines[0], lines[2]]
} else {
smallestLines = lines.slice(0, 2)
}
let needPlusLine
let needMinusLine
const direction = smallestLines[0].direction
if (direction === 'top' || direction === 'bottom') {
needPlusLine =
smallestLines[0].x1 < smallestLines[1].x1
? smallestLines[0]
: smallestLines[1]
needMinusLine =
needPlusLine === smallestLines[0] ? smallestLines[1] : smallestLines[0]
type = 1 // 가로가 긴 사각형
}
if (direction === 'left' || direction === 'right') {
needPlusLine =
smallestLines[0].y1 < smallestLines[1].y1
? smallestLines[0]
: smallestLines[1]
needMinusLine =
needPlusLine === smallestLines[0] ? smallestLines[1] : smallestLines[0]
type = 2 // 세로가 긴 사각형
}
let point1
let point2
if (type === 1) {
point1 = {
x: needPlusLine.x1 + smallestLength / 2,
y:
needPlusLine.y1 > needPlusLine.y2
? needPlusLine.y1 - smallestLength / 2
: needPlusLine.y2 - smallestLength / 2,
}
point2 = {
x: needMinusLine.x1 - smallestLength / 2,
y:
needMinusLine.y1 > needMinusLine.y2
? needMinusLine.y1 - smallestLength / 2
: needMinusLine.y2 - smallestLength / 2,
}
} else if (type === 2) {
point1 = {
x:
needPlusLine.x1 > needPlusLine.x2
? needPlusLine.x1 - smallestLength / 2
: needPlusLine.x2 - smallestLength / 2,
y: needPlusLine.y1 + smallestLength / 2,
}
point2 = {
x:
needMinusLine.x1 > needMinusLine.x2
? needMinusLine.x1 - smallestLength / 2
: needMinusLine.x2 - smallestLength / 2,
y: needMinusLine.y1 - smallestLength / 2,
}
}
// 빗변1
const realLine1 = new QLine(
[needPlusLine.x1, needPlusLine.y1, point1.x, point1.y],
{ fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 },
getRoofHypotenuse(smallestLength / 2),
)
// 빗변2
const realLine2 = new QLine(
[needPlusLine.x2, needPlusLine.y2, point1.x, point1.y],
{ fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 },
getRoofHypotenuse(smallestLength / 2),
)
// 빗변3
const realLine3 = new QLine(
[needMinusLine.x1, needMinusLine.y1, point2.x, point2.y],
{ fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 },
getRoofHypotenuse(smallestLength / 2),
)
// 빗변4
const realLine4 = new QLine(
[needMinusLine.x2, needMinusLine.y2, point2.x, point2.y],
{ fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 },
getRoofHypotenuse(smallestLength / 2),
)
let centerPoint1
let centerPoint2
if (type === 1) {
centerPoint1 = { x: point1.x - smallestLength / 2, y: point1.y }
centerPoint2 = { x: point2.x + smallestLength / 2, y: point2.y }
} else if (type === 2) {
centerPoint1 = { x: point1.x, y: point1.y - smallestLength / 2 }
centerPoint2 = { x: point2.x, y: point2.y + smallestLength / 2 }
}
// 옆으로 누워있는 지붕의 높이
const realLine5 = new QLine(
[point1.x, point1.y, centerPoint1.x, centerPoint1.y],
{
fontSize: this.fontSize,
stroke: 'black',
strokeWidth: 1,
strokeDashArray: [5, 5],
},
getRoofHeight(smallestLength / 2, getDegreeByChon(chon)),
)
// 옆으로 누워있는 지붕의 높이
const realLine6 = new QLine(
[point2.x, point2.y, centerPoint2.x, centerPoint2.y],
{
fontSize: this.fontSize,
stroke: 'black',
strokeWidth: 1,
strokeDashArray: [5, 5],
},
getRoofHeight(smallestLength / 2, getDegreeByChon(chon)),
)
// 용마루
const ridge = new QLine([point1.x, point1.y, point2.x, point2.y], {
fontSize: this.fontSize,
stroke: 'black',
strokeWidth: 1,
})
this.addWithUpdate(realLine1)
this.addWithUpdate(realLine2)
this.addWithUpdate(realLine3)
this.addWithUpdate(realLine4)
this.addWithUpdate(realLine5)
this.addWithUpdate(realLine6)
if (smallestLength !== maxLength) {
// 정사각형이 아닌경우에만 용마루를 추가한다.
this.canvas.add(ridge)
}
}
#drawHelpLineInHexagon(chon) {
let type = this.shape
// 1 = 0, 3
// 2 = 2, 5
// 3 = 1, 4
// 4 = 0, 3
// 라인 기준점 1,2
let lines, lines2
// 용마루 시작점 2개
let vPoint1, vPoint2
// 용마루 시작점과 만나는 지붕의 중앙
let centerPoint1, centerPoint2
// 가장 긴 라인
let longestLines
// 용마루 길이
let ridgeLength = 0
let ridgeStartPoint1, ridgeStartPoint2
let ridgeEndPoint1, ridgeEndPoint2
let ridgeLength1, ridgeLength2
let ridgeHelpLinePoint1, ridgeHelpLinePoint2
if (type === 1) {
lines = [this.lines[0], this.lines[3]]
lines2 = [this.lines[1], this.lines[4]]
longestLines = [this.lines[4], this.lines[5]]
ridgeLength1 = lines2[0].length
ridgeLength2 = longestLines[0].length - lines[1].length
vPoint1 = {
x: lines[0].x1 + lines[0].length / 2,
y: lines[0].y1 + lines[0].length / 2,
}
vPoint2 = {
x: lines[1].x1 + lines[1].length / 2,
y: lines[1].y1 - lines[1].length / 2,
}
centerPoint1 = {
x: (lines[0].x1 + lines[0].x2) / 2,
y: (lines[0].y1 + lines[0].y2) / 2,
}
centerPoint2 = {
x: (lines[1].x1 + lines[1].x2) / 2,
y: (lines[1].y1 + lines[1].y2) / 2,
}
ridgeEndPoint1 = [
vPoint1.x,
vPoint1.y,
vPoint1.x + ridgeLength1,
vPoint1.y,
]
ridgeEndPoint2 = [
vPoint2.x,
vPoint2.y,
vPoint2.x,
vPoint2.y - ridgeLength2,
]
ridgeHelpLinePoint1 = [
lines2[0].x2,
lines2[0].y2,
ridgeEndPoint1[2],
ridgeEndPoint1[3],
]
ridgeHelpLinePoint2 = [
lines2[1].x2,
lines2[1].y2,
ridgeEndPoint2[2],
ridgeEndPoint2[3],
]
} else if (type === 2) {
lines = [this.lines[2], this.lines[5]]
lines2 = [this.lines[0], this.lines[3]]
longestLines = [this.lines[0], this.lines[1]]
ridgeLength1 = lines2[1].length
ridgeLength2 = longestLines[0].length - lines[1].length
vPoint1 = {
x: lines[0].x1 - lines[0].length / 2,
y: lines[0].y1 - lines[0].length / 2,
}
vPoint2 = {
x: lines[1].x1 - lines[1].length / 2,
y: lines[1].y1 + lines[1].length / 2,
}
centerPoint1 = {
x: (lines[0].x1 + lines[0].x2) / 2,
y: (lines[0].y1 + lines[0].y2) / 2,
}
centerPoint2 = {
x: (lines[1].x1 + lines[1].x2) / 2,
y: (lines[1].y1 + lines[1].y2) / 2,
}
ridgeEndPoint1 = [
vPoint1.x,
vPoint1.y,
vPoint1.x - ridgeLength1,
vPoint1.y,
]
ridgeEndPoint2 = [
vPoint2.x,
vPoint2.y,
vPoint2.x,
vPoint2.y + ridgeLength2,
]
ridgeHelpLinePoint1 = [
lines2[1].x2,
lines2[1].y2,
ridgeEndPoint1[2],
ridgeEndPoint1[3],
]
ridgeHelpLinePoint2 = [
lines2[0].x2,
lines2[0].y2,
ridgeEndPoint2[2],
ridgeEndPoint2[3],
]
} else if (type === 3) {
lines = [this.lines[1], this.lines[4]]
lines2 = [this.lines[2], this.lines[5]]
longestLines = [this.lines[0], this.lines[5]]
ridgeLength1 = this.lines[3].length
ridgeLength2 = longestLines[0].length - lines[0].length
vPoint1 = {
x: lines[0].x1 + lines[0].length / 2,
y: lines[0].y1 - lines[0].length / 2,
}
vPoint2 = {
x: lines[1].x1 - lines[1].length / 2,
y: lines[1].y1 - lines[1].length / 2,
}
centerPoint1 = {
x: (lines[0].x1 + lines[0].x2) / 2,
y: (lines[0].y1 + lines[0].y2) / 2,
}
centerPoint2 = {
x: (lines[1].x1 + lines[1].x2) / 2,
y: (lines[1].y1 + lines[1].y2) / 2,
}
ridgeEndPoint1 = [
vPoint1.x,
vPoint1.y,
vPoint1.x,
vPoint1.y - ridgeLength2,
]
ridgeEndPoint2 = [
vPoint2.x,
vPoint2.y,
vPoint2.x - ridgeLength1,
vPoint2.y,
]
ridgeHelpLinePoint1 = [
lines2[1].x2,
lines2[1].y2,
ridgeEndPoint1[2],
ridgeEndPoint1[3],
]
ridgeHelpLinePoint2 = [
lines2[0].x2,
lines2[0].y2,
ridgeEndPoint2[2],
ridgeEndPoint2[3],
]
} else if (type === 4) {
lines = [this.lines[0], this.lines[3]]
lines2 = [this.lines[1], this.lines[4]]
longestLines = [this.lines[1], this.lines[2]]
ridgeLength1 = longestLines[0].length - lines[0].length
ridgeLength2 = this.lines[4].length
vPoint1 = {
x: lines[0].x1 + lines[0].length / 2,
y: lines[0].y1 + lines[0].length / 2,
}
vPoint2 = {
x: lines[1].x1 - lines[1].length / 2,
y: lines[1].y1 + lines[1].length / 2,
}
centerPoint1 = {
x: (lines[0].x1 + lines[0].x2) / 2,
y: (lines[0].y1 + lines[0].y2) / 2,
}
centerPoint2 = {
x: (lines[1].x1 + lines[1].x2) / 2,
y: (lines[1].y1 + lines[1].y2) / 2,
}
ridgeEndPoint1 = [
vPoint1.x,
vPoint1.y,
vPoint1.x + ridgeLength1,
vPoint1.y,
]
ridgeEndPoint2 = [
vPoint2.x,
vPoint2.y,
vPoint2.x,
vPoint2.y + ridgeLength2,
]
ridgeHelpLinePoint1 = [
lines2[0].x2,
lines2[0].y2,
ridgeEndPoint1[2],
ridgeEndPoint1[3],
]
ridgeHelpLinePoint2 = [
lines2[1].x2,
lines2[1].y2,
ridgeEndPoint2[2],
ridgeEndPoint2[3],
]
}
const realLine1 = new QLine(
[lines[0].x1, lines[0].y1, vPoint1.x, vPoint1.y],
{ fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 },
getRoofHypotenuse(lines[0].length / 2),
)
const realLine2 = new QLine(
[lines[0].x2, lines[0].y2, vPoint1.x, vPoint1.y],
{ fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 },
getRoofHypotenuse(lines[0].length / 2),
)
const realLine3 = new QLine(
[lines[1].x1, lines[1].y1, vPoint2.x, vPoint2.y],
{ fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 },
getRoofHypotenuse(lines[1].length / 2),
)
const realLine4 = new QLine(
[lines[1].x2, lines[1].y2, vPoint2.x, vPoint2.y],
{ fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 },
getRoofHypotenuse(lines[1].length / 2),
)
// 옆으로 누워있는 지붕의 높이 점선
const realLine5 = new QLine(
[vPoint1.x, vPoint1.y, centerPoint1.x, centerPoint1.y],
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
strokeDashArray: [5, 5],
},
getRoofHeight(lines[0].length / 2, getDegreeByChon(chon)),
)
// 옆으로 누워있는 지붕의 높이 점선
const realLine6 = new QLine(
[vPoint2.x, vPoint2.y, centerPoint2.x, centerPoint2.y],
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
strokeDashArray: [5, 5],
},
getRoofHeight(lines[1].length / 2, getDegreeByChon(chon)),
)
// 용마루 보조선
const ridgeHelpLine1 = new QLine(
ridgeHelpLinePoint1,
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
},
getRoofHypotenuse(lines[0].length / 2),
)
// 용마루 보조선
const ridgeHelpLine2 = new QLine(
ridgeHelpLinePoint2,
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
},
getRoofHypotenuse(lines[1].length / 2),
)
// 용마루
const ridge1 = new QLine(
[vPoint1.x, vPoint1.y, ridgeEndPoint1[2], ridgeEndPoint1[3]],
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
},
)
// 용마루
const ridge2 = new QLine(
[vPoint2.x, vPoint2.y, ridgeEndPoint2[2], ridgeEndPoint2[3]],
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
},
)
const ridgeEndLine = new QLine(
[
ridgeEndPoint1[2],
ridgeEndPoint1[3],
ridgeEndPoint2[2],
ridgeEndPoint2[3],
],
{
fontSize: this.fontSize,
stroke: 'blue',
strokeWidth: 1,
},
Math.abs(realLine1.length - realLine3.length),
)
this.helpLines = [
realLine1,
realLine2,
realLine3,
realLine4,
realLine5,
realLine6,
ridge1,
ridge2,
ridgeEndLine,
]
this.addWithUpdate(realLine1)
this.addWithUpdate(realLine2)
this.addWithUpdate(realLine3)
this.addWithUpdate(realLine4)
this.addWithUpdate(realLine5)
this.addWithUpdate(realLine6)
this.addWithUpdate(ridgeHelpLine1)
this.addWithUpdate(ridgeHelpLine2)
this.addWithUpdate(ridge1)
this.addWithUpdate(ridge2)
this.addWithUpdate(ridgeEndLine)
this.canvas.renderAll()
}
#drawHelpLineInOctagon(chon) {}
}

View File

@ -0,0 +1,98 @@
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)
})
}
}

View File

@ -0,0 +1,28 @@
export default function RangeSlider(
props = { title: 'default title', initValue: 0, onchange: () => {}, step: 1, min:0, max:100},
) {
const { title, initValue, onchange, step, min, max } = props
const handleChange = (e) => {
// console.log(e.target.value)
onchange(e.target.value)
}
return (
<>
<label htmlFor="default-range" className="block mb-2 text-gray-900">
{title}
</label>
<input
id="default-range"
type="range"
value={initValue}
min={min}
max={max}
step={step}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
onChange={handleChange}
/>
</>
)
}

View File

@ -3,20 +3,21 @@ import { fabric } from 'fabric'
import {
actionHandler,
anchorWrapper,
calculateShapeLength,
polygonPositionHandler,
} from '@/app/util/canvas-util'
} from '@/util/canvas-util'
const CANVAS = {
WIDTH: 1000,
HEIGHT: 1000,
}
import { useRecoilState } from 'recoil'
import { canvasSizeState, fontSizeState } from '@/store/canvasAtom'
import QPolygon from '@/components/fabric/QPolygon'
import { QLine } from '@/components/fabric/QLine'
import QRect from '@/components/fabric/QRect'
export function useCanvas(id) {
const [canvas, setCanvas] = useState()
const [isLocked, setIsLocked] = useState(false)
const [history, setHistory] = useState([])
const [canvasSize] = useRecoilState(canvasSizeState)
const [fontSize] = useRecoilState(fontSizeState)
const points = useRef([])
/**
@ -24,9 +25,10 @@ export function useCanvas(id) {
*/
useEffect(() => {
const c = new fabric.Canvas(id, {
height: CANVAS.HEIGHT,
width: CANVAS.WIDTH,
height: canvasSize.vertical,
width: canvasSize.horizontal,
backgroundColor: 'white',
selection: false,
})
// settings for all canvas in the app
@ -36,18 +38,60 @@ export function useCanvas(id) {
fabric.Object.prototype.cornerStrokeColor = '#2BEBC8'
fabric.Object.prototype.cornerSize = 6
QPolygon.prototype.canvas = c
QLine.prototype.canvas = c
QRect.prototype.canvas = c
setCanvas(c)
return () => {
c.dispose()
}
}, [])
useEffect(() => {
// canvas 사이즈가 변경되면 다시
removeEventOnCanvas()
addEventOnCanvas()
}, [canvasSize])
useEffect(() => {
canvas
?.getObjects()
.filter(
(obj) =>
obj.type === 'textbox' ||
obj.type === 'text' ||
obj.type === 'i-text',
)
.forEach((obj) => {
obj.set({ fontSize: fontSize })
})
canvas
?.getObjects()
.filter(
(obj) =>
obj.type === 'QLine' ||
obj.type === 'QPolygon' ||
obj.type === 'QRect',
)
.forEach((obj) => {
obj.setFontSize(fontSize)
})
canvas?.renderAll()
}, [fontSize])
/**
* 캔버스 초기화
*/
useEffect(() => {
if (canvas) {
initialize()
canvas?.on('object:added', onChange)
canvas?.on('object:modified', onChange)
canvas?.on('object:removed', onChange)
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:out', removeMouseLines)
}
}, [canvas])
const addEventOnCanvas = () => {
@ -92,11 +136,11 @@ export function useCanvas(id) {
canvas?.clear()
// 기존 이벤트가 있을 경우 제거한다.
removeEventOnCanvas()
// removeEventOnCanvas()
// 작업 후에 event를 추가해준다.
addEventOnCanvas()
// addEventOnCanvas()
}
/**
@ -111,7 +155,7 @@ export function useCanvas(id) {
const onChange = (e) => {
const target = e.target
if (target) {
settleDown(target)
// settleDown(target)
}
if (!isLocked) {
@ -133,25 +177,23 @@ export function useCanvas(id) {
// 가로선을 그립니다.
const horizontalLine = new fabric.Line(
[0, pointer.y, CANVAS.WIDTH, pointer.y],
[0, pointer.y, canvasSize.horizontal, pointer.y],
{
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
strokeDashArray: [5, 5],
},
)
// 세로선을 그립니다.
const verticalLine = new fabric.Line(
[pointer.x, 0, pointer.x, CANVAS.HEIGHT],
[pointer.x, 0, pointer.x, canvasSize.vertical],
{
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
strokeDashArray: [5, 5],
},
)
@ -208,15 +250,16 @@ export function useCanvas(id) {
*/
const handleUndo = () => {
if (canvas) {
if (canvas._objects.length > 0) {
const poppedObject = canvas._objects.pop()
if (canvas?._objects.length > 0) {
const poppedObject = canvas?._objects.pop()
setHistory((prev) => {
if (prev === undefined) {
return poppedObject ? [poppedObject] : []
}
return poppedObject ? [...prev, poppedObject] : prev
})
canvas.renderAll()
canvas?.renderAll()
}
}
}
@ -225,21 +268,13 @@ export function useCanvas(id) {
if (canvas && history) {
if (history.length > 0) {
setIsLocked(true)
canvas.add(history[history.length - 1])
canvas?.add(history[history.length - 1])
const newHistory = history.slice(0, -1)
setHistory(newHistory)
}
}
}
/**
* 해당 캔버스를 비운다.
*/
const handleClear = () => {
canvas?.clear()
initialize()
}
/**
* 선택한 도형을 복사한다.
*/
@ -440,6 +475,7 @@ export function useCanvas(id) {
const lastControl = poly.points?.length - 1
poly.cornerStyle = 'rect'
poly.cornerColor = 'rgba(0,0,255,0.5)'
poly.objectCaching = false
poly.controls = poly.points.reduce(function (acc, point, index) {
acc['p' + index] = new fabric.Control({
positionHandler: polygonPositionHandler,
@ -494,11 +530,45 @@ export function useCanvas(id) {
canvas?.renderAll()
}
// 선의 길이가 변경될 때마다 텍스트를 업데이트하는 함수
const updateTextOnLineChange = (group, text) => {
const length = calculateShapeLength(group)
text.set({ text: length.toString() })
canvas?.renderAll()
function fillCanvasWithDots(canvas, gap) {
const width = canvas.getWidth()
const height = canvas.getHeight()
for (let x = 0; x < width; x += gap) {
for (let y = 0; y < height; y += gap) {
const circle = new fabric.Circle({
radius: 1,
fill: 'black',
left: x,
top: y,
selectable: false,
})
canvas.add(circle)
}
}
canvas.renderAll()
}
const setCanvasBackgroundWithDots = (canvas, gap) => {
// Create a new canvas and fill it with dots
const tempCanvas = new fabric.StaticCanvas()
tempCanvas.setDimensions({
width: canvas.getWidth(),
height: canvas.getHeight(),
})
fillCanvasWithDots(tempCanvas, gap)
// Convert the dotted canvas to an image
const dataUrl = tempCanvas.toDataURL({ format: 'png' })
// Set the image as the background of the original canvas
fabric.Image.fromURL(dataUrl, function (img) {
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
scaleX: canvas.width / img.width,
scaleY: canvas.height / img.height,
})
})
}
return {
@ -506,7 +576,6 @@ export function useCanvas(id) {
addShape,
handleUndo,
handleRedo,
handleClear,
handleCopy,
handleDelete,
handleSave,
@ -515,6 +584,6 @@ export function useCanvas(id) {
attachCustomControlOnPolygon,
saveImage,
handleFlip,
updateTextOnLineChange,
setCanvasBackgroundWithDots,
}
}

1101
src/hooks/useMode.js Normal file

File diff suppressed because it is too large Load Diff

12
src/middleware.js Normal file
View File

@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request) {
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-pathname', request.nextUrl.pathname)
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
}

37
src/store/canvasAtom.js Normal file
View File

@ -0,0 +1,37 @@
import { atom } from 'recoil'
export const textState = atom({
key: 'textState',
default: 'test text',
})
export const fontSizeState = atom({
key: 'fontSizeState',
default: 16,
})
export const canvasSizeState = atom({
key: 'canvasSize',
default: {
vertical: 1000,
horizontal: 1000,
},
})
export const sortedPolygonArray = atom({
key: 'sortedArray',
default: [],
dangerouslyAllowMutability: true,
})
export const roofState = atom({
key: 'roof',
default: {},
dangerouslyAllowMutability: true,
})
export const wallState = atom({
key: 'wall',
default: {},
dangerouslyAllowMutability: true,
})

419
src/util/canvas-util.js Normal file
View File

@ -0,0 +1,419 @@
/**
* Collection of function to use on canvas
*/
// define a function that can locate the controls
export function polygonPositionHandler(dim, finalMatrix, fabricObject) {
// @ts-ignore
let x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x
// @ts-ignore
let y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y
return fabric.util.transformPoint(
{ x, y },
fabric.util.multiplyTransformMatrices(
fabricObject.canvas.viewportTransform,
fabricObject.calcTransformMatrix(),
),
)
}
function getObjectSizeWithStroke(object) {
let stroke = new fabric.Point(
object.strokeUniform ? 1 / object.scaleX : 1,
object.strokeUniform ? 1 / object.scaleY : 1,
).multiply(object.strokeWidth)
return new fabric.Point(object.width + stroke.x, object.height + stroke.y)
}
// define a function that will define what the control does
export function actionHandler(eventData, transform, x, y) {
let polygon = transform.target,
currentControl = polygon.controls[polygon.__corner],
mouseLocalPosition = polygon.toLocalPoint(
new fabric.Point(x, y),
'center',
'center',
),
polygonBaseSize = getObjectSizeWithStroke(polygon),
size = polygon._getTransformedDimensions(0, 0)
polygon.points[currentControl.pointIndex] = {
x:
(mouseLocalPosition.x * polygonBaseSize.x) / size.x +
polygon.pathOffset.x,
y:
(mouseLocalPosition.y * polygonBaseSize.y) / size.y +
polygon.pathOffset.y,
}
return true
}
// define a function that can keep the polygon in the same position when we change its width/height/top/left
export function anchorWrapper(anchorIndex, fn) {
return function (eventData, transform, x, y) {
let fabricObject = transform.target
let originX =
fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
let absolutePoint = fabric.util.transformPoint(
{
x: originX,
y: originY,
},
fabricObject.calcTransformMatrix(),
)
let actionPerformed = fn(eventData, transform, x, y)
let newDim = fabricObject._setPositionDimensions({})
let polygonBaseSize = getObjectSizeWithStroke(fabricObject)
let newX =
(fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) /
polygonBaseSize.x
let newY =
(fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) /
polygonBaseSize.y
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5)
return actionPerformed
}
}
/**
* 좌표의 중간점 좌표를 계산해서 반환하는 함수
* @param {number} point1
* @param {number} point2 방향에 상관없이 항상 값이 뒤에 위치해야
* @returns
*/
export const getCenterPoint = (point1, point2) => {
return point1 + (point2 - point1) / 2
}
/**
* 사이의 거리를 계산하는 함수
* @param {*} x1 첫번째 x좌표
* @param {*} y1 첫번째 y좌표
* @param {*} x2 두번째 x좌표
* @param {*} y2 두번째 y좌표
* @returns
*/
export const getDistance = (x1, y1, x2, y2) => {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
// polygon의 각 변에 해당 점과 점 사이의 거리를 나타내는 IText를 추가하는 함수
export function addDistanceTextToPolygon(polygon) {
const points = polygon.get('points')
const texts = []
for (let i = 0; i < points.length; i++) {
const start = points[i]
const end = points[(i + 1) % points.length] // 다음 점 (마지막 점의 경우 첫번째 점으로)
const distance = Math.sqrt(
Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2),
) // 두 점 사이의 거리 계산
const text = new fabric.Textbox(distance.toFixed(2), {
// 소수 둘째자리까지 표시
left: (start.x + end.x) / 2, // 텍스트의 위치는 두 점의 중간
top: (start.y + end.y) / 2,
fontSize: 20,
})
texts.push(text)
}
return new fabric.Group([polygon, ...texts], {
// polygon과 텍스트들을 그룹화
selectable: true,
})
}
/**
*
* @param {number} value
* @param {boolean} useDefault
* @param {string} delimeter
* @returns
* ex) 1,100 mm
*/
export const formattedWithComma = (value, unit = 'mm') => {
let formatterdData = value.toLocaleString('ko-KR')
if (unit === 'cm') {
formatterdData = value.toLocaleString('ko-KR') / 10
} else if (unit === 'm') {
formatterdData = value.toLocaleString('ko-KR') / 1000
}
return `${formatterdData} ${unit}`
}
export const distanceBetweenPoints = (point1, point2) => {
const dx = point2.x - point1.x
const dy = point2.y - point1.y
return Math.sqrt(dx * dx + dy * dy)
}
/**
* line의 시작점을 찾는 함수
* @param lines
* @returns {number}
*/
export const getStartIndex = (lines) => {
let smallestIndex = 0
let smallestX1 = lines[0].x1
let smallestY1 = lines[0].y1
for (let i = 1; i < lines.length; i++) {
if (
lines[i].x1 < smallestX1 ||
(lines[i].x1 === smallestX1 && lines[i].y1 < smallestY1)
) {
smallestIndex = i
smallestX1 = lines[i].x1
smallestY1 = lines[i].y1
}
}
return smallestIndex
}
/**
* points 배열에서 시작점을 찾는 함수
* @param points
* @returns {number}
*/
export const getStartIndexPoint = (points) => {
let smallestIndex = 0
let smallestX1 = points[0].x
let smallestY1 = points[0].y
for (let i = 1; i < points.length; i++) {
if (
points[i].x < smallestX1 ||
(points[i].x === smallestX1 && points[i].y < smallestY1)
) {
smallestIndex = i
smallestX1 = points[i].x
smallestY1 = points[i].y
}
}
return smallestIndex
}
/**
* 함수는 개의 매개변수를 받습니다: array와 index.
* array는 재배열할 대상 배열입니다.
* index는 재배열의 기준이 배열 내의 위치입니다.
* 함수는 먼저 index 위치부터 배열의 마지막 요소까지를 추출합니다(fromIndexToEnd).
* 다음, 배열의 처음부터 index 위치까지의 요소를 추출합니다(fromStartToIndex).
* 마지막으로, fromIndexToEnd와 fromStartToIndex 부분을 concat 메소드를 이용해 합칩니다.
* 따라서, 함수는 주어진 index를 기준으로 배열을 부분으로 나누고, index부터 시작하는 부분을 앞에 두고, 뒤에 index 이전의 부분을 이어붙여 새로운 배열을 생성합니다. 이는 배열의 회전(rotating) 연산을 수행하는 것과 유사합니다.
* @param array 재배열할 대상 배열
* @param index 재배열 기준이 배열 내의 인덱스
* @returns {*} 새로 재배열된 배열
*/
export const rearrangeArray = (array, index) => {
// 배열의 특정 인덱스부터 마지막 요소까지를 가져옵니다.
const fromIndexToEnd = array.slice(index)
// 배열의 처음부터 특정 인덱스까지의 요소를 가져옵니다.
const fromStartToIndex = array.slice(0, index)
// 두 부분을 concat 메소드를 이용해 합칩니다.
return fromIndexToEnd.concat(fromStartToIndex)
}
export const findTopTwoIndexesByDistance = (objArr) => {
if (objArr.length < 2) {
return [] // 배열의 길이가 2보다 작으면 빈 배열 반환
}
let firstIndex = -1
let secondIndex = -1
let firstDistance = -Infinity
let secondDistance = -Infinity
for (let i = 0; i < objArr.length; i++) {
const distance = objArr[i].length
if (distance > firstDistance) {
secondDistance = firstDistance
secondIndex = firstIndex
firstDistance = distance
firstIndex = i
} else if (distance > secondDistance) {
secondDistance = distance
secondIndex = i
}
}
return [firstIndex, secondIndex]
}
/**
* 지붕의 누워있는 높이
* @param base 밑변
* @param degree 각도 ex(4촌의 경우 21.8)
*/
export const getRoofHeight = (base, degree) => {
return base / Math.cos((degree * Math.PI) / 180)
}
/**
* 지붕 빗변의 길이
* @param base 밑변
*/
export const getRoofHypotenuse = (base) => {
return Math.sqrt(base * base * 2)
}
/**
* 촌을 입력받아 각도를 반환
* @param chon
* @returns {number}
*/
export const getDegreeByChon = (chon) => {
// tan(theta) = height / base
const radians = Math.atan(chon / 10)
// 라디안을 도 단위로 변환
return Number((radians * (180 / Math.PI)).toFixed(2))
}
/**
* 사이의 방향을 반환합니다.
* @param a {fabric.Object}
* @param b {fabric.Object}
* @returns {string}
*/
export const getDirection = (a, b) => {
const vector = {
x: b.left - a.left,
y: b.top - a.top,
}
if (Math.abs(vector.x) > Math.abs(vector.y)) {
// x축 방향으로 더 많이 이동
return vector.x > 0 ? 'right' : 'left'
} else {
// y축 방향으로 더 많이 이동
return vector.y > 0 ? 'bottom' : 'top'
}
}
/**
* 사이의 방향을 반환합니다.
*/
export const getDirectionByPoint = (a, b) => {
const vector = {
x: b.x - a.x,
y: b.y - a.y,
}
if (Math.abs(vector.x) > Math.abs(vector.y)) {
// x축 방향으로 더 많이 이동
return vector.x > 0 ? 'right' : 'left'
} else {
// y축 방향으로 더 많이 이동
return vector.y > 0 ? 'bottom' : 'top'
}
}
/**
* line을 두개를 이용해서 교차점을 찾는 함수
* @param line1
* @param line2
* @returns {{x: number, y: number}|null}
*/
export function calculateIntersection(line1, line2) {
const x1 = line1.x1,
y1 = line1.y1,
x2 = line1.x2,
y2 = line1.y2
const x3 = line2.x1,
y3 = line2.y1,
x4 = line2.x2,
y4 = line2.y2
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
if (denom === 0) return null // 선분이 평행하거나 일치
const intersectX =
((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom
const intersectY =
((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom
// 교차점이 두 선분의 x 좌표 범위 내에 있는지 확인
if (
intersectX < Math.min(x1, x2) ||
intersectX > Math.max(x1, x2) ||
intersectX < Math.min(x3, x4) ||
intersectX > Math.max(x3, x4)
) {
return null // 교차점이 선분 범위 밖에 있음
}
return { x: intersectX, y: intersectY }
}
/**
* points배열을 입력받아 반시계방향으로 정렬된 points를 반환합니다.
* @param points
*/
export const sortedPoints = (points) => {
const copyPoints = [...points]
//points를 x,y좌표를 기준으로 정렬합니다.
copyPoints.sort((a, b) => {
if (a.x === b.x) {
return a.y - b.y
}
return a.x - b.x
})
// 이때 copyPoints를 순회하며 최초엔 x값을 비교하여 같은 점을 찾는다. 이때 이 점이 2번째 점이 된다.
// 그 다음점은 2번째 점과 y값이 같은 점이 된다.
// 또 그다음 점은 3번째 점과 x값이 같은 점이 된다.
// 이를 반복하여 copyPoints를 재배열한다.
const resultPoints = [copyPoints[0]]
let index = 1
let currentPoint = { ...copyPoints[0] }
copyPoints.splice(0, 1)
while (index < points.length) {
if (index === points.length - 1) {
resultPoints.push(copyPoints[0])
index++
break
} else if (index % 2 === 0) {
// 짝수번째는 y값이 같은 점을 찾는다.
for (let i = 0; i < copyPoints.length; i++) {
// y값이 같은 point가 많은 경우 그 중 x값이 가장 큰걸 찾는다.
const temp = copyPoints.filter((point) => point.y === currentPoint.y)
// temp중 x값이 가장 큰 값
const max = temp.reduce((prev, current) =>
prev.x >= current.x ? prev : current,
)
resultPoints.push(max)
currentPoint = max
copyPoints.splice(copyPoints.indexOf(max), 1)
index++
break
}
} else {
// 홀수번째는 x값이 같은 점을 찾는다.
for (let i = 0; i < copyPoints.length; i++) {
// x값이 같은 point가 많은 경우 그 중 y값이 가장 큰걸 찾는다.
const temp = copyPoints.filter((point) => point.x === currentPoint.x)
// temp중 y값이 가장 큰 값
const max = temp.reduce((prev, current) =>
prev.y >= current.y ? prev : current,
)
resultPoints.push(max)
currentPoint = max
copyPoints.splice(copyPoints.indexOf(max), 1)
index++
break
}
}
}
return resultPoints
}

View File

@ -1,18 +1,22 @@
const { nextui } = require('@nextui-org/react')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
};
darkMode: 'class',
plugins: [nextui()],
}

2089
yarn.lock

File diff suppressed because it is too large Load Diff