Merge branch 'dev' of https://git.jetbrains.space/nalpari/q-cast-iii/qcast-front into dev-ck
This commit is contained in:
commit
2e48112873
@ -1,8 +1,10 @@
|
||||
NEXT_PUBLIC_TEST="테스트변수입니다. development"
|
||||
|
||||
NEXT_PUBLIC_API_SERVER_PATH="http://1.248.227.176:38080"
|
||||
NEXT_PUBLIC_API_SERVER_PATH="http://1.248.227.176:38080"
|
||||
# NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
|
||||
|
||||
DATABASE_URL="sqlserver://mssql.devgrr.kr:1433;database=qcast;user=qcast;password=Qwertqaz12345;trustServerCertificate=true"
|
||||
|
||||
SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
|
||||
|
||||
NEXT_PUBLIC_CONVERTER_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_bV5zuYMyyIYFlOb3"
|
||||
@ -5,3 +5,5 @@ NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
|
||||
DATABASE_URL=""
|
||||
|
||||
SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
|
||||
|
||||
NEXT_PUBLIC_CONVERTER_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_bV5zuYMyyIYFlOb3"
|
||||
@ -25,6 +25,7 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-datepicker": "^7.3.0",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-responsive-modal": "^6.4.2",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recoil": "^0.7.7",
|
||||
@ -32,6 +33,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turf/turf": "^7.0.0",
|
||||
"convertapi": "^1.14.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="600" height="300" viewBox="0 0 1280 875"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,875.000000) scale(0.100000,-0.100000)" fill="purple" stroke="#000000" stroke-width="10">
|
||||
|
||||
<path d="M3403 7423 c-257 -762 -623 -1817 -636 -1829 -11 -11 -1264 -358
|
||||
-1817 -504 -405 -106 -540 -144 -539 -150 0 -3 30 -28 67 -55 117 -88 1826
|
||||
-1407 1841 -1421 12 -11 11 -104 -8 -646 -24 -697 -58 -1634 -64 -1770 -3 -49
|
||||
-3 -88 -1 -88 5 0 949 674 1528 1092 l479 346 61 -23 c34 -13 279 -104 546
|
||||
-203 267 -100 754 -282 1083 -405 328 -123 597 -219 597 -215 0 5 -54 189
|
||||
-119 411 -66 221 -192 652 -280 957 -88 305 -187 643 -219 751 l-58 195 304
|
||||
385 c168 211 503 636 745 944 242 308 447 568 455 578 8 9 12 20 8 24 -4 4
|
||||
-220 11 -479 15 -413 6 -1036 22 -1745 44 l-213 7 -427 636 c-235 350 -541
|
||||
808 -682 1018 -140 210 -258 383 -261 383 -3 0 -78 -215 -166 -477z"/>
|
||||
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,4 +0,0 @@
|
||||
<svg height="280" width="600" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="120,15 42,202 400,202 400,99 150,99 " style="fill:lime;stroke:purple;stroke-width:3" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 179 B |
5
src/app/[locale]/floor-plan/page.jsx
Normal file
5
src/app/[locale]/floor-plan/page.jsx
Normal file
@ -0,0 +1,5 @@
|
||||
import FloorPlan from '@/components/floor-plan/FloorPlan'
|
||||
|
||||
export default function floorPlanPage() {
|
||||
return <FloorPlan />
|
||||
}
|
||||
29
src/app/api/html2canvas/route.js
Normal file
29
src/app/api/html2canvas/route.js
Normal file
@ -0,0 +1,29 @@
|
||||
'use server'
|
||||
|
||||
import fs from 'fs/promises'
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(req) {
|
||||
const path = 'public/mapImages'
|
||||
const q = req.nextUrl.searchParams.get('q')
|
||||
const fileNm = req.nextUrl.searchParams.get('fileNm')
|
||||
const zoom = req.nextUrl.searchParams.get('zoom')
|
||||
const targetUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${q}&zoom=${zoom}&maptype=satellite&size=640x640&scale=1&key=AIzaSyDO7nVR1N_D2tKy60hgGFavpLaXkHpiHpc`
|
||||
const decodeUrl = decodeURIComponent(targetUrl)
|
||||
|
||||
const response = await fetch(decodeUrl)
|
||||
|
||||
const data = await response.arrayBuffer()
|
||||
const buffer = Buffer.from(data)
|
||||
|
||||
try {
|
||||
await fs.readdir(path)
|
||||
} catch {
|
||||
await fs.mkdir(path)
|
||||
} finally {
|
||||
await fs.writeFile(`${path}/${fileNm}.png`, buffer)
|
||||
}
|
||||
|
||||
return NextResponse.json({ fileNm: `${fileNm}.png` })
|
||||
}
|
||||
@ -12,6 +12,8 @@ export const Mode = {
|
||||
CELL_POWERCON: 'cellPowercon', //파워콘
|
||||
DRAW_HELP_LINE: 'drawHelpLine', // 보조선 그리기 모드 지붕 존재해야함
|
||||
ADSORPTION_POINT: 'adsorptionPoint', //흡착점 모드
|
||||
OPENING: 'opening', //개구 모드
|
||||
SHADOW: 'shadow', //그림자 생성 모드
|
||||
DEFAULT: 'default',
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import { Button } from '@nextui-org/react'
|
||||
import SingleDatePicker from './common/datepicker/SingleDatePicker'
|
||||
import RangeDatePicker from './common/datepicker/RangeDatePicker'
|
||||
import QGrid from './common/grid/QGrid'
|
||||
import { QToast } from '@/hooks/useToast'
|
||||
import { toastUp } from '@/hooks/useToast'
|
||||
|
||||
export default function Intro() {
|
||||
const { get } = useAxios()
|
||||
@ -127,7 +127,7 @@ export default function Intro() {
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
QToast({
|
||||
toastUp({
|
||||
message: 'This is a toast message',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
@ -1,20 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react'
|
||||
import ColorPicker from './common/color-picker/ColorPicker'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { FaAnglesUp } from 'react-icons/fa6'
|
||||
import { FaAnglesDown } from 'react-icons/fa6'
|
||||
|
||||
import { useAxios } from '@/hooks/useAxios'
|
||||
import { useMessage } from '@/hooks/useMessage'
|
||||
// import { get } from '@/lib/Axios'
|
||||
import { convertDwgToPng } from '@/lib/cadAction'
|
||||
import { cadFileNameState, googleMapFileNameState, useCadFileState, useGoogleMapFileState } from '@/store/canvasAtom'
|
||||
|
||||
import QSelect from '@/components/ui/QSelect'
|
||||
import { Button } from '@nextui-org/react'
|
||||
import ColorPicker from './common/color-picker/ColorPicker'
|
||||
import { toastUp } from '@/hooks/useToast'
|
||||
|
||||
import styles from './playground.module.css'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function Playground() {
|
||||
const { get } = useAxios()
|
||||
const [useCadFile, setUseCadFile] = useRecoilState(useCadFileState)
|
||||
const [cadFileName, setCadFileName] = useRecoilState(cadFileNameState)
|
||||
const [useGoogleMapFile, setUseGoogleMapFile] = useRecoilState(useGoogleMapFileState)
|
||||
const [googleMapFileName, setGoogleMapFileName] = useRecoilState(googleMapFileNameState)
|
||||
const fileRef = useRef(null)
|
||||
const queryRef = useRef(null)
|
||||
const [zoom, setZoom] = useState(20)
|
||||
const { get, promisePost } = useAxios()
|
||||
const testVar = process.env.NEXT_PUBLIC_TEST
|
||||
const converterUrl = process.env.NEXT_PUBLIC_CONVERTER_API_URL
|
||||
const { getMessage } = useMessage()
|
||||
|
||||
const [color, setColor] = useState('#ff0000')
|
||||
@ -28,6 +43,46 @@ export default function Playground() {
|
||||
console.log('users', users)
|
||||
}
|
||||
|
||||
const handleConvert = async () => {
|
||||
console.log('file', fileRef.current.files[0])
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', fileRef.current.files[0])
|
||||
|
||||
await promisePost({ url: converterUrl, data: formData })
|
||||
.then((res) => {
|
||||
console.log('response: ', res)
|
||||
convertDwgToPng(res.data.Files[0].FileName, res.data.Files[0].FileData)
|
||||
setUseCadFile(true)
|
||||
setCadFileName(res.data.Files[0].FileName)
|
||||
toastUp({ message: '파일 변환 완료', type: 'success' })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
toastUp({ message: '파일 변환 실패', type: 'error' })
|
||||
})
|
||||
}
|
||||
|
||||
const handleDownImage = async (fileName = '') => {
|
||||
const fileNm = fileName === '' ? uuidv4() : fileName
|
||||
const queryString = queryRef.current.value === '' ? '서울시 서대문구 연세로5다길 22-3 발리빌라 3층' : queryRef.current.value
|
||||
const res = await get({ url: `http://localhost:3000/api/html2canvas?q=${queryString}&fileNm=${fileNm}&zoom=${zoom}` })
|
||||
console.log('res', res)
|
||||
setGoogleMapFileName(res.fileNm)
|
||||
toastUp({ message: '이미지 저장 완료', type: 'success' })
|
||||
setUseGoogleMapFile(true)
|
||||
}
|
||||
|
||||
const handleZoom = async (type) => {
|
||||
if (type === 'up') {
|
||||
setZoom((prevState) => prevState + 1)
|
||||
} else {
|
||||
setZoom((prevState) => prevState - 1)
|
||||
}
|
||||
|
||||
await handleDownImage()
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
@ -70,6 +125,32 @@ export default function Playground() {
|
||||
<ColorPicker color={color} setColor={setColor} />
|
||||
<div className="p-4">{color}</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl">캐드 파일 이미지 사용</h1>
|
||||
<input type="file" name="file" ref={fileRef} />
|
||||
<div>
|
||||
<Button onClick={handleConvert}>Convert</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl">구글 맵 이미지 사용</h1>
|
||||
<input type="text" ref={queryRef} className="w-80 border-medium my-2" />
|
||||
<div>
|
||||
<Button onClick={handleDownImage}>Google map Download to Image</Button>
|
||||
</div>
|
||||
{useGoogleMapFile && (
|
||||
<>
|
||||
<div className="my-2">
|
||||
<p className="text-lg">Zoom Controller : {zoom}</p>
|
||||
<Button startContent={<FaAnglesUp />} className="mx-2" onClick={() => handleZoom('up')}></Button>
|
||||
<Button startContent={<FaAnglesDown />} className="mx-2" onClick={() => handleZoom('down')}></Button>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Image src={`/mapImages/${googleMapFileName}`} width={640} height={640} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { Children, useEffect, useState } from 'react'
|
||||
|
||||
export default function QContextMenu(props) {
|
||||
const { contextRef, canvasProps } = props
|
||||
@ -8,6 +7,19 @@ export default function QContextMenu(props) {
|
||||
// const children = useRecoilValue(modalContent)
|
||||
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 })
|
||||
|
||||
const activeObject = canvasProps.getActiveObject() //액티브된 객체를 가져옴
|
||||
|
||||
let contextType = ''
|
||||
|
||||
if (activeObject) {
|
||||
if (activeObject.initOptions) {
|
||||
//이건 바뀔 가능성이 있음
|
||||
if (activeObject.initOptions.name.indexOf('guide') > -1) {
|
||||
contextType = 'surface' //면형상
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextRef.current) return
|
||||
|
||||
@ -29,21 +41,49 @@ export default function QContextMenu(props) {
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent the default context menu from appearing on the canvas
|
||||
canvasProps.upperCanvasEl.addEventListener('contextmenu', handleContextMenu)
|
||||
document.addEventListener('click', handleClick)
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
|
||||
return () => {
|
||||
// canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu)
|
||||
document.removeEventListener('click', handleClick)
|
||||
document.removeEventListener('click', handleOutsideClick)
|
||||
}
|
||||
}, [contextRef, contextMenu])
|
||||
|
||||
const handleMenuClick = (option) => {
|
||||
alert(`option ${option} clicked`)
|
||||
setContextMenu({ ...contextMenu, visible: false })
|
||||
const handleObjectMove = () => {
|
||||
activeObject.set({
|
||||
lockMovementX: false, // X 축 이동 잠금
|
||||
lockMovementY: false, // Y 축 이동 잠금
|
||||
})
|
||||
|
||||
canvasProps.on('object:modified', function (e) {
|
||||
activeObject.set({
|
||||
lockMovementX: true, // X 축 이동 잠금
|
||||
lockMovementY: true, // Y 축 이동 잠금
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleObjectDelete = () => {
|
||||
if (confirm('삭제하실거?')) {
|
||||
canvasProps.remove(activeObject)
|
||||
}
|
||||
}
|
||||
|
||||
const handleObjectCopy = () => {
|
||||
activeObject.clone((cloned) => {
|
||||
cloned.set({
|
||||
left: activeObject.left + activeObject.width + 20,
|
||||
initOptions: { ...activeObject.initOptions },
|
||||
lockMovementX: true, // X 축 이동 잠금
|
||||
lockMovementY: true, // Y 축 이동 잠금
|
||||
lockRotation: true, // 회전 잠금
|
||||
lockScalingX: true, // X 축 크기 조정 잠금
|
||||
lockScalingY: true, // Y 축 크기 조정 잠금
|
||||
})
|
||||
canvasProps?.add(cloned)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@ -51,15 +91,16 @@ export default function QContextMenu(props) {
|
||||
{contextMenu.visible && (
|
||||
<div style={{ position: 'absolute', top: contextMenu.y, left: contextMenu.x, zIndex: 2000 }}>
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: '5px' }}>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
|
||||
Option 1
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleObjectMove()}>
|
||||
이동
|
||||
</li>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(2)}>
|
||||
Option 2
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleObjectDelete()}>
|
||||
삭제
|
||||
</li>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(3)}>
|
||||
Option 3
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleObjectCopy()}>
|
||||
복사
|
||||
</li>
|
||||
{props.children}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,71 +1,20 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import QContextMenu from './QContextMenu'
|
||||
|
||||
export default function QPolygonContextMenu(props) {
|
||||
const { contextRef, canvasProps } = props
|
||||
|
||||
// const children = useRecoilValue(modalContent)
|
||||
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextRef.current) return
|
||||
|
||||
const handleContextMenu = (e) => {
|
||||
e.preventDefault() //기존 contextmenu 막고
|
||||
setContextMenu({ visible: true, x: e.pageX, y: e.pageY })
|
||||
canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu) //한번 노출 후 이벤트 삭제
|
||||
}
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault()
|
||||
setContextMenu({ ...contextMenu, visible: false })
|
||||
}
|
||||
|
||||
const handleOutsideClick = (e) => {
|
||||
e.preventDefault()
|
||||
if (contextMenu.visible && !ref.current.contains(e.target)) {
|
||||
setContextMenu({ ...contextMenu, visible: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent the default context menu from appearing on the canvas
|
||||
canvasProps.upperCanvasEl.addEventListener('contextmenu', handleContextMenu)
|
||||
document.addEventListener('click', handleClick)
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
|
||||
return () => {
|
||||
// canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu)
|
||||
document.removeEventListener('click', handleClick)
|
||||
document.removeEventListener('click', handleOutsideClick)
|
||||
}
|
||||
}, [contextRef, contextMenu])
|
||||
|
||||
const handleMenuClick = (option) => {
|
||||
alert(`option ${option} clicked`)
|
||||
setContextMenu({ ...contextMenu, visible: false })
|
||||
function handleMenuClick(index) {
|
||||
alert('test')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextMenu.visible && (
|
||||
<div style={{ position: 'absolute', top: contextMenu.y, left: contextMenu.x, zIndex: 2000 }}>
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: '5px' }}>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
|
||||
polygon
|
||||
</li>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
|
||||
Option 1
|
||||
</li>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(2)}>
|
||||
Option 2
|
||||
</li>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(3)}>
|
||||
Option 3
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<QContextMenu contextRef={contextRef} canvasProps={canvasProps}>
|
||||
<>
|
||||
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(4)}>
|
||||
모듈 채우기
|
||||
</li>
|
||||
</>
|
||||
</QContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,7 +2,15 @@ import { fabric } from 'fabric'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util'
|
||||
import { calculateAngle, drawHippedRoof, inPolygon, splitPolygonWithLines, toGeoJSON } from '@/util/qpolygon-utils'
|
||||
import {
|
||||
calculateAngle,
|
||||
drawDirectionArrow,
|
||||
drawHippedRoof,
|
||||
drawPolygonArrow,
|
||||
inPolygon,
|
||||
splitPolygonWithLines,
|
||||
toGeoJSON,
|
||||
} from '@/util/qpolygon-utils'
|
||||
import * as turf from '@turf/turf'
|
||||
|
||||
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
@ -19,6 +27,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
innerLines: [],
|
||||
children: [],
|
||||
initOptions: null,
|
||||
direction: null,
|
||||
arrow: null,
|
||||
initialize: function (points, options, canvas) {
|
||||
// 소수점 전부 제거
|
||||
points.forEach((point) => {
|
||||
@ -100,6 +110,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
|
||||
toObject: function (propertiesToInclude) {
|
||||
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
text: this.text,
|
||||
hips: this.hips,
|
||||
@ -116,9 +127,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
|
||||
this.on('modified', (e) => {
|
||||
this.addLengthText()
|
||||
if (this.arrow) {
|
||||
drawDirectionArrow(this)
|
||||
}
|
||||
})
|
||||
|
||||
this.on('selected', () => {
|
||||
drawDirectionArrow(this)
|
||||
Object.keys(this.controls).forEach((controlKey) => {
|
||||
if (controlKey !== 'ml' && controlKey !== 'mr') {
|
||||
this.setControlVisible(controlKey, false)
|
||||
@ -132,6 +147,17 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.canvas.remove(text)
|
||||
})
|
||||
this.texts = null
|
||||
|
||||
if (this.arrow) {
|
||||
this.canvas.remove(this.arrow)
|
||||
this.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.name === 'directionText' && obj.parent === this.arrow)
|
||||
.forEach((text) => {
|
||||
this.canvas.remove(text)
|
||||
})
|
||||
this.arrow = null
|
||||
}
|
||||
})
|
||||
|
||||
// polygon.fillCell({ width: 50, height: 30, padding: 10 })
|
||||
@ -162,11 +188,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
},
|
||||
|
||||
addLengthText() {
|
||||
if (this.texts.length > 0) {
|
||||
this.texts.forEach((text) => {
|
||||
this.canvas
|
||||
?.getObjects()
|
||||
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
|
||||
.forEach((text) => {
|
||||
this.canvas.remove(text)
|
||||
})
|
||||
}
|
||||
|
||||
let points = this.getCurrentPoints()
|
||||
|
||||
points.forEach((start, i) => {
|
||||
@ -209,7 +237,12 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
},
|
||||
setFontSize(fontSize) {
|
||||
this.fontSize = fontSize
|
||||
this.text.set({ fontSize })
|
||||
this.canvas
|
||||
?.getObjects()
|
||||
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
|
||||
.forEach((text) => {
|
||||
text.set({ fontSize: fontSize })
|
||||
})
|
||||
},
|
||||
_render: function (ctx) {
|
||||
this.callSuper('_render', ctx)
|
||||
@ -689,9 +722,20 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.wall = wall
|
||||
},
|
||||
setViewLengthText(isView) {
|
||||
this.texts.forEach((text) => {
|
||||
text.set({ visible: isView })
|
||||
})
|
||||
this.canvas
|
||||
?.getObjects()
|
||||
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
|
||||
.forEach((text) => {
|
||||
text.set({ visible: isView })
|
||||
})
|
||||
},
|
||||
setScaleX(scale) {
|
||||
this.scaleX = scale
|
||||
this.addLengthText()
|
||||
},
|
||||
setScaleY(scale) {
|
||||
this.scaleY = scale
|
||||
this.addLengthText()
|
||||
},
|
||||
divideLine() {
|
||||
splitPolygonWithLines(this)
|
||||
|
||||
7
src/components/floor-plan/FloorPlan.jsx
Normal file
7
src/components/floor-plan/FloorPlan.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function FloorPlan() {
|
||||
return (
|
||||
<>
|
||||
<h1>도면 작성 페이지</h1>
|
||||
</>
|
||||
)
|
||||
}
|
||||
155
src/components/ui/ObjectPlacement.jsx
Normal file
155
src/components/ui/ObjectPlacement.jsx
Normal file
@ -0,0 +1,155 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Button, Input } from '@nextui-org/react'
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil'
|
||||
import { modalState } from '@/store/modalAtom'
|
||||
import { fabric } from 'fabric'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import { modeState, objectPlacementModeState } from '@/store/canvasAtom'
|
||||
|
||||
const BATCH_TYPE = {
|
||||
OPENING: 'opening',
|
||||
SHADOW: 'shadow',
|
||||
}
|
||||
|
||||
const INPUT_TYPE = {
|
||||
FREE: 'free',
|
||||
DIMENSION: 'dimension',
|
||||
}
|
||||
|
||||
const ObjectPlacement = ({ canvas }) => {
|
||||
const [open, setOpen] = useRecoilState(modalState)
|
||||
const [mode, setMode] = useRecoilState(modeState)
|
||||
const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState)
|
||||
const [width, setWidth] = useState(0)
|
||||
const [height, setHeight] = useState(0)
|
||||
const [areaBoundary, setAreaBoundary] = useState(true)
|
||||
|
||||
// opening or shadow 개구 / 그림자
|
||||
const [batchType, setBatchType] = useState(BATCH_TYPE.OPENING)
|
||||
|
||||
// free or dimension 프리 / 치수
|
||||
const [inputType, setInputType] = useState(INPUT_TYPE.FREE)
|
||||
|
||||
const handleSave = () => {
|
||||
setMode(batchType)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full max-w-xs border border-gray-300">
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">오브젝트 배치</label>
|
||||
</div>
|
||||
|
||||
<div className="border-b mb-4">
|
||||
<h2 className="font-semibold">개구 · 그림자 배치</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex">
|
||||
<Button
|
||||
className={`w-1/2 py-2 ${objectPlacementMode.batchType === BATCH_TYPE.OPENING ? 'bg-blue-500 text-white rounded-l' : 'bg-gray-200 text-gray-700 rounded-r'}`}
|
||||
onClick={() => {
|
||||
setBatchType(BATCH_TYPE.OPENING)
|
||||
setObjectPlacementModeState({ ...objectPlacementMode, batchType: BATCH_TYPE.OPENING })
|
||||
}}
|
||||
>
|
||||
개구 배치
|
||||
</Button>
|
||||
<Button
|
||||
className={`w-1/2 py-2 ${objectPlacementMode.batchType === BATCH_TYPE.SHADOW ? 'bg-blue-500 text-white rounded-l' : 'bg-gray-200 text-gray-700 rounded-r'}`}
|
||||
onClick={() => {
|
||||
setBatchType(BATCH_TYPE.SHADOW)
|
||||
setObjectPlacementModeState({ ...objectPlacementMode, batchType: BATCH_TYPE.SHADOW })
|
||||
}}
|
||||
>
|
||||
그림자 배치
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-gray-700 font-semibold">설정</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="inline-flex items-center">
|
||||
<Input
|
||||
type="radio"
|
||||
name="inputType"
|
||||
checked={objectPlacementMode.inputType === INPUT_TYPE.FREE}
|
||||
onClick={() => {
|
||||
setObjectPlacementModeState({ ...objectPlacementMode, inputType: INPUT_TYPE.FREE })
|
||||
}}
|
||||
className="form-radio text-blue-500"
|
||||
/>
|
||||
<span className="ml-2">프리입력</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="inline-flex items-center">
|
||||
<Input
|
||||
type="radio"
|
||||
name="inputType"
|
||||
checked={objectPlacementMode.inputType === INPUT_TYPE.DIMENSION}
|
||||
onClick={() => {
|
||||
setObjectPlacementModeState({ ...objectPlacementMode, inputType: INPUT_TYPE.DIMENSION })
|
||||
}}
|
||||
className="form-radio text-blue-500"
|
||||
/>
|
||||
<span className="ml-2">치수입력</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex mb-2">
|
||||
<div className="mr-2">
|
||||
<label className="block text-gray-700 text-sm mb-1">가로길이</label>
|
||||
<Input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
placeholder="mm"
|
||||
value={objectPlacementMode.width}
|
||||
onChange={(e) => {
|
||||
setObjectPlacementModeState({ ...objectPlacementMode, width: e.target.value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-1">세로길이</label>
|
||||
<Input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
placeholder="mm"
|
||||
value={objectPlacementMode.height}
|
||||
onChange={(e) => {
|
||||
setObjectPlacementModeState({ ...objectPlacementMode, height: e.target.value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="inline-flex items-center">
|
||||
<Input
|
||||
type="checkbox"
|
||||
name={`areaBoundary`}
|
||||
checked={objectPlacementMode.areaBoundary}
|
||||
onClick={() => setObjectPlacementModeState({ ...objectPlacementMode, areaBoundary: !objectPlacementMode.areaBoundary })}
|
||||
className="form-checkbox text-blue-500"
|
||||
/>
|
||||
<span className="ml-2">영역교차</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button onClick={handleSave} className="bg-gray-500 text-white py-2 px-4 rounded">
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ObjectPlacement
|
||||
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,10 @@ export function useAxios() {
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const promisePost = async ({ url, data }) => {
|
||||
return await getInstances(url).post(url, data)
|
||||
}
|
||||
|
||||
const put = async ({ url, data }) => {
|
||||
return await getInstances(url)
|
||||
.put(url, data)
|
||||
@ -66,5 +70,5 @@ export function useAxios() {
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
return { get, post, put, patch, del }
|
||||
return { get, post, promisePost, put, patch, del }
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ export function useCanvas(id) {
|
||||
const [canvas, setCanvas] = useState()
|
||||
const [isLocked, setIsLocked] = useState(false)
|
||||
const [history, setHistory] = useState([])
|
||||
const [backImg, setBackImg] = useState()
|
||||
const [canvasSize] = useRecoilState(canvasSizeState)
|
||||
const [fontSize] = useRecoilState(fontSizeState)
|
||||
const { setCanvasForEvent, attachDefaultEventOnCanvas } = useCanvasEvent()
|
||||
@ -427,6 +428,7 @@ export function useCanvas(id) {
|
||||
'minY',
|
||||
'x',
|
||||
'y',
|
||||
'stickeyPoint',
|
||||
])
|
||||
|
||||
const str = JSON.stringify(objs)
|
||||
@ -444,6 +446,30 @@ export function useCanvas(id) {
|
||||
// }, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* cad 파일 사용시 이미지 로딩 함수
|
||||
*/
|
||||
const handleBackImageLoadToCanvas = (url) => {
|
||||
console.log('image load url: ', url)
|
||||
|
||||
fabric.Image.fromURL(url, function (img) {
|
||||
img.set({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 1500,
|
||||
height: 1500,
|
||||
selectable: true,
|
||||
})
|
||||
canvas.add(img)
|
||||
canvas.renderAll()
|
||||
setBackImg(img)
|
||||
})
|
||||
}
|
||||
|
||||
const handleCadImageInit = () => {
|
||||
canvas.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
addShape,
|
||||
@ -459,5 +485,9 @@ export function useCanvas(id) {
|
||||
setCanvasBackgroundWithDots,
|
||||
addCanvas,
|
||||
removeMouseLines,
|
||||
handleBackImageLoadToCanvas,
|
||||
handleCadImageInit,
|
||||
backImg,
|
||||
setBackImg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,11 +27,12 @@ import {
|
||||
guideLineState,
|
||||
horiGuideLinesState,
|
||||
vertGuideLinesState,
|
||||
objectPlacementModeState,
|
||||
} from '@/store/canvasAtom'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { fabric } from 'fabric'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import offsetPolygon from '@/util/qpolygon-utils'
|
||||
import offsetPolygon, { inPolygon } from '@/util/qpolygon-utils'
|
||||
import { isObjectNotEmpty } from '@/util/common-utils'
|
||||
import * as turf from '@turf/turf'
|
||||
import { Mode } from '@/common/common'
|
||||
@ -74,6 +75,8 @@ export function useMode() {
|
||||
const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState)
|
||||
const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState)
|
||||
|
||||
const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState)
|
||||
|
||||
useEffect(() => {
|
||||
// if (!canvas) {
|
||||
// canvas?.setZoom(0.8)
|
||||
@ -81,8 +84,11 @@ export function useMode() {
|
||||
// }
|
||||
if (!canvas) return
|
||||
setCanvas(canvas)
|
||||
canvas?.off('mouse:out', removeMouseLines)
|
||||
canvas?.on('mouse:out', removeMouseLines)
|
||||
canvas?.off('mouse:move')
|
||||
canvas?.on('mouse:move', drawMouseLines)
|
||||
}, [canvas]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함
|
||||
}, [canvas, zoom]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함
|
||||
|
||||
useEffect(() => {
|
||||
if (canvas?.getObjects().find((obj) => obj.name === 'connectLine')) {
|
||||
@ -101,11 +107,7 @@ export function useMode() {
|
||||
}, [endPoint])
|
||||
|
||||
useEffect(() => {
|
||||
canvas?.off('mouse:out', removeMouseLines)
|
||||
canvas?.on('mouse:out', removeMouseLines)
|
||||
changeMode(canvas, mode)
|
||||
canvas?.off('mouse:move')
|
||||
canvas?.on('mouse:move', drawMouseLines)
|
||||
}, [mode, horiGuideLines, vertGuideLines])
|
||||
|
||||
useEffect(() => {
|
||||
@ -278,7 +280,7 @@ export function useMode() {
|
||||
}
|
||||
|
||||
// 가로선을 그립니다.
|
||||
const horizontalLine = new fabric.Line([0, newY, canvasSize.horizontal, newY], {
|
||||
const horizontalLine = new fabric.Line([0, newY, 2 * canvas.width, newY], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
@ -286,7 +288,7 @@ export function useMode() {
|
||||
})
|
||||
|
||||
// 세로선을 그립니다.
|
||||
const verticalLine = new fabric.Line([newX, 0, newX, canvasSize.vertical], {
|
||||
const verticalLine = new fabric.Line([newX, 0, newX, 2 * canvas.height], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
@ -420,6 +422,17 @@ export function useMode() {
|
||||
break
|
||||
case 'adsorptionPoint':
|
||||
canvas?.on('mouse:down', mouseEvent.adsorptionPoint)
|
||||
break
|
||||
case 'shadow':
|
||||
canvas?.on('mouse:down', mouseEvent.shadowMode.down)
|
||||
canvas?.on('mouse:move', mouseEvent.shadowMode.move)
|
||||
canvas?.on('mouse:up', mouseEvent.shadowMode.up)
|
||||
break
|
||||
case 'opening':
|
||||
canvas?.on('mouse:down', mouseEvent.openingMode.down)
|
||||
canvas?.on('mouse:move', mouseEvent.openingMode.move)
|
||||
canvas?.on('mouse:up', mouseEvent.openingMode.up)
|
||||
|
||||
break
|
||||
case 'default':
|
||||
canvas?.off('mouse:down')
|
||||
@ -584,10 +597,9 @@ export function useMode() {
|
||||
|
||||
const mouseAndkeyboardEventClear = () => {
|
||||
canvas?.off('mouse:down')
|
||||
Object.keys(mouseEvent).forEach((key) => {
|
||||
canvas?.off('mouse:down', mouseEvent[key])
|
||||
document.removeEventListener('contextmenu', mouseEvent[key])
|
||||
})
|
||||
canvas?.off('mouse:move')
|
||||
canvas?.off('mouse:up')
|
||||
canvas?.off('mouse:out')
|
||||
|
||||
Object.keys(keyboardEvent).forEach((key) => {
|
||||
document.removeEventListener('keydown', keyboardEvent[key])
|
||||
@ -647,7 +659,7 @@ export function useMode() {
|
||||
}
|
||||
|
||||
case 'Enter': {
|
||||
const result = prompt('입력하세요 (a(A패턴),b(B패턴),t(지붕))')
|
||||
const result = prompt('입력하세요 (a(A패턴), b(B패턴), t(지붕), e(변별))')
|
||||
|
||||
switch (result) {
|
||||
case 'a':
|
||||
@ -659,6 +671,8 @@ export function useMode() {
|
||||
case 't':
|
||||
templateMode()
|
||||
break
|
||||
case 'e':
|
||||
templateSideMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -669,6 +683,7 @@ export function useMode() {
|
||||
|
||||
const changeMode = (canvas, mode) => {
|
||||
mouseAndkeyboardEventClear()
|
||||
addCommonMouseEvent()
|
||||
setMode(mode)
|
||||
setCanvas(canvas)
|
||||
|
||||
@ -708,6 +723,12 @@ export function useMode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 모드에서 사용되는 공통 이벤트 추가
|
||||
const addCommonMouseEvent = () => {
|
||||
canvas?.on('mouse:move', drawMouseLines)
|
||||
canvas?.on('mouse:out', removeMouseLines)
|
||||
}
|
||||
|
||||
const changeKeyboardEvent = (mode) => {
|
||||
if (mode === Mode.EDIT) {
|
||||
switch (mode) {
|
||||
@ -930,7 +951,7 @@ export function useMode() {
|
||||
width: pointer.x - origX,
|
||||
height: pointer.y - origY,
|
||||
angle: 0,
|
||||
fill: 'transparent',
|
||||
fill: 'white',
|
||||
stroke: 'black',
|
||||
transparentCorners: false,
|
||||
})
|
||||
@ -987,6 +1008,168 @@ export function useMode() {
|
||||
canvas.add(circle)
|
||||
canvas.renderAll()
|
||||
},
|
||||
//면 형상 배치 모드
|
||||
surfaceShapeMode: (o) => {},
|
||||
// 그림자 모드
|
||||
shadowMode: {
|
||||
rect: null,
|
||||
isDown: false,
|
||||
origX: 0,
|
||||
origY: 0,
|
||||
down: (o) => {
|
||||
if (mode !== Mode.SHADOW) return
|
||||
mouseEvent.shadowMode.isDown = true
|
||||
const pointer = canvas.getPointer(o.e)
|
||||
mouseEvent.shadowMode.origX = pointer.x
|
||||
mouseEvent.shadowMode.origY = pointer.y
|
||||
mouseEvent.shadowMode.rect = new fabric.Rect({
|
||||
fill: 'grey',
|
||||
left: mouseEvent.shadowMode.origX,
|
||||
top: mouseEvent.shadowMode.origY,
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
opacity: 0.3,
|
||||
width: 0,
|
||||
height: 0,
|
||||
angle: 0,
|
||||
transparentCorners: false,
|
||||
})
|
||||
canvas.add(mouseEvent.shadowMode.rect)
|
||||
},
|
||||
move: (e) => {
|
||||
if (!mouseEvent.shadowMode.isDown) return
|
||||
const pointer = canvas.getPointer(e.e)
|
||||
if (mouseEvent.shadowMode.origX > pointer.x) {
|
||||
mouseEvent.shadowMode.rect.set({ left: Math.abs(pointer.x) })
|
||||
}
|
||||
if (mouseEvent.shadowMode.origY > pointer.y) {
|
||||
mouseEvent.shadowMode.rect.set({ top: Math.abs(pointer.y) })
|
||||
}
|
||||
|
||||
mouseEvent.shadowMode.rect.set({ width: Math.abs(mouseEvent.shadowMode.origX - pointer.x) })
|
||||
mouseEvent.shadowMode.rect.set({ height: Math.abs(mouseEvent.shadowMode.origY - pointer.y) })
|
||||
},
|
||||
up: (o) => {
|
||||
mouseEvent.shadowMode.isDown = false
|
||||
setMode(Mode.DEFAULT)
|
||||
},
|
||||
},
|
||||
openingMode: {
|
||||
rect: null,
|
||||
isDown: false,
|
||||
origX: 0,
|
||||
origY: 0,
|
||||
down: (o) => {
|
||||
if (mode !== Mode.OPENING) return
|
||||
const roofs = canvas?._objects.filter((obj) => obj.name === 'roof')
|
||||
if (roofs.length === 0) {
|
||||
alert('지붕을 먼저 그려주세요')
|
||||
setMode(Mode.DEFAULT)
|
||||
return
|
||||
}
|
||||
const pointer = canvas.getPointer(o.e)
|
||||
let selectRoof = null
|
||||
roofs.forEach((roof) => {
|
||||
if (roof.inPolygon({ x: pointer.x, y: pointer.y })) {
|
||||
selectRoof = roof
|
||||
}
|
||||
})
|
||||
if (!selectRoof) {
|
||||
alert('지붕 내부에만 생성 가능합니다.')
|
||||
return
|
||||
}
|
||||
mouseEvent.openingMode.isDown = true
|
||||
|
||||
mouseEvent.openingMode.origX = pointer.x
|
||||
mouseEvent.openingMode.origY = pointer.y
|
||||
mouseEvent.openingMode.rect = new fabric.Rect({
|
||||
fill: 'white',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
left: mouseEvent.openingMode.origX,
|
||||
top: mouseEvent.openingMode.origY,
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
width: pointer.x - mouseEvent.openingMode.origX,
|
||||
height: pointer.y - mouseEvent.openingMode.origY,
|
||||
angle: 0,
|
||||
transparentCorners: false,
|
||||
})
|
||||
canvas.add(mouseEvent.openingMode.rect)
|
||||
},
|
||||
move: (e) => {
|
||||
if (!mouseEvent.openingMode.isDown) return
|
||||
const pointer = canvas.getPointer(e.e)
|
||||
if (mouseEvent.openingMode.origX > pointer.x) {
|
||||
mouseEvent.openingMode.rect.set({ left: Math.abs(pointer.x) })
|
||||
}
|
||||
if (mouseEvent.openingMode.origY > pointer.y) {
|
||||
mouseEvent.openingMode.rect.set({ top: Math.abs(pointer.y) })
|
||||
}
|
||||
|
||||
mouseEvent.openingMode.rect.set({ width: Math.abs(mouseEvent.openingMode.origX - pointer.x) })
|
||||
mouseEvent.openingMode.rect.set({ height: Math.abs(mouseEvent.openingMode.origY - pointer.y) })
|
||||
},
|
||||
up: (o) => {
|
||||
mouseEvent.openingMode.isDown = false
|
||||
|
||||
const { areaBoundary } = objectPlacementMode
|
||||
|
||||
//roof의 내부에 있는지 확인
|
||||
if (!checkInsideRoof(mouseEvent.openingMode.rect)) {
|
||||
setMode(Mode.DEFAULT)
|
||||
}
|
||||
|
||||
// 영역 교차인지 확인
|
||||
if (!areaBoundary) {
|
||||
const isCross = checkCrossAreaBoundary(mouseEvent.openingMode.rect)
|
||||
if (isCross) {
|
||||
alert('영역이 교차되었습니다.')
|
||||
canvas.remove(mouseEvent.openingMode.rect)
|
||||
}
|
||||
}
|
||||
|
||||
mouseEvent.openingMode.rect.set({ name: 'opening' })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const checkCrossAreaBoundary = (rect) => {
|
||||
const openings = canvas?._objects.filter((obj) => obj.name === 'opening')
|
||||
if (openings.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rectPoints = [
|
||||
{ x: rect.left, y: rect.top },
|
||||
{ x: rect.left, y: rect.top + rect.height },
|
||||
{ x: rect.left + rect.width, y: rect.top + rect.height },
|
||||
{ x: rect.left + rect.width, y: rect.top },
|
||||
]
|
||||
|
||||
for (let i = 0; i < openings.length; i++) {
|
||||
const rect2 = openings[i]
|
||||
const rect2Points = [
|
||||
{ x: rect2.left, y: rect2.top },
|
||||
{ x: rect2.left, y: rect2.top + rect2.height },
|
||||
{ x: rect2.left + rect2.width, y: rect2.top + rect2.height },
|
||||
{ x: rect2.left + rect2.width, y: rect2.top },
|
||||
]
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const checkInsideRoof = (rect) => {
|
||||
let result = true
|
||||
const roofs = canvas?._objects.filter((obj) => obj.name === 'roof')
|
||||
if (roofs.length === 0) {
|
||||
alert('지붕을 먼저 그려주세요')
|
||||
canvas?.remove(rect)
|
||||
return false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const getInterSectPointByMouseLine = () => {
|
||||
@ -1050,6 +1233,18 @@ export function useMode() {
|
||||
setTemplateType(1)
|
||||
}
|
||||
}
|
||||
|
||||
const templateSideMode = () => {
|
||||
changeMode(canvas, Mode.EDIT)
|
||||
|
||||
if (historyPoints.current.length >= 4) {
|
||||
const wall = drawWallPolygon()
|
||||
setWall(wall)
|
||||
|
||||
console.log('sideWall', wall)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 점을 연결하는 선과 길이를 그립니다.
|
||||
* a : 시작점, b : 끝점
|
||||
@ -1138,11 +1333,17 @@ export function useMode() {
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
if (canvas.getZoom() + 0.1 > 1.6) {
|
||||
return
|
||||
}
|
||||
canvas?.setZoom(canvas.getZoom() + 0.1)
|
||||
setZoom(Math.round(zoom + 10))
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
if (canvas.getZoom() - 0.1 < 0.5) {
|
||||
return
|
||||
}
|
||||
canvas?.setZoom(canvas.getZoom() - 0.1)
|
||||
setZoom(Math.ceil(zoom - 10))
|
||||
}
|
||||
@ -4572,18 +4773,14 @@ export function useMode() {
|
||||
}
|
||||
|
||||
const createRoofRack = () => {
|
||||
roof.divideLine()
|
||||
const trestlePolygons = canvas?.getObjects().filter((obj) => obj.name === 'trestle')
|
||||
// 이미 만들어진 가대가 있을 경우 return
|
||||
if (trestlePolygons.length !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
removeMouseLines()
|
||||
canvas?.off('mouse:move')
|
||||
canvas?.off('mouse:out')
|
||||
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof')
|
||||
|
||||
roofs.forEach((roof, index) => {
|
||||
const offsetPolygonPoint = offsetPolygon(roof.points, -20)
|
||||
// const offsetPolygonPoint = offsetPolygon(roof.points(), -20) //이동되서 찍을라고 바꿈
|
||||
const offsetPolygonPoint = offsetPolygon(roof.getCurrentPoints(), -20)
|
||||
|
||||
const trestlePoly = new QPolygon(offsetPolygonPoint, {
|
||||
fill: 'transparent',
|
||||
@ -4604,6 +4801,7 @@ export function useMode() {
|
||||
})
|
||||
|
||||
canvas?.add(trestlePoly)
|
||||
trestlePoly.setViewLengthText(false)
|
||||
})
|
||||
removeHelpPointAndHelpLine()
|
||||
}
|
||||
@ -4637,10 +4835,10 @@ export function useMode() {
|
||||
|
||||
let drawRoofCells
|
||||
if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') {
|
||||
drawRoofCells = trestle.fillCell({ width: 50, height: 100, padding: 10 })
|
||||
drawRoofCells = trestle.fillCell({ width: 113.4, height: 172.2, padding: 0 })
|
||||
trestle.direction = 'south'
|
||||
} else {
|
||||
drawRoofCells = trestle.fillCell({ width: 100, height: 50, padding: 10 })
|
||||
drawRoofCells = trestle.fillCell({ width: 172.2, height: 113.4, padding: 0 })
|
||||
trestle.direction = 'east'
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const toastDefaultOptions = {
|
||||
closeOnClick: true,
|
||||
}
|
||||
|
||||
const QToast = (props) => {
|
||||
const toastUp = (props) => {
|
||||
// type TypeOptions = 'info' | 'success' | 'warning' | 'error' | 'default'
|
||||
const { message, type = 'info', options } = props
|
||||
const customOptions = { ...toastDefaultOptions, ...options }
|
||||
@ -32,4 +32,4 @@ const QToast = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
export { QToast }
|
||||
export { toastUp }
|
||||
|
||||
18
src/lib/cadAction.js
Normal file
18
src/lib/cadAction.js
Normal file
@ -0,0 +1,18 @@
|
||||
'use server'
|
||||
|
||||
import fs from 'fs/promises'
|
||||
|
||||
const imageSavePath = 'public/cadImages'
|
||||
|
||||
const convertDwgToPng = async (fileName, data) => {
|
||||
console.log('fileName', fileName)
|
||||
try {
|
||||
await fs.readdir(imageSavePath)
|
||||
} catch {
|
||||
await fs.mkdir(imageSavePath)
|
||||
}
|
||||
|
||||
return await fs.writeFile(`${imageSavePath}/${fileName}`, data, 'base64')
|
||||
}
|
||||
|
||||
export { convertDwgToPng }
|
||||
@ -1,3 +1,90 @@
|
||||
{
|
||||
"hi": "こんにちは"
|
||||
"hi": "こんにちは",
|
||||
"common.message.no.data": "No data",
|
||||
"common.message.no.dataDown": "ダウンロードするデータがありません",
|
||||
"common.message.noData": "表示するデータがありません",
|
||||
"common.message.search": "search success",
|
||||
"common.message.insert": "insert success",
|
||||
"common.message.update": "update success",
|
||||
"common.message.delete": "削除",
|
||||
"common.message.restoration": "復元",
|
||||
"common.message.cancel": "キャンセル",
|
||||
"common.message.send": "メールを送信しました.",
|
||||
"common.message.no.delete": "削除するデータがありません",
|
||||
"common.message.save": "保存",
|
||||
"common.message.transfer": "転送",
|
||||
"common.message.batch.exec": "batch success",
|
||||
"common.message.not.mov": "移動できません.",
|
||||
"common.message.required.data": "{0} は入力必須項目となります。",
|
||||
"common.message.save.error": "データの保存中にエラーが発生しました。 サイト管理者にお問い合わせください。",
|
||||
"common.message.transfer.error": "データの転送中にエラーが発生しました。 サイト管理者にお問い合わせください。",
|
||||
"common.message.delete.error": "データの削除中にエラーが発生しました。 サイト管理者にお問い合わせください。",
|
||||
"common.message.batch.error": "バッチの実行中にエラーが発生しました。 サイト管理者に連絡してください。",
|
||||
"common.message.send.error": "データの送信中にエラーが発生しました。サイト管理者にお問い合わせください",
|
||||
"common.message.communication.error": "ネットワークエラーが発生しました。サイト管理者に連絡してください。",
|
||||
"common.message.data.error": "{0} はデータ形式が無効です。",
|
||||
"common.message.data.setting.error": "{0} は削除されたか、すでに構成されているデータです。",
|
||||
"common.message.parameter.error": "パラメータエラー",
|
||||
"common.message.product.parameter.error": "存在しない製品があります。",
|
||||
"common.message.customer.parameter.error": "存在しない顧客があります。",
|
||||
"common.message.file.exists.error": "ファイルが正常にアップロードされないためにエラーが発生しました",
|
||||
"common.message.file.download.exists": "ファイルが存在しません。",
|
||||
"common.message.file.download.error": "ァイルのダウンロードエラー",
|
||||
"common.message.file.template.validation01": "フォルダをアップロードできません",
|
||||
"common.message.file.template.validation02": "アップロードできるのはExcelファイルのみです。",
|
||||
"common.message.file.template.validation03": "登録できない拡張子です",
|
||||
"common.message.file.template.validation04": "容量を超えています アップロード可能な容量:{0} MB",
|
||||
"common.message.file.template.validation05": "アップロードファイルを選択して下さい",
|
||||
"common.message.multi.insert": "合計 {0} 件数 ({1}成功、 {2} 失敗 {3})",
|
||||
"common.message.error": "エラーが発生しました。サイト管理者に連絡してください。",
|
||||
"common.message.data.save": "保存しますか?",
|
||||
"common.message.data.delete": " 削除しますか?",
|
||||
"common.message.data.exists": "{0} はすでに存在するデータです。",
|
||||
"common.message.data.no.exists": "{0} は存在しないデータです。",
|
||||
"common.message.all": "All",
|
||||
"common.message.tab.close.all": "すべてのタブを閉じますか?",
|
||||
"common.message.transfer.save": "{0}件転送しますか?",
|
||||
"common.message.confirm.save": "保存しますか?",
|
||||
"common.message.confirm.confirm": "承認しますか?",
|
||||
"common.message.confirm.request": "承認リクエストしますか?",
|
||||
"common.message.confirm.delete": "削除しますか?",
|
||||
"common.message.confirm.close": "閉じますか?",
|
||||
"common.message.confirm.unclose": "クローズ中止しますか?",
|
||||
"common.message.confirm.cancel": "キャンセルしますか?",
|
||||
"common.message.confirm.uncancel": "キャンセル中止しますか?",
|
||||
"common.message.confirm.copy": "コピーしますか?",
|
||||
"common.message.confirm.createSo": "S/O作成しますか?",
|
||||
"common.message.confirm.mark": "保存完了",
|
||||
"common.message.confirm.mail": "メールを送信しますか?",
|
||||
"common.message.confirm.printPriceItem": "価格を印刷しますか?",
|
||||
"common.message.confirm.allAppr ": "Do you want to Batch approve the selected data?",
|
||||
"common.message.confirm.deliveryFee": "送料を登録しますか?",
|
||||
"common.message.success.delete": "削除完了",
|
||||
"common.message.success.close": "閉じる",
|
||||
"common.message.success.unclose": "キャンセルしました",
|
||||
"common.message.validation.date": "終了日を開始日より前にすることはできません。 もう一度入力してください。",
|
||||
"common.message.no.editfield": "フィールドを編集できません",
|
||||
"common.message.success.rmmail": "リスク管理チームにメールを送信しました。",
|
||||
"common.message.password.validation01": "パスワードの変更が一致しません。",
|
||||
"common.message.password.validation02": "英語、数字、特殊文字を組み合わせた8桁以上を入力してください。",
|
||||
"common.message.password.validation03": "パスワードをIDと同じにすることはできません。",
|
||||
"common.message.menu.validation01": "注文を保存するメニューはありません.",
|
||||
"common.message.menu.validation02": "The same sort order exists.",
|
||||
"common.message.menuCode.check01": "登録可能",
|
||||
"common.message.menuCode.check02": "登録できません",
|
||||
"common.message.pleaseSelect": "{0}を選択してください",
|
||||
"common.message.pleaseInput": "{0}を入力してください。",
|
||||
"common.message.pleaseInputOr": "{0}または{1}を入力してください。",
|
||||
"common.message.approved ": "承認済み",
|
||||
"common.message.errorFieldExist": "エラー項目が存在します",
|
||||
"common.message.storeIdExist ": "既に利用されている販売店IDです",
|
||||
"common.message.userIdExist ": "すでに使用しているユーザーID。",
|
||||
"common.message.noExists ": "削除された掲示物です",
|
||||
"common.message.emailReqTo": "メール宛先が必要です",
|
||||
"common.message.downloadPeriod": "ダウンロード検索期間を{0}日以内に選択してください。",
|
||||
"common.message.backToSubmit": "販売店ブロック解除実行しますか?",
|
||||
"common.message.backToG3": "Back to G3処理実行しますか?",
|
||||
"common.message.writeToConfirm": "作成解除を実行しますか?",
|
||||
"common.message.password.init.success": "パスワード [{0}] に初期化されました。",
|
||||
"common.message.no.edit.save": "この文書は変更できません。"
|
||||
}
|
||||
|
||||
@ -1,3 +1,90 @@
|
||||
{
|
||||
"hi": "안녕하세요"
|
||||
"hi": "안녕하세요",
|
||||
"common.message.no.data": "No data",
|
||||
"common.message.no.dataDown": "No data to download",
|
||||
"common.message.noData": "No data to display",
|
||||
"common.message.search": "search success",
|
||||
"common.message.insert": "insert success",
|
||||
"common.message.update": "update success",
|
||||
"common.message.delete": "Deleted",
|
||||
"common.message.restoration": "Restored",
|
||||
"common.message.cancel": "Canceled",
|
||||
"common.message.send": "The mail has been sent.",
|
||||
"common.message.no.delete": "There is no data to delete.",
|
||||
"common.message.save": "Saved.",
|
||||
"common.message.transfer": "Transfered",
|
||||
"common.message.batch.exec": "batch success",
|
||||
"common.message.not.mov": "Its impossible to move.",
|
||||
"common.message.required.data": "{0} is required input value.",
|
||||
"common.message.save.error": "An error occurred while saving the data. Please contact site administrator.",
|
||||
"common.message.transfer.error": "An error occurred while transfer the data. Please contact site administrator.",
|
||||
"common.message.delete.error": "An error occurred while deleting data. Please contact site administrator.",
|
||||
"common.message.batch.error": "An error occurred while executing the batch. Please contact site administrator.",
|
||||
"common.message.send.error": "Error sending data, please contact your administrator.",
|
||||
"common.message.communication.error": "Network error occurred. \n Please contact site administrator.",
|
||||
"common.message.data.error": "{0} The data format is not valid.",
|
||||
"common.message.data.setting.error": "{0} is data that has been deleted or already configured.",
|
||||
"common.message.parameter.error": "Parameter Error",
|
||||
"common.message.product.parameter.error": "존재하지 않는 제품이 있습니다.",
|
||||
"common.message.customer.parameter.error": "존재하지 않는 고객이 있습니다.",
|
||||
"common.message.file.exists.error": "Error due to file not uploading normally",
|
||||
"common.message.file.download.exists": "File does not exist.",
|
||||
"common.message.file.download.error": "File download error",
|
||||
"common.message.file.template.validation01": "Unable to upload folder",
|
||||
"common.message.file.template.validation02": "Only Excel files can be uploaded.",
|
||||
"common.message.file.template.validation03": "Non-registerable extension",
|
||||
"common.message.file.template.validation04": "Exceed capacity \n Uploadable capacity : {0} MB",
|
||||
"common.message.file.template.validation05": "업로드 파일을 선택해주세요.",
|
||||
"common.message.multi.insert": "Total {0} cases ({1} successes, {2} failures {3})",
|
||||
"common.message.error": "Error occurred, please contact site administrator.",
|
||||
"common.message.data.save": "Do you want to save it?",
|
||||
"common.message.data.delete": "Do you want to delete it?",
|
||||
"common.message.data.exists": "{0} is data that already exists.",
|
||||
"common.message.data.no.exists": "{0} is data that does not exist.",
|
||||
"common.message.all": "All",
|
||||
"common.message.tab.close.all": "Close all tabs?",
|
||||
"common.message.transfer.save": "Want to {0} transfer it?",
|
||||
"common.message.confirm.save": "Want to save it?",
|
||||
"common.message.confirm.confirm": "Want to approve?",
|
||||
"common.message.confirm.request": "Would you like to request a Approval?",
|
||||
"common.message.confirm.delete": "Do you want to delete it?",
|
||||
"common.message.confirm.close": "Want to close?",
|
||||
"common.message.confirm.unclose": "Do you want to cancel the close?",
|
||||
"common.message.confirm.cancel": "Want to cancellation?",
|
||||
"common.message.confirm.uncancel": "Do you want to cancel the cancellation?",
|
||||
"common.message.confirm.copy": "Do you want to copy?",
|
||||
"common.message.confirm.createSo": "Create Sales Order?",
|
||||
"common.message.confirm.mark": "Saved.",
|
||||
"common.message.confirm.mail": "Do you want to send mail?",
|
||||
"common.message.confirm.printPriceItem": "Would you like to print item price?",
|
||||
"common.message.confirm.allAppr ": "Do you want to Batch approve the selected data?",
|
||||
"common.message.confirm.deliveryFee": "Do you want to register shipping fee?",
|
||||
"common.message.success.delete": "Deleted.",
|
||||
"common.message.success.close": "Closed.",
|
||||
"common.message.success.unclose": "Cancel Closed.",
|
||||
"common.message.validation.date": "The end date cannot be earlier than the start date. Please enter it again.",
|
||||
"common.message.no.editfield": "Can not edit field",
|
||||
"common.message.success.rmmail": "You have successfully sent mail to the Risk Management team.",
|
||||
"common.message.password.validation01": "Change passwords do not match.",
|
||||
"common.message.password.validation02": "Please enter at least 8 digits combining English, numbers, and special characters.",
|
||||
"common.message.password.validation03": "Password cannot be the same as ID.",
|
||||
"common.message.menu.validation01": "There is no menu to save the order.",
|
||||
"common.message.menu.validation02": "The same sort order exists.",
|
||||
"common.message.menuCode.check01": "Registerable",
|
||||
"common.message.menuCode.check02": "Unable to register",
|
||||
"common.message.pleaseSelect": "Please Select {0}",
|
||||
"common.message.pleaseInput": "Please Input a {0}.",
|
||||
"common.message.pleaseInputOr": "Please Input a {0} or {1}.",
|
||||
"common.message.approved ": "Approved.",
|
||||
"common.message.errorFieldExist": "Error Field Exist",
|
||||
"common.message.storeIdExist ": "이미 사용하고 있는 판매점 ID 입니다.",
|
||||
"common.message.userIdExist ": "이미 사용하고 있는 사용자 ID 입니다.",
|
||||
"common.message.noExists ": "삭제된 게시물 입니다.",
|
||||
"common.message.emailReqTo": "Email To is required",
|
||||
"common.message.downloadPeriod": "Please select the download search period within {0} days.",
|
||||
"common.message.backToSubmit": "판매점 블록 해제를 실행하시겠습니까?",
|
||||
"common.message.backToG3": "Back to G3 처리를 실행하시겠습니까?",
|
||||
"common.message.writeToConfirm": "작성 해제를 실행하시겠습니까?",
|
||||
"common.message.password.init.success": "비밀번호 [{0}]로 초기화 되었습니다.",
|
||||
"common.message.no.edit.save": "This document cannot be changed."
|
||||
}
|
||||
|
||||
@ -126,3 +126,50 @@ export const customSettingsState = atom({
|
||||
default: {},
|
||||
dangerouslyAllowMutability: true,
|
||||
})
|
||||
|
||||
// cad 도면 파일 사용 여부
|
||||
export const useCadFileState = atom({
|
||||
key: 'useCadFile',
|
||||
default: false,
|
||||
})
|
||||
|
||||
// cad 도면 파일 이름
|
||||
export const cadFileNameState = atom({
|
||||
key: 'cadFileName',
|
||||
default: '',
|
||||
})
|
||||
|
||||
// cad 도면 파일 조정 완료
|
||||
export const cadFileCompleteState = atom({
|
||||
key: 'cadFileComplete',
|
||||
default: false,
|
||||
})
|
||||
|
||||
export const useGoogleMapFileState = atom({
|
||||
key: 'useGoogleMapFile',
|
||||
default: false,
|
||||
})
|
||||
|
||||
// 구글맵 저장 이미지 파일 이름
|
||||
export const googleMapFileNameState = atom({
|
||||
key: 'googleMapFileName',
|
||||
default: '',
|
||||
})
|
||||
|
||||
export const globalCompassState = atom({
|
||||
key: 'globalCompass',
|
||||
default: 0,
|
||||
dangerouslyAllowMutability: true,
|
||||
})
|
||||
|
||||
// 면형상 배치 모드
|
||||
export const surfacePlacementModeState = atom({
|
||||
key: 'surfacePlacementMode',
|
||||
default: { width: 0, height: 0, areaBoundary: true, inputType: 'free' },
|
||||
})
|
||||
|
||||
// 오브젝트 배치 모드
|
||||
export const objectPlacementModeState = atom({
|
||||
key: 'objectPlacementMode',
|
||||
default: { width: 0, height: 0, areaBoundary: false, inputType: 'free', batchType: 'opening' },
|
||||
})
|
||||
|
||||
@ -705,3 +705,23 @@ export const getClosestVerticalLine = (pointer, verticalLineArray) => {
|
||||
|
||||
return closestLine
|
||||
}
|
||||
|
||||
/**
|
||||
* 빗변과 높이를 가지고 빗변의 x값을 구함
|
||||
* @param {} p1
|
||||
* @param {*} p2
|
||||
* @param {*} y
|
||||
* @returns
|
||||
*/
|
||||
export const getIntersectionPoint = (p1, p2, y) => {
|
||||
const { x: x1, y: y1 } = p1
|
||||
const { x: x2, y: y2 } = p2
|
||||
|
||||
// 기울기와 y 절편을 이용해 선의 방정식을 구합니다.
|
||||
const slope = (y2 - y1) / (x2 - x1)
|
||||
const intercept = y1 - slope * x1
|
||||
|
||||
// y = 150일 때의 x 좌표를 구합니다.
|
||||
const x = (y - intercept) / slope
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
@ -2704,3 +2704,314 @@ export const inPolygon = (polygonPoints, rectPoints) => {
|
||||
|
||||
return allPointsInsidePolygon && noPolygonPointsInsideRect
|
||||
}
|
||||
|
||||
/**
|
||||
* poolygon의 방향에 따라 화살표를 추가한다.
|
||||
* @param polygon
|
||||
*/
|
||||
export const drawDirectionArrow = (polygon) => {
|
||||
const direction = polygon.direction
|
||||
if (!direction) {
|
||||
return
|
||||
}
|
||||
|
||||
polygon.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.name === 'directionText' && obj.parent === polygon.arrow)
|
||||
.forEach((obj) => polygon.canvas.remove(obj))
|
||||
|
||||
let arrow = null
|
||||
let points = []
|
||||
|
||||
if (polygon.arrow) {
|
||||
polygon.canvas.remove(polygon.arrow)
|
||||
}
|
||||
|
||||
let centerPoint = { x: polygon.width / 2 + polygon.left, y: polygon.height / 2 + polygon.top }
|
||||
let stickeyPoint
|
||||
|
||||
const polygonMaxX = Math.max(...polygon.getCurrentPoints().map((point) => point.x))
|
||||
const polygonMinX = Math.min(...polygon.getCurrentPoints().map((point) => point.x))
|
||||
const polygonMaxY = Math.max(...polygon.getCurrentPoints().map((point) => point.y))
|
||||
const polygonMinY = Math.min(...polygon.getCurrentPoints().map((point) => point.y))
|
||||
|
||||
switch (direction) {
|
||||
case 'north':
|
||||
points = [
|
||||
{ x: centerPoint.x, y: polygonMinY - 20 },
|
||||
{ x: centerPoint.x + 20, y: polygonMinY - 20 },
|
||||
{ x: centerPoint.x + 20, y: polygonMinY - 50 },
|
||||
{ x: centerPoint.x + 50, y: polygonMinY - 50 },
|
||||
{ x: centerPoint.x, y: polygonMinY - 80 },
|
||||
{ x: centerPoint.x - 50, y: polygonMinY - 50 },
|
||||
{ x: centerPoint.x - 20, y: polygonMinY - 50 },
|
||||
{ x: centerPoint.x - 20, y: polygonMinY - 20 },
|
||||
]
|
||||
|
||||
stickeyPoint = { x: centerPoint.x, y: polygonMinY - 80 }
|
||||
break
|
||||
case 'south':
|
||||
points = [
|
||||
{ x: centerPoint.x, y: polygonMaxY + 20 },
|
||||
{ x: centerPoint.x + 20, y: polygonMaxY + 20 },
|
||||
{ x: centerPoint.x + 20, y: polygonMaxY + 50 },
|
||||
{ x: centerPoint.x + 50, y: polygonMaxY + 50 },
|
||||
{ x: centerPoint.x, y: polygonMaxY + 80 },
|
||||
{ x: centerPoint.x - 50, y: polygonMaxY + 50 },
|
||||
{ x: centerPoint.x - 20, y: polygonMaxY + 50 },
|
||||
{ x: centerPoint.x - 20, y: polygonMaxY + 20 },
|
||||
]
|
||||
stickeyPoint = { x: centerPoint.x, y: polygonMaxY + 80 }
|
||||
break
|
||||
case 'west':
|
||||
points = [
|
||||
{ x: polygonMinX - 20, y: centerPoint.y },
|
||||
{ x: polygonMinX - 20, y: centerPoint.y + 20 },
|
||||
{ x: polygonMinX - 50, y: centerPoint.y + 20 },
|
||||
{ x: polygonMinX - 50, y: centerPoint.y + 50 },
|
||||
{ x: polygonMinX - 80, y: centerPoint.y },
|
||||
{ x: polygonMinX - 50, y: centerPoint.y - 50 },
|
||||
{ x: polygonMinX - 50, y: centerPoint.y - 20 },
|
||||
{ x: polygonMinX - 20, y: centerPoint.y - 20 },
|
||||
]
|
||||
|
||||
stickeyPoint = { x: polygonMinX - 80, y: centerPoint.y }
|
||||
break
|
||||
case 'east':
|
||||
points = [
|
||||
{ x: polygonMaxX + 20, y: centerPoint.y },
|
||||
{ x: polygonMaxX + 20, y: centerPoint.y + 20 },
|
||||
{ x: polygonMaxX + 50, y: centerPoint.y + 20 },
|
||||
{ x: polygonMaxX + 50, y: centerPoint.y + 50 },
|
||||
{ x: polygonMaxX + 80, y: centerPoint.y },
|
||||
{ x: polygonMaxX + 50, y: centerPoint.y - 50 },
|
||||
{ x: polygonMaxX + 50, y: centerPoint.y - 20 },
|
||||
{ x: polygonMaxX + 20, y: centerPoint.y - 20 },
|
||||
]
|
||||
|
||||
stickeyPoint = { x: polygonMaxX + 80, y: centerPoint.y }
|
||||
break
|
||||
}
|
||||
|
||||
arrow = new fabric.Polygon(points, {
|
||||
selectable: false,
|
||||
name: 'arrow',
|
||||
fill: 'transparent',
|
||||
stroke: 'black',
|
||||
direction: direction,
|
||||
parent: polygon,
|
||||
stickeyPoint: stickeyPoint,
|
||||
})
|
||||
|
||||
polygon.arrow = arrow
|
||||
polygon.canvas.add(arrow)
|
||||
polygon.canvas.renderAll()
|
||||
drawDirectionStringToArrow(polygon.canvas, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 방향을 나타낸 화살표에 각도에 따라 글씨 추가
|
||||
* @param canvas
|
||||
* @param compass
|
||||
*/
|
||||
export const drawDirectionStringToArrow = (canvas, compass = 0) => {
|
||||
const arrows = canvas?.getObjects().filter((obj) => obj.name === 'arrow')
|
||||
|
||||
if (arrows.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const eastArrows = arrows.filter((arrow) => arrow.direction === 'east')
|
||||
const westArrows = arrows.filter((arrow) => arrow.direction === 'west')
|
||||
const northArrows = arrows.filter((arrow) => arrow.direction === 'north')
|
||||
const southArrows = arrows.filter((arrow) => arrow.direction === 'south')
|
||||
|
||||
let southText = '南'
|
||||
let eastText = '東'
|
||||
let westText = '西'
|
||||
let northText = '北'
|
||||
|
||||
if (compass === 0 || compass === 360) {
|
||||
// 남,동,서 가능
|
||||
// 그대로
|
||||
} else if (compass < 45) {
|
||||
//남(남남동),동(동북동),서(서남서) 가능
|
||||
//북(북북서)
|
||||
southText = '南南東'
|
||||
eastText = '東北東'
|
||||
westText = '西南西'
|
||||
northText = '北北西'
|
||||
} else if (compass === 45) {
|
||||
// 남, 서 가능
|
||||
// 남(남동)
|
||||
// 서(남서)
|
||||
// 북(북서)
|
||||
// 동(북동)
|
||||
southText = '南東'
|
||||
westText = '南西'
|
||||
northText = '北西'
|
||||
eastText = '北東'
|
||||
} else if (compass < 90) {
|
||||
// 북(서북서)
|
||||
// 동 (북북동)
|
||||
// 남(동남동)
|
||||
// 서(남남서)
|
||||
northText = '北西北'
|
||||
eastText = '北北東'
|
||||
southText = '東南東'
|
||||
westText = '南南西'
|
||||
} else if (compass === 90) {
|
||||
// 동(북)
|
||||
// 서(남)
|
||||
// 남(동)
|
||||
// 북(서)
|
||||
eastText = '北'
|
||||
westText = '南'
|
||||
southText = '東'
|
||||
northText = '西'
|
||||
} else if (compass < 135) {
|
||||
// 남,서,북 가능
|
||||
// 동(북북서)
|
||||
// 서(남남동)
|
||||
// 남(동북동)
|
||||
// 북(서남서)
|
||||
eastText = '北北西'
|
||||
westText = '南南東'
|
||||
southText = '東北東'
|
||||
northText = '西南西'
|
||||
} else if (compass === 135) {
|
||||
// 서,북 가능
|
||||
|
||||
// 서(남동)
|
||||
// 북(남서)
|
||||
// 남(북동)
|
||||
// 동(북서)
|
||||
|
||||
westText = '南東'
|
||||
northText = '南西'
|
||||
southText = '北東'
|
||||
eastText = '北西'
|
||||
} else if (compass < 180) {
|
||||
// 북,동,서 가능
|
||||
// 북(남남서)
|
||||
// 동(서북서)
|
||||
// 남(북북동)
|
||||
// 서(동남동)
|
||||
|
||||
northText = '南南西'
|
||||
eastText = '西北西'
|
||||
southText = '北北東'
|
||||
westText = '東南東'
|
||||
} else if (compass === 180) {
|
||||
// 북,동,서 가능
|
||||
// 북(남)
|
||||
// 동(서)
|
||||
// 남(북)
|
||||
// 서(동)
|
||||
northText = '南'
|
||||
eastText = '西'
|
||||
southText = '北'
|
||||
westText = '東'
|
||||
} else if (compass < 225) {
|
||||
// 서,북,동 가능
|
||||
// 북(남남동)
|
||||
// 동(서남서)
|
||||
// 남(북북서)
|
||||
// 서(동남동)
|
||||
northText = '南南東'
|
||||
eastText = '西南西'
|
||||
southText = '北北西'
|
||||
westText = '東南東'
|
||||
} else if (compass === 225) {
|
||||
// 북,동 가능
|
||||
// 북(남동)
|
||||
// 동(남서)
|
||||
// 남(북서)
|
||||
// 서(북동)
|
||||
northText = '南東'
|
||||
eastText = '南西'
|
||||
southText = '北西'
|
||||
westText = '北東'
|
||||
} else if (compass < 270) {
|
||||
// 북동남 가능
|
||||
// 북(동남동)
|
||||
// 동(남남서)
|
||||
// 남(서북서)
|
||||
// 서(북북동)
|
||||
northText = '東南東'
|
||||
eastText = '南南西'
|
||||
southText = '西北西'
|
||||
westText = '北北東'
|
||||
} else if (compass === 270) {
|
||||
// 북동남 가능
|
||||
// 북(동)
|
||||
// 동(남)
|
||||
// 남(서)
|
||||
// 서(북)
|
||||
northText = '東'
|
||||
eastText = '南'
|
||||
southText = '西'
|
||||
westText = '北'
|
||||
} else if (compass < 315) {
|
||||
// 북,동,남 가능
|
||||
// 북(동북동)
|
||||
// 동(남남동)
|
||||
// 남(서남서)
|
||||
// 서(북북서)
|
||||
northText = '東北東'
|
||||
eastText = '南南東'
|
||||
southText = '西南西'
|
||||
westText = '北北西'
|
||||
} else if (compass === 315) {
|
||||
// 동,남 가능
|
||||
// 북(북동)
|
||||
// 동(남동)
|
||||
// 남(남서)
|
||||
// 서(북서)
|
||||
northText = '北東'
|
||||
eastText = '南東'
|
||||
southText = '南西'
|
||||
westText = '北西'
|
||||
} else if (compass < 360) {
|
||||
// 남,동,서 가능
|
||||
// 북(북북동)
|
||||
// 동(동남동)
|
||||
// 남(남남서)
|
||||
// 서(서북서)
|
||||
northText = '北北東'
|
||||
eastText = '東南東'
|
||||
southText = '南南西'
|
||||
westText = '西北西'
|
||||
}
|
||||
|
||||
clearDirectionText(canvas)
|
||||
|
||||
addTextByArrows(eastArrows, eastText, canvas)
|
||||
addTextByArrows(westArrows, westText, canvas)
|
||||
addTextByArrows(northArrows, northText, canvas)
|
||||
addTextByArrows(southArrows, southText, canvas)
|
||||
}
|
||||
|
||||
const clearDirectionText = (canvas) => {
|
||||
const texts = canvas.getObjects().filter((obj) => obj.name === 'directionText')
|
||||
texts.forEach((text) => {
|
||||
canvas.remove(text)
|
||||
})
|
||||
}
|
||||
|
||||
const addTextByArrows = (arrows, txt, canvas) => {
|
||||
arrows.forEach((arrow, index) => {
|
||||
const text = new fabric.Text(`${txt}${index + 1}`, {
|
||||
fontSize: arrow.parent.fontSize,
|
||||
fill: 'black',
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
name: 'directionText',
|
||||
selectable: false,
|
||||
left: arrow.stickeyPoint.x,
|
||||
top: arrow.stickeyPoint.y,
|
||||
parent: arrow,
|
||||
})
|
||||
canvas.add(text)
|
||||
})
|
||||
}
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@ -4017,6 +4017,15 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.6.2:
|
||||
version "1.7.7"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
|
||||
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axios@^1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz"
|
||||
@ -4252,6 +4261,13 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"
|
||||
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
|
||||
|
||||
convertapi@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/convertapi/-/convertapi-1.14.0.tgz#a291a98cb986ae1e0f2340a130adbe17f65c8c76"
|
||||
integrity sha512-9Rzkn+Mjs4jVLQ5pRUC8KpIjnT9WFUkuJZ5yjCJaDJsDM7Na2lWPKtDJdkfKcYCNDuo1h+OYZedne5SLp60EkQ==
|
||||
dependencies:
|
||||
axios "^1.6.2"
|
||||
|
||||
cookie@0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
|
||||
@ -5499,6 +5515,11 @@ react-dom@^18:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-icons@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c"
|
||||
integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user