This commit is contained in:
yoosangwook 2024-09-02 14:05:46 +09:00
commit 0d189fdc2c
8 changed files with 449 additions and 24 deletions

View File

@ -36,6 +36,7 @@
"postcss": "^8",
"prettier": "^3.3.3",
"prisma": "^5.18.0",
"react-color-palette": "^7.2.2",
"sass": "^1.77.8",
"tailwindcss": "^3.4.1"
}

View File

@ -0,0 +1,23 @@
<?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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 179 B

View File

@ -4,8 +4,10 @@ import { useRecoilState, useRecoilValue } from 'recoil'
import { modalContent, modalState } from '@/store/modalAtom'
import { guideLineState, horiGuideLinesState, vertGuideLinesState } from '@/store/canvasAtom'
import { fabric } from 'fabric'
import { ColorPicker, useColor } from 'react-color-palette'
import 'react-color-palette/css'
export default function SettingsModal(props) {
export default function GridSettingsModal(props) {
const { canvasProps } = props
const [isCustomGridSetting, setIsCustomGridSetting] = useState(true)
const [gridCheckedValue, setGridCheckValue] = useState([])
@ -21,6 +23,15 @@ export default function SettingsModal(props) {
const gridSettingArray = []
const [guideColor, setGuideColor] = useColor('rgb(200, 15, 15)')
const [colorPickerShow, setColorPickerShow] = useState(false)
const boxStyle = {
width: '50px',
height: '30px',
border: '1px solid black',
backgroundColor: guideColor.hex,
}
useEffect(() => {
moduleLength.current.value = 90
customModuleHoriLength.current.value = 90
@ -56,7 +67,7 @@ export default function SettingsModal(props) {
const horizontalLine = new fabric.Line(
[0, i * moduleVertLength - moduleVertLength / 2, canvasProps.width, i * moduleVertLength - moduleVertLength / 2],
{
stroke: 'gray',
stroke: guideColor.hex,
strokeWidth: 1,
selectable: true,
lockMovementX: true,
@ -65,6 +76,8 @@ export default function SettingsModal(props) {
lockScalingX: true,
lockScalingY: true,
name: 'guideLine',
strokeDashArray: [5, 2],
opacity: 0.3,
direction: 'horizontal',
},
)
@ -76,7 +89,7 @@ export default function SettingsModal(props) {
const verticalLine = new fabric.Line(
[i * moduleHoriLength - moduleHoriLength / 2, 0, i * moduleHoriLength - moduleHoriLength / 2, canvasProps.height],
{
stroke: 'gray',
stroke: guideColor.hex,
strokeWidth: 1,
selectable: true,
lockMovementX: true,
@ -85,6 +98,8 @@ export default function SettingsModal(props) {
lockScalingX: true,
lockScalingY: true,
name: 'guideLine',
strokeDashArray: [5, 2],
opacity: 0.3,
direction: 'vertical',
},
)
@ -118,7 +133,9 @@ export default function SettingsModal(props) {
if (gridCheckedValue.includes('dot')) {
const circle = new fabric.Circle({
radius: 2,
fill: 'red',
fill: 'white',
stroke: guideColor.hex,
strokeWidth: 0.7,
originX: 'center',
originY: 'center',
selectable: false,
@ -216,11 +233,19 @@ export default function SettingsModal(props) {
<Radio value="custom">임의간격</Radio>
</RadioGroup>
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"></div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
가이드컬러 <div style={boxStyle} onClick={() => setColorPickerShow(!colorPickerShow)}></div>
</div>
{colorPickerShow && (
<ColorPicker color={guideColor} onChange={setGuideColor} hideInput={['hsv', 'rgb', 'hex']} height={100} hideAlpha={true} />
)}
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Checkbox value="linked" isDisabled={isCustomGridSetting}>
종횡연동
</Checkbox>
</div>
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="가로간격" ref={customModuleHoriLength} min={0} isDisabled={isCustomGridSetting} />
mm
@ -229,6 +254,7 @@ export default function SettingsModal(props) {
<Input type="number" label="세로간격" ref={customModuleVertLength} min={0} isDisabled={isCustomGridSetting} />
mm
</div>
<div className="flex gap-4 items-center">
<Button size="sm">초기화</Button>
<Button size="sm" color="secondary" onClick={drawGridSettings} isDisabled={gridCheckedValue.length === 0}>

View File

@ -0,0 +1,189 @@
import { useEffect, useState, memo, useCallback } from 'react'
import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input, Select, SelectItem } from '@nextui-org/react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { modalContent, modalState } from '@/store/modalAtom'
import { canvasSettingState } from '@/store/canvasAtom'
import { useAxios } from '@/hooks/useAxios'
export default function InitSettingsModal(props) {
const [open, setOpen] = useRecoilState(modalState)
const [canvasSetting, setCanvasSetting] = useRecoilState(canvasSettingState)
const [roofMaterials, setRoofMaterials] = useState([])
const [basicSetting, setBasicSettings] = useState({
type: '1',
inputType: '1',
angleType: 'slope',
roofs: [],
})
const { get, post } = useAxios()
useEffect(() => {
get({ url: '/api/roof-material/roof-material-infos' }).then((res) => {
//TODO: error handling
if (!res) return
setRoofMaterials(res)
})
if (!(Object.keys(canvasSetting).length === 0 && canvasSetting.constructor === Object)) {
setBasicSettings({ ...canvasSetting })
}
}, [])
//
const handleBasicSetting = (event) => {
const newBasicSetting = { ...basicSetting, [event.target.name]: event.target.value }
setBasicSettings(newBasicSetting)
}
//
const addRoofSetting = () => {
if (basicSetting.roofs.length === 4) {
alert('지붕재는 최대 4종까지 선택할 수 있습니다.')
return
}
//
const newRoofSettings = {
id: basicSetting.roofs.length + 1,
roofId: '3',
width: '200',
height: '200',
gap: '0',
layout: 'parallel',
}
setBasicSettings((prevState) => ({
...prevState,
roofs: [...prevState.roofs, newRoofSettings],
}))
}
//
const handleRoofSettings = (id, event) => {
const roof = basicSetting.roofs.map((roof, i) => (id === roof.id ? { ...roof, [event.target.name]: event.target.value } : roof))
setBasicSettings((prevState) => ({
...prevState,
roofs: [...roof],
}))
}
const submitCanvasConfig = () => {
post({ url: '/api/canvas-config', data: basicSetting }).then((res) => {
if (!res) {
setCanvasSetting({ ...basicSetting })
}
})
}
return (
<>
<div className="container mx-auto mt-10 p-6 bg-white shadow-lg rounded-lg">
<div className="text-lg font-semibold mb-4">배치면 초기설정</div>
<div className="mb-6">
<div className="flex space-x-4">
<RadioGroup label="도면 작성방법" name="type" orientation="horizontal" value={basicSetting.type} onChange={handleBasicSetting}>
<Radio value="1">치수 입력에 의한 물건작성</Radio>
</RadioGroup>
</div>
</div>
<div className="mb-6">
<div className="flex space-x-4">
<RadioGroup label="치수 입력방법" name="inputType" orientation="horizontal" value={basicSetting.inputType} onChange={handleBasicSetting}>
<Radio value="1">복사도 입력</Radio>
<Radio value="2">실측값 입력</Radio>
<Radio value="3">육지붕</Radio>
</RadioGroup>
</div>
</div>
<div className="mb-6">
<div className="flex space-x-4">
<RadioGroup label="지붕각도 설정" name="angleType" orientation="horizontal" value={basicSetting.angleType} onChange={handleBasicSetting}>
<Radio value="slope">경사</Radio>
<Radio value="angle">각도</Radio>
</RadioGroup>
</div>
</div>
<div className="flex items-center mb-4">
<button className="px-3 py-1 bg-blue-500 text-white rounded mr-3" onClick={addRoofSetting}>
Add
</button>
<span className="text-sm text-gray-500"> 지붕재는 최대 4종까지 선택할 있습니다.</span>
</div>
{basicSetting.roofs &&
basicSetting.roofs.map((roof, index) => {
return <RoofSelectBox roofMaterials={roofMaterials} roof={roof} key={index} onChange={handleRoofSettings} />
})}
<div className="flex gap-4 items-right">
<Button size="sm" color="secondary" onClick={submitCanvasConfig}>
저장
</Button>
<Button size="sm" onClick={() => setOpen(!open)}>
취소
</Button>
</div>
</div>
</>
)
}
const RoofSelectBox = (props) => {
return (
<div className="mb-4 flex flex-wrap items-center space-x-4" style={{ border: '1px solid black' }}>
<Select
aria-label="roofMaterial"
className={'w-52'}
name="roofId"
onChange={(e) => props.onChange(props.roof.id, e)}
items={props.roofMaterials}
defaultSelectedKeys={props.roof.roofId ? props.roof.roofId : ''}
selectedKeys={props.roof.roofId}
value={props.roof.roofId}
>
{(roofMaterial) => (
<SelectItem key={roofMaterial.id} value={roofMaterial.id}>
{roofMaterial.name}
</SelectItem>
)}
</Select>
<Input
type="text"
name="width"
placeholder="너비"
value={props.roof.width}
className="w-24"
onChange={(e) => props.onChange(props.roof.id, e)}
/>
<Input
type="text"
name="height"
placeholder="높이"
value={props.roof.height}
className="w-24"
onChange={(e) => props.onChange(props.roof.id, e)}
/>
mm
<Input type="text" name="gap" placeholder="간격" value={props.roof.gap} className="w-24" onChange={(e) => props.onChange(props.roof.id, e)} />
mm
<div className="flex space-x-4">
<RadioGroup
orientation="horizontal"
name="layout"
value={props.roof.layout}
defaultValue="parallel"
onChange={(e) => props.onChange(props.roof.id, e)}
>
<Radio value="parallel">병렬식</Radio>
<Radio value="cascade">계단식</Radio>
</RadioGroup>
</div>
</div>
)
}

View File

@ -26,12 +26,14 @@ import { QPolygon } from '@/components/fabric/QPolygon'
import ThumbnailList from './ui/ThumbnailLIst'
import QContextMenu from './common/context-menu/QContextMenu'
import { modalContent, modalState } from '@/store/modalAtom'
import SettingsModal from './SettingsModal'
import { useAxios } from '@/hooks/useAxios'
import QPolygonContextMenu from '@/components/common/context-menu/QPolygonContextMenu'
import QLineContextMenu from '@/components/common/context-menu/QLineContextMenu'
import QEmptyContextMenu from '@/components/common/context-menu/QEmptyContextMenu'
import { radiansToDegrees } from '@turf/turf'
import { degreesToRadians, radiansToDegrees } from '@turf/turf'
import InitSettingsModal from './InitSettingsModal'
import GridSettingsModal from './GridSettingsModal'
export default function Roof2(props) {
const { name, userId, email, isLoggedIn } = props
@ -105,6 +107,8 @@ export default function Roof2(props) {
useEffect(() => {
get({ url: `/api/canvas-management/canvas-statuses/by-object/test123240822001` }).then((res) => {
console.log(res)
const arrangeData = res.map((item) => {
console.log(item.canvasStatus.replace(/##/g, '"').replace(/\\/g, ''))
const test = item.canvasStatus.replace(/##/g, '"').replace(/\\/g, '')
@ -622,8 +626,6 @@ export default function Roof2(props) {
const angle = Math.atan(t2 / t)
const angle2 = Math.atan(t3 / (a - b - t))
console.log(angle, angle2)
let isDrawing = true
let pentagon
canvas?.on('mouse:move', (e) => {
@ -1466,12 +1468,15 @@ export default function Roof2(props) {
})
}
const createTemplate26 = () => {
//todo
const length1 = 300 //Number(prompt('1'))
const length1 = 500 //Number(prompt('1'))
const length2 = 200 //Number(prompt('2'))
const length3 = 300 //Number(prompt('3'))
const length4 = 400 //Number(prompt('3'))
const angle = (Math.asin(length3 / length4) * 180) / Math.PI //
const topL = (length1 - length2) / 2 / Math.cos((angle * Math.PI) / 180) //
let isDrawing = true
canvas?.on('mouse:move', (e) => {
if (!isDrawing) {
@ -1479,15 +1484,96 @@ export default function Roof2(props) {
}
canvas?.remove(...canvas?.getObjects().filter((obj) => obj.name === 'guideTriangle'))
const pointer = canvas?.getPointer(e.e)
const triangle = new QPolygon([], {
const triangle = new QPolygon(
[
{
x: pointer.x - length1 / 2 + length1,
y: pointer.y + length3 / 2,
},
{
x: pointer.x - length1 / 2,
y: pointer.y + length3 / 2,
},
{
x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)),
y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)),
},
{
x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)) + length2,
y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)),
},
{
x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)) + length2 + topL * Math.cos(degreesToRadians(angle)),
y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)) + topL * Math.sin(degreesToRadians(angle)),
},
],
{
fill: 'transparent',
stroke: 'black',
strokeWidth: 2,
selectable: true,
fontSize: fontSize,
name: 'guideTriangle',
},
)
canvas?.add(triangle)
})
canvas?.on('mouse:down', (e) => {
isDrawing = false
})
}
const createTemplate27 = () => {
const length1 = 500 //Number(prompt('1'))
const length2 = 200 //Number(prompt('2'))
const length3 = 300 //Number(prompt('3'))
const length4 = 400 //Number(prompt('3'))
const angle = (Math.asin(length3 / length4) * 180) / Math.PI //
const topL = (length1 - length2) / 2 / Math.cos((angle * Math.PI) / 180) //
let isDrawing = true
canvas?.on('mouse:move', (e) => {
if (!isDrawing) {
return
}
canvas?.remove(...canvas?.getObjects().filter((obj) => obj.name === 'guideTriangle'))
const pointer = canvas?.getPointer(e.e)
const triangle = new QPolygon(
[
{
x: pointer.x - length1 / 2,
y: pointer.y + length3 / 2,
},
{
x: pointer.x - length1 / 2 + length1,
y: pointer.y + length3 / 2,
},
{
x: pointer.x - length1 / 2 + length1 - length4 * Math.cos(degreesToRadians(angle)),
y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)),
},
{
x: pointer.x - length1 / 2 + length1 - length4 * Math.cos(degreesToRadians(angle)) - length2,
y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)),
},
{
x: pointer.x - length1 / 2 + length1 - length4 * Math.cos(degreesToRadians(angle)) - length2 - topL * Math.cos(degreesToRadians(angle)),
y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)) + topL * Math.sin(degreesToRadians(angle)),
},
],
{
fill: 'transparent',
stroke: 'black',
strokeWidth: 2,
selectable: true,
fontSize: fontSize,
name: 'guideTriangle',
},
)
canvas?.add(triangle)
})
@ -1566,6 +1652,74 @@ export default function Roof2(props) {
canvas?.renderAll()
})
}
const createTemplate29 = () => {
const length1 = Number(prompt('밑변'))
const length2 = Number(prompt('높이'))
const length3 = Number(prompt('빗변'))
const a = Math.sqrt(length3 * length3 - length2 * length2) //
const sinA = a / length3
const angleInRadians = Math.asin(sinA)
const angleInDegrees = angleInRadians * (180 / Math.PI)
const b = a - length1 / 2
const c = b / Math.tan(angleInRadians)
const d = Math.sqrt(b * b + c * c)
if (isNaN(a)) {
alert('값이 잘못되었습니다.')
return
}
if (b < 0) {
alert('값이 잘못되었습니다.')
return
}
if (angleInDegrees === 0 || angleInRadians === 0) {
alert('값이 잘못되었습니다.')
return
}
let isDrawing = true
let pentagon
canvas?.on('mouse:move', (e) => {
if (!isDrawing) {
return
}
canvas?.remove(...canvas?.getObjects().filter((obj) => obj.name === 'guideTriangle'))
const pointer = canvas?.getPointer(e.e)
const newAngleInRadians = (90 - angleInDegrees) * (Math.PI / 180)
pentagon = new QPolygon(
[
{
x: pointer.x + length1 / 2 - b / 2 - length3 * Math.cos(newAngleInRadians),
y: pointer.y + length2 / 2 - length3 * Math.sin(newAngleInRadians),
},
{ x: pointer.x + length1 / 2 - b / 2, y: pointer.y + length2 / 2 },
{ x: pointer.x - (length1 + b) / 2, y: pointer.y + length2 / 2 },
{ x: pointer.x - (length1 + b) / 2 - d * Math.cos(newAngleInRadians), y: pointer.y + length2 / 2 - d * Math.sin(newAngleInRadians) },
],
{
fill: 'transparent',
stroke: 'gray',
strokeWidth: 1,
selectable: true,
fontSize: fontSize,
name: 'guideTriangle',
},
)
canvas?.add(pentagon)
})
canvas?.on('mouse:down', (e) => {
isDrawing = false
pentagon.set('name', 'roof')
pentagon.set('stroke', 2)
canvas?.renderAll()
})
}
return (
<>
@ -1575,7 +1729,24 @@ export default function Roof2(props) {
<Button
className="m-1 p-2"
onClick={() => {
setContent(<SettingsModal canvasProps={canvas} />)
svgLoad()
}}
>
svg로딩
</Button>
<Button
className="m-1 p-2"
onClick={() => {
setContent(<InitSettingsModal canvasProps={canvas} />)
setOpen(true)
}}
>
배치면 초기설정
</Button>
<Button
className="m-1 p-2"
onClick={() => {
setContent(<GridSettingsModal canvasProps={canvas} />)
setOpen(true)
}}
>
@ -1779,9 +1950,15 @@ export default function Roof2(props) {
<Button className="m-1 p-2" onClick={createTemplate26}>
26 추가
</Button>
<Button className="m-1 p-2" onClick={createTemplate27}>
27 추가
</Button>
<Button className="m-1 p-2" onClick={createTemplate28}>
28 추가
</Button>
<Button className="m-1 p-2" onClick={createTemplate29}>
29 추가
</Button>
<Button className="m-1 p-2" onClick={createPentagon2}>
오각형 추가2
</Button>

View File

@ -19,7 +19,7 @@ function ThumbnailList(props) {
<div className="flex justify-center m-4 w-full">
{thumbnails.length > 0 &&
thumbnails.map((thumbnail, index) => (
<Card isFooterBlurred radius="lg" className="border-none m-2">
<Card isFooterBlurred radius="lg" key={index} className="border-none m-2">
<Image
alt="Woman listing to music"
className="object-cover"

View File

@ -105,5 +105,10 @@ export const horiGuideLinesState = atom({
export const vertGuideLinesState = atom({
key: 'vertGuideLines',
default: [],
})
export const canvasSettingState = atom({
key: 'canvasSetting',
default: {},
dangerouslyAllowMutability: true,
})