Merge branch 'feature/test'
This commit is contained in:
commit
663ec38bd5
37
README.md
37
README.md
@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|

|
||||||
|
|
||||||
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
|

|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@ -9,10 +9,13 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nextui-org/react": "^2.4.2",
|
||||||
"fabric": "^5.3.0",
|
"fabric": "^5.3.0",
|
||||||
|
"framer-motion": "^11.2.13",
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"recoil": "^0.7.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
BIN
public/assets/img/check.jpg
Normal file
BIN
public/assets/img/check.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/assets/img/check2.png
Normal file
BIN
public/assets/img/check2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 B |
6
src/app/RecoilWrapper.js
Normal file
6
src/app/RecoilWrapper.js
Normal 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
5
src/app/UIProvider.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { NextUIProvider } from '@nextui-org/react'
|
||||||
|
|
||||||
|
export default function UIProvider({ children }) {
|
||||||
|
return <NextUIProvider>{children}</NextUIProvider>
|
||||||
|
}
|
||||||
40
src/app/changelog/page.jsx
Normal file
40
src/app/changelog/page.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -31,3 +31,9 @@ body {
|
|||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
.archivo-black-regular {
|
||||||
|
font-family: 'Archivo Black', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import Headers from '@/components/Headers'
|
import Headers from '@/components/Headers'
|
||||||
|
import RecoilRootWrapper from './RecoilWrapper'
|
||||||
|
import UIProvider from './UIProvider'
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
@ -10,11 +13,17 @@ export const metadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
|
const headersList = headers()
|
||||||
|
const headerPathname = headersList.get('x-pathname') || ''
|
||||||
|
// console.log('headerPathname', headerPathname)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<Headers />
|
{headerPathname !== '/login' && <Headers />}
|
||||||
{children}
|
<RecoilRootWrapper>
|
||||||
|
<UIProvider>{children}</UIProvider>
|
||||||
|
</RecoilRootWrapper>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
91
src/app/login/page.jsx
Normal file
91
src/app/login/page.jsx
Normal 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
23
src/app/roof2/page.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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}`
|
|
||||||
}
|
|
||||||
@ -9,7 +9,9 @@ export default function Headers() {
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="space-x-4 text-xl">
|
<div className="space-x-4 text-xl">
|
||||||
<Link href="/intro">Intro</Link>
|
<Link href="/intro">Intro</Link>
|
||||||
|
<Link href="/changelog">Changelog</Link>
|
||||||
<Link href="/roof">Roof</Link>
|
<Link href="/roof">Roof</Link>
|
||||||
|
<Link href="/roof2">Roof2</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export default function Hero(props) {
|
export default function Hero(props) {
|
||||||
return (
|
return (
|
||||||
<div className="pt-48 flex justify-center">
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { useCanvas } from '@/hooks/useCanvas'
|
||||||
import { fabric } from 'fabric'
|
import { fabric } from 'fabric'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Roof() {
|
export default function Roof() {
|
||||||
const {
|
const {
|
||||||
@ -18,9 +19,88 @@ export default function Roof() {
|
|||||||
attachCustomControlOnPolygon,
|
attachCustomControlOnPolygon,
|
||||||
saveImage,
|
saveImage,
|
||||||
handleFlip,
|
handleFlip,
|
||||||
updateTextOnLineChange,
|
|
||||||
} = useCanvas('canvas')
|
} = 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 addRect = () => {
|
||||||
const rect = new fabric.Rect({
|
const rect = new fabric.Rect({
|
||||||
height: 200,
|
height: 200,
|
||||||
@ -92,9 +172,9 @@ export default function Roof() {
|
|||||||
const trapezoid = new fabric.Polygon(
|
const trapezoid = new fabric.Polygon(
|
||||||
[
|
[
|
||||||
{ x: 100, y: 100 }, // 좌상단
|
{ x: 100, y: 100 }, // 좌상단
|
||||||
{ x: 300, y: 100 }, // 우상단
|
{ x: 500, y: 100 }, // 우상단
|
||||||
{ x: 250, y: 200 }, // 우하단
|
{ x: 750, y: 700 }, // 우하단
|
||||||
{ x: 150, y: 200 }, // 좌하단
|
{ x: 250, y: 400 }, // 좌하단
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
name: uuidv4(),
|
name: uuidv4(),
|
||||||
@ -102,35 +182,96 @@ export default function Roof() {
|
|||||||
opacity: 0.4,
|
opacity: 0.4,
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
objectCaching: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
attachCustomControlOnPolygon(trapezoid)
|
attachCustomControlOnPolygon(trapezoid)
|
||||||
addShape(trapezoid)
|
const group = addDistanceTextToPolygon(trapezoid)
|
||||||
|
addGroupClickEvent(group)
|
||||||
|
canvas?.add(group)
|
||||||
|
canvas?.renderAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
const addTextWithLine = () => {
|
// group에 클릭 이벤트를 추가하여 클릭 시 group을 제거하고 object들만 남기는 함수
|
||||||
const { x1, y1, x2, y2 } = { x1: 20, y1: 100, x2: 220, y2: 100 }
|
function addGroupClickEvent(group) {
|
||||||
/**
|
group.on('selected', (e) => {
|
||||||
* 시작X,시작Y,도착X,도착Y 좌표
|
console.log(e)
|
||||||
*/
|
|
||||||
const horizontalLine = new fabric.Line([x1, y1, x2, y2], {
|
|
||||||
name: uuidv4(),
|
|
||||||
stroke: 'red',
|
|
||||||
strokeWidth: 3,
|
|
||||||
selectable: true,
|
|
||||||
})
|
})
|
||||||
|
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(), {
|
const objectCenter = object.getCenterPoint() // TextBox 객체의 중심점 가져오기
|
||||||
fontSize: 20,
|
const dx = objectCenter.x + groupPoint.x - pointer.x
|
||||||
left: (x2 - x1) / 2,
|
const dy = objectCenter.y + groupPoint.y - pointer.y
|
||||||
top: y1 - 20,
|
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)
|
// IText를 수정할 때 해당 값을 길이로 갖는 다른 polygon을 생성하고 다시 그룹화하는 함수
|
||||||
addShape(group)
|
function addTextModifiedEvent(text, polygon, index) {
|
||||||
|
text.on('editing:exited', function () {})
|
||||||
// 선의 길이가 변경될 때마다 텍스트를 업데이트하는 이벤트 리스너를 추가합니다.
|
|
||||||
group.on('modified', () => updateTextOnLineChange(group, text))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomColor = () => {
|
const randomColor = () => {
|
||||||
@ -232,12 +373,6 @@ export default function Roof() {
|
|||||||
>
|
>
|
||||||
도형반전
|
도형반전
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="w-30 mx-2 p-2 rounded bg-black text-white"
|
|
||||||
onClick={addTextWithLine}
|
|
||||||
>
|
|
||||||
숫자가 있는 선
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
426
src/components/Roof2.jsx
Normal file
426
src/components/Roof2.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
108
src/components/fabric/QLine.js
Normal file
108
src/components/fabric/QLine.js
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
924
src/components/fabric/QPolygon.js
Normal file
924
src/components/fabric/QPolygon.js
Normal 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) {}
|
||||||
|
}
|
||||||
98
src/components/fabric/QRect.js
Normal file
98
src/components/fabric/QRect.js
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/components/ui/RangeSlider.jsx
Normal file
28
src/components/ui/RangeSlider.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,20 +3,21 @@ import { fabric } from 'fabric'
|
|||||||
import {
|
import {
|
||||||
actionHandler,
|
actionHandler,
|
||||||
anchorWrapper,
|
anchorWrapper,
|
||||||
calculateShapeLength,
|
|
||||||
polygonPositionHandler,
|
polygonPositionHandler,
|
||||||
} from '@/app/util/canvas-util'
|
} from '@/util/canvas-util'
|
||||||
|
|
||||||
const CANVAS = {
|
import { useRecoilState } from 'recoil'
|
||||||
WIDTH: 1000,
|
import { canvasSizeState, fontSizeState } from '@/store/canvasAtom'
|
||||||
HEIGHT: 1000,
|
import QPolygon from '@/components/fabric/QPolygon'
|
||||||
}
|
import { QLine } from '@/components/fabric/QLine'
|
||||||
|
import QRect from '@/components/fabric/QRect'
|
||||||
|
|
||||||
export function useCanvas(id) {
|
export function useCanvas(id) {
|
||||||
const [canvas, setCanvas] = useState()
|
const [canvas, setCanvas] = useState()
|
||||||
const [isLocked, setIsLocked] = useState(false)
|
const [isLocked, setIsLocked] = useState(false)
|
||||||
const [history, setHistory] = useState([])
|
const [history, setHistory] = useState([])
|
||||||
|
const [canvasSize] = useRecoilState(canvasSizeState)
|
||||||
|
const [fontSize] = useRecoilState(fontSizeState)
|
||||||
const points = useRef([])
|
const points = useRef([])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,9 +25,10 @@ export function useCanvas(id) {
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const c = new fabric.Canvas(id, {
|
const c = new fabric.Canvas(id, {
|
||||||
height: CANVAS.HEIGHT,
|
height: canvasSize.vertical,
|
||||||
width: CANVAS.WIDTH,
|
width: canvasSize.horizontal,
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
|
selection: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// settings for all canvas in the app
|
// settings for all canvas in the app
|
||||||
@ -36,18 +38,60 @@ export function useCanvas(id) {
|
|||||||
fabric.Object.prototype.cornerStrokeColor = '#2BEBC8'
|
fabric.Object.prototype.cornerStrokeColor = '#2BEBC8'
|
||||||
fabric.Object.prototype.cornerSize = 6
|
fabric.Object.prototype.cornerSize = 6
|
||||||
|
|
||||||
|
QPolygon.prototype.canvas = c
|
||||||
|
QLine.prototype.canvas = c
|
||||||
|
QRect.prototype.canvas = c
|
||||||
|
|
||||||
setCanvas(c)
|
setCanvas(c)
|
||||||
return () => {
|
return () => {
|
||||||
c.dispose()
|
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(() => {
|
useEffect(() => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
initialize()
|
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])
|
}, [canvas])
|
||||||
const addEventOnCanvas = () => {
|
const addEventOnCanvas = () => {
|
||||||
@ -92,11 +136,11 @@ export function useCanvas(id) {
|
|||||||
canvas?.clear()
|
canvas?.clear()
|
||||||
|
|
||||||
// 기존 이벤트가 있을 경우 제거한다.
|
// 기존 이벤트가 있을 경우 제거한다.
|
||||||
removeEventOnCanvas()
|
// removeEventOnCanvas()
|
||||||
|
|
||||||
// 작업 후에 event를 추가해준다.
|
// 작업 후에 event를 추가해준다.
|
||||||
|
|
||||||
addEventOnCanvas()
|
// addEventOnCanvas()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +155,7 @@ export function useCanvas(id) {
|
|||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
if (target) {
|
if (target) {
|
||||||
settleDown(target)
|
// settleDown(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLocked) {
|
if (!isLocked) {
|
||||||
@ -133,25 +177,23 @@ export function useCanvas(id) {
|
|||||||
|
|
||||||
// 가로선을 그립니다.
|
// 가로선을 그립니다.
|
||||||
const horizontalLine = new fabric.Line(
|
const horizontalLine = new fabric.Line(
|
||||||
[0, pointer.y, CANVAS.WIDTH, pointer.y],
|
[0, pointer.y, canvasSize.horizontal, pointer.y],
|
||||||
{
|
{
|
||||||
stroke: 'black',
|
stroke: 'black',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
name: 'mouseLine',
|
name: 'mouseLine',
|
||||||
strokeDashArray: [5, 5],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 세로선을 그립니다.
|
// 세로선을 그립니다.
|
||||||
const verticalLine = new fabric.Line(
|
const verticalLine = new fabric.Line(
|
||||||
[pointer.x, 0, pointer.x, CANVAS.HEIGHT],
|
[pointer.x, 0, pointer.x, canvasSize.vertical],
|
||||||
{
|
{
|
||||||
stroke: 'black',
|
stroke: 'black',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
name: 'mouseLine',
|
name: 'mouseLine',
|
||||||
strokeDashArray: [5, 5],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -208,15 +250,16 @@ export function useCanvas(id) {
|
|||||||
*/
|
*/
|
||||||
const handleUndo = () => {
|
const handleUndo = () => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
if (canvas._objects.length > 0) {
|
if (canvas?._objects.length > 0) {
|
||||||
const poppedObject = canvas._objects.pop()
|
const poppedObject = canvas?._objects.pop()
|
||||||
|
|
||||||
setHistory((prev) => {
|
setHistory((prev) => {
|
||||||
if (prev === undefined) {
|
if (prev === undefined) {
|
||||||
return poppedObject ? [poppedObject] : []
|
return poppedObject ? [poppedObject] : []
|
||||||
}
|
}
|
||||||
return poppedObject ? [...prev, poppedObject] : prev
|
return poppedObject ? [...prev, poppedObject] : prev
|
||||||
})
|
})
|
||||||
canvas.renderAll()
|
canvas?.renderAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,21 +268,13 @@ export function useCanvas(id) {
|
|||||||
if (canvas && history) {
|
if (canvas && history) {
|
||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
setIsLocked(true)
|
setIsLocked(true)
|
||||||
canvas.add(history[history.length - 1])
|
canvas?.add(history[history.length - 1])
|
||||||
const newHistory = history.slice(0, -1)
|
const newHistory = history.slice(0, -1)
|
||||||
setHistory(newHistory)
|
setHistory(newHistory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 해당 캔버스를 비운다.
|
|
||||||
*/
|
|
||||||
const handleClear = () => {
|
|
||||||
canvas?.clear()
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 선택한 도형을 복사한다.
|
* 선택한 도형을 복사한다.
|
||||||
*/
|
*/
|
||||||
@ -440,6 +475,7 @@ export function useCanvas(id) {
|
|||||||
const lastControl = poly.points?.length - 1
|
const lastControl = poly.points?.length - 1
|
||||||
poly.cornerStyle = 'rect'
|
poly.cornerStyle = 'rect'
|
||||||
poly.cornerColor = 'rgba(0,0,255,0.5)'
|
poly.cornerColor = 'rgba(0,0,255,0.5)'
|
||||||
|
poly.objectCaching = false
|
||||||
poly.controls = poly.points.reduce(function (acc, point, index) {
|
poly.controls = poly.points.reduce(function (acc, point, index) {
|
||||||
acc['p' + index] = new fabric.Control({
|
acc['p' + index] = new fabric.Control({
|
||||||
positionHandler: polygonPositionHandler,
|
positionHandler: polygonPositionHandler,
|
||||||
@ -494,11 +530,45 @@ export function useCanvas(id) {
|
|||||||
canvas?.renderAll()
|
canvas?.renderAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선의 길이가 변경될 때마다 텍스트를 업데이트하는 함수
|
function fillCanvasWithDots(canvas, gap) {
|
||||||
const updateTextOnLineChange = (group, text) => {
|
const width = canvas.getWidth()
|
||||||
const length = calculateShapeLength(group)
|
const height = canvas.getHeight()
|
||||||
text.set({ text: length.toString() })
|
|
||||||
canvas?.renderAll()
|
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 {
|
return {
|
||||||
@ -506,7 +576,6 @@ export function useCanvas(id) {
|
|||||||
addShape,
|
addShape,
|
||||||
handleUndo,
|
handleUndo,
|
||||||
handleRedo,
|
handleRedo,
|
||||||
handleClear,
|
|
||||||
handleCopy,
|
handleCopy,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
handleSave,
|
handleSave,
|
||||||
@ -515,6 +584,6 @@ export function useCanvas(id) {
|
|||||||
attachCustomControlOnPolygon,
|
attachCustomControlOnPolygon,
|
||||||
saveImage,
|
saveImage,
|
||||||
handleFlip,
|
handleFlip,
|
||||||
updateTextOnLineChange,
|
setCanvasBackgroundWithDots,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1101
src/hooks/useMode.js
Normal file
1101
src/hooks/useMode.js
Normal file
File diff suppressed because it is too large
Load Diff
12
src/middleware.js
Normal file
12
src/middleware.js
Normal 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
37
src/store/canvasAtom.js
Normal 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
419
src/util/canvas-util.js
Normal 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
|
||||||
|
}
|
||||||
@ -1,18 +1,22 @@
|
|||||||
|
const { nextui } = require('@nextui-org/react')
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
"gradient-conic":
|
'gradient-conic':
|
||||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
darkMode: 'class',
|
||||||
};
|
plugins: [nextui()],
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user