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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.4.2",
|
||||
"fabric": "^5.3.0",
|
||||
"framer-motion": "^11.2.13",
|
||||
"next": "14.2.3",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"recoil": "^0.7.7",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
BIN
public/assets/img/check.jpg
Normal file
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;
|
||||
}
|
||||
} */
|
||||
|
||||
.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 './globals.css'
|
||||
import Headers from '@/components/Headers'
|
||||
import RecoilRootWrapper from './RecoilWrapper'
|
||||
import UIProvider from './UIProvider'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
@ -10,11 +13,17 @@ export const metadata = {
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
const headersList = headers()
|
||||
const headerPathname = headersList.get('x-pathname') || ''
|
||||
// console.log('headerPathname', headerPathname)
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<Headers />
|
||||
{children}
|
||||
{headerPathname !== '/login' && <Headers />}
|
||||
<RecoilRootWrapper>
|
||||
<UIProvider>{children}</UIProvider>
|
||||
</RecoilRootWrapper>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
91
src/app/login/page.jsx
Normal file
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>
|
||||
<div className="space-x-4 text-xl">
|
||||
<Link href="/intro">Intro</Link>
|
||||
<Link href="/changelog">Changelog</Link>
|
||||
<Link href="/roof">Roof</Link>
|
||||
<Link href="/roof2">Roof2</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export default function Hero(props) {
|
||||
return (
|
||||
<div className="pt-48 flex justify-center">
|
||||
<h1 className="text-6xl">{props.title}</h1>
|
||||
<h1 className="text-6xl archivo-black-regular">{props.title}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { createGroupWithLineAndText, getDistance } from '@/app/util/canvas-util'
|
||||
import { addDistanceTextToPolygon, getDistance } from '@/util/canvas-util'
|
||||
import { useCanvas } from '@/hooks/useCanvas'
|
||||
import { fabric } from 'fabric'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Roof() {
|
||||
const {
|
||||
@ -18,9 +19,88 @@ export default function Roof() {
|
||||
attachCustomControlOnPolygon,
|
||||
saveImage,
|
||||
handleFlip,
|
||||
updateTextOnLineChange,
|
||||
} = useCanvas('canvas')
|
||||
|
||||
useEffect(() => {
|
||||
let circle = new fabric.Circle({
|
||||
radius: 40,
|
||||
fill: 'rgba(200, 0, 0, 0.3)',
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
})
|
||||
|
||||
let text = new fabric.Textbox('AJLoveChina', {
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
})
|
||||
|
||||
let group = new fabric.Group([circle, text], {
|
||||
left: 100,
|
||||
top: 100,
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
})
|
||||
|
||||
group.on('mousedblclick', () => {
|
||||
// textForEditing is temporary obj,
|
||||
// and will be removed after editing
|
||||
console.log(text.type)
|
||||
let textForEditing = new fabric.Textbox(text.text, {
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
textAlign: text.textAlign,
|
||||
fontSize: text.fontSize,
|
||||
|
||||
left: group.left,
|
||||
top: group.top,
|
||||
})
|
||||
|
||||
// hide group inside text
|
||||
text.visible = false
|
||||
// note important, text cannot be hidden without this
|
||||
group.addWithUpdate()
|
||||
|
||||
textForEditing.visible = true
|
||||
// do not give controls, do not allow move/resize/rotation on this
|
||||
textForEditing.hasConstrols = false
|
||||
|
||||
// now add this temporary obj to canvas
|
||||
canvas.add(textForEditing)
|
||||
canvas.setActiveObject(textForEditing)
|
||||
// make the cursor showing
|
||||
textForEditing.enterEditing()
|
||||
textForEditing.selectAll()
|
||||
|
||||
// editing:exited means you click outside of the textForEditing
|
||||
textForEditing.on('editing:exited', () => {
|
||||
let newVal = textForEditing.text
|
||||
let oldVal = text.text
|
||||
|
||||
// then we check if text is changed
|
||||
if (newVal !== oldVal) {
|
||||
text.set({
|
||||
text: newVal,
|
||||
visible: true,
|
||||
})
|
||||
|
||||
// comment before, you must call this
|
||||
group.addWithUpdate()
|
||||
|
||||
// we do not need textForEditing anymore
|
||||
textForEditing.visible = false
|
||||
canvas?.remove(textForEditing)
|
||||
|
||||
// optional, buf for better user experience
|
||||
canvas?.setActiveObject(group)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
canvas?.add(group)
|
||||
}, [canvas])
|
||||
|
||||
const addRect = () => {
|
||||
const rect = new fabric.Rect({
|
||||
height: 200,
|
||||
@ -92,9 +172,9 @@ export default function Roof() {
|
||||
const trapezoid = new fabric.Polygon(
|
||||
[
|
||||
{ x: 100, y: 100 }, // 좌상단
|
||||
{ x: 300, y: 100 }, // 우상단
|
||||
{ x: 250, y: 200 }, // 우하단
|
||||
{ x: 150, y: 200 }, // 좌하단
|
||||
{ x: 500, y: 100 }, // 우상단
|
||||
{ x: 750, y: 700 }, // 우하단
|
||||
{ x: 250, y: 400 }, // 좌하단
|
||||
],
|
||||
{
|
||||
name: uuidv4(),
|
||||
@ -102,35 +182,96 @@ export default function Roof() {
|
||||
opacity: 0.4,
|
||||
strokeWidth: 3,
|
||||
selectable: true,
|
||||
objectCaching: false,
|
||||
},
|
||||
)
|
||||
attachCustomControlOnPolygon(trapezoid)
|
||||
addShape(trapezoid)
|
||||
const group = addDistanceTextToPolygon(trapezoid)
|
||||
addGroupClickEvent(group)
|
||||
canvas?.add(group)
|
||||
canvas?.renderAll()
|
||||
}
|
||||
|
||||
const addTextWithLine = () => {
|
||||
const { x1, y1, x2, y2 } = { x1: 20, y1: 100, x2: 220, y2: 100 }
|
||||
/**
|
||||
* 시작X,시작Y,도착X,도착Y 좌표
|
||||
*/
|
||||
const horizontalLine = new fabric.Line([x1, y1, x2, y2], {
|
||||
name: uuidv4(),
|
||||
stroke: 'red',
|
||||
strokeWidth: 3,
|
||||
selectable: true,
|
||||
// group에 클릭 이벤트를 추가하여 클릭 시 group을 제거하고 object들만 남기는 함수
|
||||
function addGroupClickEvent(group) {
|
||||
group.on('selected', (e) => {
|
||||
console.log(e)
|
||||
})
|
||||
group.on('mousedblclick', (e) => {
|
||||
// textForEditing is temporary obj,
|
||||
// and will be removed after editing
|
||||
const pointer = canvas?.getPointer(e.e) // 마우스 클릭 위치 가져오기
|
||||
let minDistance = Infinity
|
||||
let closestTextbox = null
|
||||
const groupPoint = group.getCenterPoint()
|
||||
group.getObjects().forEach(function (object) {
|
||||
if (object.type === 'textbox') {
|
||||
// 객체가 TextBox인지 확인
|
||||
|
||||
const text = new fabric.Text(getDistance(x1, y1, x2, y2).toString(), {
|
||||
fontSize: 20,
|
||||
left: (x2 - x1) / 2,
|
||||
top: y1 - 20,
|
||||
const objectCenter = object.getCenterPoint() // TextBox 객체의 중심점 가져오기
|
||||
const dx = objectCenter.x + groupPoint.x - pointer.x
|
||||
const dy = objectCenter.y + groupPoint.y - pointer.y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy) // 마우스 클릭 위치와 TextBox 객체 사이의 거리 계산
|
||||
|
||||
if (distance < minDistance) {
|
||||
// 가장 짧은 거리를 가진 TextBox 객체 찾기
|
||||
minDistance = distance
|
||||
closestTextbox = object
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let textForEditing = new fabric.Textbox(closestTextbox.text, {
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
textAlign: closestTextbox.textAlign,
|
||||
fontSize: closestTextbox.fontSize,
|
||||
left: closestTextbox.left + groupPoint.x,
|
||||
top: closestTextbox.top + groupPoint.y,
|
||||
})
|
||||
|
||||
// hide group inside text
|
||||
closestTextbox.visible = false
|
||||
// note important, text cannot be hidden without this
|
||||
group.addWithUpdate()
|
||||
|
||||
textForEditing.visible = true
|
||||
// do not give controls, do not allow move/resize/rotation on this
|
||||
textForEditing.hasConstrols = false
|
||||
|
||||
// now add this temporary obj to canvas
|
||||
canvas?.add(textForEditing)
|
||||
canvas?.setActiveObject(textForEditing)
|
||||
// make the cursor showing
|
||||
textForEditing?.enterEditing()
|
||||
textForEditing?.selectAll()
|
||||
|
||||
// editing:exited means you click outside of the textForEditing
|
||||
textForEditing?.on('editing:exited', () => {
|
||||
let newVal = textForEditing.text
|
||||
|
||||
// then we check if text is changed
|
||||
closestTextbox.set({
|
||||
text: newVal,
|
||||
visible: true,
|
||||
})
|
||||
|
||||
// comment before, you must call this
|
||||
group.addWithUpdate()
|
||||
|
||||
// we do not need textForEditing anymore
|
||||
textForEditing.visible = false
|
||||
canvas?.remove(textForEditing)
|
||||
|
||||
// optional, buf for better user experience
|
||||
canvas?.setActiveObject(group)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const group = createGroupWithLineAndText(horizontalLine, text)
|
||||
addShape(group)
|
||||
|
||||
// 선의 길이가 변경될 때마다 텍스트를 업데이트하는 이벤트 리스너를 추가합니다.
|
||||
group.on('modified', () => updateTextOnLineChange(group, text))
|
||||
// IText를 수정할 때 해당 값을 길이로 갖는 다른 polygon을 생성하고 다시 그룹화하는 함수
|
||||
function addTextModifiedEvent(text, polygon, index) {
|
||||
text.on('editing:exited', function () {})
|
||||
}
|
||||
|
||||
const randomColor = () => {
|
||||
@ -232,12 +373,6 @@ export default function Roof() {
|
||||
>
|
||||
도형반전
|
||||
</button>
|
||||
<button
|
||||
className="w-30 mx-2 p-2 rounded bg-black text-white"
|
||||
onClick={addTextWithLine}
|
||||
>
|
||||
숫자가 있는 선
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
426
src/components/Roof2.jsx
Normal file
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 {
|
||||
actionHandler,
|
||||
anchorWrapper,
|
||||
calculateShapeLength,
|
||||
polygonPositionHandler,
|
||||
} from '@/app/util/canvas-util'
|
||||
} from '@/util/canvas-util'
|
||||
|
||||
const CANVAS = {
|
||||
WIDTH: 1000,
|
||||
HEIGHT: 1000,
|
||||
}
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { canvasSizeState, fontSizeState } from '@/store/canvasAtom'
|
||||
import QPolygon from '@/components/fabric/QPolygon'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import QRect from '@/components/fabric/QRect'
|
||||
|
||||
export function useCanvas(id) {
|
||||
const [canvas, setCanvas] = useState()
|
||||
const [isLocked, setIsLocked] = useState(false)
|
||||
const [history, setHistory] = useState([])
|
||||
|
||||
const [canvasSize] = useRecoilState(canvasSizeState)
|
||||
const [fontSize] = useRecoilState(fontSizeState)
|
||||
const points = useRef([])
|
||||
|
||||
/**
|
||||
@ -24,9 +25,10 @@ export function useCanvas(id) {
|
||||
*/
|
||||
useEffect(() => {
|
||||
const c = new fabric.Canvas(id, {
|
||||
height: CANVAS.HEIGHT,
|
||||
width: CANVAS.WIDTH,
|
||||
height: canvasSize.vertical,
|
||||
width: canvasSize.horizontal,
|
||||
backgroundColor: 'white',
|
||||
selection: false,
|
||||
})
|
||||
|
||||
// settings for all canvas in the app
|
||||
@ -36,18 +38,60 @@ export function useCanvas(id) {
|
||||
fabric.Object.prototype.cornerStrokeColor = '#2BEBC8'
|
||||
fabric.Object.prototype.cornerSize = 6
|
||||
|
||||
QPolygon.prototype.canvas = c
|
||||
QLine.prototype.canvas = c
|
||||
QRect.prototype.canvas = c
|
||||
|
||||
setCanvas(c)
|
||||
return () => {
|
||||
c.dispose()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// canvas 사이즈가 변경되면 다시
|
||||
removeEventOnCanvas()
|
||||
addEventOnCanvas()
|
||||
}, [canvasSize])
|
||||
|
||||
useEffect(() => {
|
||||
canvas
|
||||
?.getObjects()
|
||||
.filter(
|
||||
(obj) =>
|
||||
obj.type === 'textbox' ||
|
||||
obj.type === 'text' ||
|
||||
obj.type === 'i-text',
|
||||
)
|
||||
.forEach((obj) => {
|
||||
obj.set({ fontSize: fontSize })
|
||||
})
|
||||
|
||||
canvas
|
||||
?.getObjects()
|
||||
.filter(
|
||||
(obj) =>
|
||||
obj.type === 'QLine' ||
|
||||
obj.type === 'QPolygon' ||
|
||||
obj.type === 'QRect',
|
||||
)
|
||||
.forEach((obj) => {
|
||||
obj.setFontSize(fontSize)
|
||||
})
|
||||
canvas?.renderAll()
|
||||
}, [fontSize])
|
||||
|
||||
/**
|
||||
* 캔버스 초기화
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (canvas) {
|
||||
initialize()
|
||||
canvas?.on('object:added', onChange)
|
||||
canvas?.on('object:modified', onChange)
|
||||
canvas?.on('object:removed', onChange)
|
||||
canvas?.on('mouse:move', drawMouseLines)
|
||||
canvas?.on('mouse:out', removeMouseLines)
|
||||
}
|
||||
}, [canvas])
|
||||
const addEventOnCanvas = () => {
|
||||
@ -92,11 +136,11 @@ export function useCanvas(id) {
|
||||
canvas?.clear()
|
||||
|
||||
// 기존 이벤트가 있을 경우 제거한다.
|
||||
removeEventOnCanvas()
|
||||
// removeEventOnCanvas()
|
||||
|
||||
// 작업 후에 event를 추가해준다.
|
||||
|
||||
addEventOnCanvas()
|
||||
// addEventOnCanvas()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,7 +155,7 @@ export function useCanvas(id) {
|
||||
const onChange = (e) => {
|
||||
const target = e.target
|
||||
if (target) {
|
||||
settleDown(target)
|
||||
// settleDown(target)
|
||||
}
|
||||
|
||||
if (!isLocked) {
|
||||
@ -133,25 +177,23 @@ export function useCanvas(id) {
|
||||
|
||||
// 가로선을 그립니다.
|
||||
const horizontalLine = new fabric.Line(
|
||||
[0, pointer.y, CANVAS.WIDTH, pointer.y],
|
||||
[0, pointer.y, canvasSize.horizontal, pointer.y],
|
||||
{
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
name: 'mouseLine',
|
||||
strokeDashArray: [5, 5],
|
||||
},
|
||||
)
|
||||
|
||||
// 세로선을 그립니다.
|
||||
const verticalLine = new fabric.Line(
|
||||
[pointer.x, 0, pointer.x, CANVAS.HEIGHT],
|
||||
[pointer.x, 0, pointer.x, canvasSize.vertical],
|
||||
{
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
name: 'mouseLine',
|
||||
strokeDashArray: [5, 5],
|
||||
},
|
||||
)
|
||||
|
||||
@ -208,15 +250,16 @@ export function useCanvas(id) {
|
||||
*/
|
||||
const handleUndo = () => {
|
||||
if (canvas) {
|
||||
if (canvas._objects.length > 0) {
|
||||
const poppedObject = canvas._objects.pop()
|
||||
if (canvas?._objects.length > 0) {
|
||||
const poppedObject = canvas?._objects.pop()
|
||||
|
||||
setHistory((prev) => {
|
||||
if (prev === undefined) {
|
||||
return poppedObject ? [poppedObject] : []
|
||||
}
|
||||
return poppedObject ? [...prev, poppedObject] : prev
|
||||
})
|
||||
canvas.renderAll()
|
||||
canvas?.renderAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,21 +268,13 @@ export function useCanvas(id) {
|
||||
if (canvas && history) {
|
||||
if (history.length > 0) {
|
||||
setIsLocked(true)
|
||||
canvas.add(history[history.length - 1])
|
||||
canvas?.add(history[history.length - 1])
|
||||
const newHistory = history.slice(0, -1)
|
||||
setHistory(newHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 캔버스를 비운다.
|
||||
*/
|
||||
const handleClear = () => {
|
||||
canvas?.clear()
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택한 도형을 복사한다.
|
||||
*/
|
||||
@ -440,6 +475,7 @@ export function useCanvas(id) {
|
||||
const lastControl = poly.points?.length - 1
|
||||
poly.cornerStyle = 'rect'
|
||||
poly.cornerColor = 'rgba(0,0,255,0.5)'
|
||||
poly.objectCaching = false
|
||||
poly.controls = poly.points.reduce(function (acc, point, index) {
|
||||
acc['p' + index] = new fabric.Control({
|
||||
positionHandler: polygonPositionHandler,
|
||||
@ -494,11 +530,45 @@ export function useCanvas(id) {
|
||||
canvas?.renderAll()
|
||||
}
|
||||
|
||||
// 선의 길이가 변경될 때마다 텍스트를 업데이트하는 함수
|
||||
const updateTextOnLineChange = (group, text) => {
|
||||
const length = calculateShapeLength(group)
|
||||
text.set({ text: length.toString() })
|
||||
canvas?.renderAll()
|
||||
function fillCanvasWithDots(canvas, gap) {
|
||||
const width = canvas.getWidth()
|
||||
const height = canvas.getHeight()
|
||||
|
||||
for (let x = 0; x < width; x += gap) {
|
||||
for (let y = 0; y < height; y += gap) {
|
||||
const circle = new fabric.Circle({
|
||||
radius: 1,
|
||||
fill: 'black',
|
||||
left: x,
|
||||
top: y,
|
||||
selectable: false,
|
||||
})
|
||||
canvas.add(circle)
|
||||
}
|
||||
}
|
||||
|
||||
canvas.renderAll()
|
||||
}
|
||||
|
||||
const setCanvasBackgroundWithDots = (canvas, gap) => {
|
||||
// Create a new canvas and fill it with dots
|
||||
const tempCanvas = new fabric.StaticCanvas()
|
||||
tempCanvas.setDimensions({
|
||||
width: canvas.getWidth(),
|
||||
height: canvas.getHeight(),
|
||||
})
|
||||
fillCanvasWithDots(tempCanvas, gap)
|
||||
|
||||
// Convert the dotted canvas to an image
|
||||
const dataUrl = tempCanvas.toDataURL({ format: 'png' })
|
||||
|
||||
// Set the image as the background of the original canvas
|
||||
fabric.Image.fromURL(dataUrl, function (img) {
|
||||
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
|
||||
scaleX: canvas.width / img.width,
|
||||
scaleY: canvas.height / img.height,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
@ -506,7 +576,6 @@ export function useCanvas(id) {
|
||||
addShape,
|
||||
handleUndo,
|
||||
handleRedo,
|
||||
handleClear,
|
||||
handleCopy,
|
||||
handleDelete,
|
||||
handleSave,
|
||||
@ -515,6 +584,6 @@ export function useCanvas(id) {
|
||||
attachCustomControlOnPolygon,
|
||||
saveImage,
|
||||
handleFlip,
|
||||
updateTextOnLineChange,
|
||||
setCanvasBackgroundWithDots,
|
||||
}
|
||||
}
|
||||
|
||||
1101
src/hooks/useMode.js
Normal file
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} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
darkMode: 'class',
|
||||
plugins: [nextui()],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user