diff --git a/src/common/common.js b/src/common/common.js index d82d43f0..abda5acd 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -203,6 +203,7 @@ export const SAVE_KEY = [ 'fontWeight', 'dormerAttributes', 'toFixed', + 'isSortedPoints', ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 396ae6af..a471b5be 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -45,8 +45,11 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { options.sort = options.sort ?? true options.parentId = options.parentId ?? null + this.isSortedPoints = false + if (!options.sort && points.length <= 8) { points = sortedPointLessEightPoint(points) + this.isSortedPoints = true } else { let isDiagonal = false points.forEach((point, i) => { @@ -62,6 +65,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { if (!isDiagonal) { points = sortedPoints(points) + this.isSortedPoints = true } } diff --git a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index 618a65d2..a836044e 100644 --- a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx @@ -479,7 +479,7 @@ export default function CircuitTrestleSetting({ id }) { console.log(stepUpListData) stepUpListData[0].pcsItemList.map((item, index) => { return item.serQtyList - .filter((serQty) => serQty.selected) + .filter((serQty) => serQty.selected && serQty.paralQty > 0) .forEach((serQty) => { pcs.push({ pcsMkrCd: item.pcsMkrCd, diff --git a/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx b/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx index d776476b..0e8ad7b6 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx @@ -573,7 +573,7 @@ export default function StepUp(props) { value={seletedMainOption} sourceKey="code" targetKey="code" - showKey="name" + showKey={`${globalLocale === 'ja' ? 'nameJp' : 'name'}`} onChange={(e) => setSeletedMainOption(e)} /> )} @@ -586,7 +586,7 @@ export default function StepUp(props) { value={seletedSubOption} sourceKey="code" targetKey="code" - showKey="name" + showKey={`${globalLocale === 'ja' ? 'nameJp' : 'name'}`} onChange={(e) => setSeletedSubOption(e)} /> )} diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 25ca72b8..e2e499b6 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -19,6 +19,7 @@ import { getChonByDegree, getDegreeByChon } from '@/util/canvas-util' import { usePolygon } from '@/hooks/usePolygon' import { canvasState } from '@/store/canvasAtom' import { useRoofFn } from '@/hooks/common/useRoofFn' +import { usePlan } from '@/hooks/usePlan' /** * 지붕 레이아웃 @@ -45,6 +46,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla const { setSurfaceShapePattern } = useRoofFn() const canvas = useRecoilValue(canvasState) const roofDisplay = useRecoilValue(roofDisplaySelector) + const { saveCanvas } = usePlan() const roofRef = { roofCd: useRef(null), @@ -205,7 +207,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla /** * 배치면초기설정 저장 버튼 클릭 */ - const handleSaveBtn = () => { + const handleSaveBtn = async () => { const roofInfo = { ...currentRoof, planNo: basicSetting.planNo, @@ -254,6 +256,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla /* 저장 후 화면 닫기 */ closePopup(id) + await saveCanvas(false) } return ( diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index 48ad5913..a584876c 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -10,6 +10,7 @@ import { useSwal } from '@/hooks/useSwal' import { useContext } from 'react' import { QcastContext } from '@/app/QcastProvider' import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle' +import { useMessage } from '@/hooks/useMessage' // 모듈간 같은 행, 열의 마진이 10 이하인 경우는 같은 행, 열로 간주 const MODULE_MARGIN = 10 @@ -26,6 +27,7 @@ export const useTrestle = () => { const { getSelectedPcsItemList } = useCircuitTrestle() const { resetCircuits } = useCircuitTrestle() + const { getMessage } = useMessage() const apply = () => { const notAllocationModules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && !obj.circuit) @@ -58,7 +60,6 @@ export const useTrestle = () => { } const construction = moduleSelectionData?.roofConstructions?.find((construction) => construction.roofIndex === roofMaterialIndex).construction if (!construction) { - swalFire({ text: 'construction 존재안함', icon: 'error' }) return } @@ -131,9 +132,9 @@ export const useTrestle = () => { surface.isChidory = isChidory if (plvrYn === 'N' && isChidory) { - swalFire({ text: '치조불가공법입니다.', icon: 'error' }) + swalFire({ text: getMessage('chidory.can.not.install'), icon: 'error' }) clear() - throw new Error('치조불가공법입니다.') + throw new Error(getMessage('chidory.can.not.install')) } surface.set({ isChidory: isChidory }) diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index cfc73b81..2b9c9494 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -27,6 +27,7 @@ import { moduleSelectionDataState } from '@/store/selectedModuleOptions' import { useCanvasPopupStatusController } from '@/hooks/common/useCanvasPopupStatusController' import { outerLinePointsState } from '@/store/outerLineAtom' import { QcastContext } from '@/app/QcastProvider' +import { usePlan } from '@/hooks/usePlan' export function useRoofAllocationSetting(id) { const canvas = useRecoilValue(canvasState) @@ -52,6 +53,7 @@ export function useRoofAllocationSetting(id) { const { swalFire } = useSwal() const { setIsGlobalLoading } = useContext(QcastContext) const { setSurfaceShapePattern } = useRoofFn() + const { saveCanvas } = usePlan() const [moduleSelectionData, setModuleSelectionData] = useRecoilState(moduleSelectionDataState) const resetPoints = useResetRecoilState(outerLinePointsState) @@ -225,6 +227,7 @@ export function useRoofAllocationSetting(id) { await post({ url: `/api/canvas-management/roof-allocation-settings`, data: patternData }).then((res) => { swalFire({ text: getMessage(res.returnMessage) }) setIsGlobalLoading(false) + saveCanvas(false) }) //Recoil 설정 diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index 296632a7..75da1422 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -159,6 +159,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { addCanvasMouseEventListener('mouse:down', (e) => { isDrawing = false + const { xInversion, yInversion } = surfaceRefs canvas?.remove(obj) //각도 추가 @@ -179,6 +180,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { } //회전, flip등이 먹은 기준으로 새로생성 + // const batchSurface = addPolygon(reorderedPoints, { const batchSurface = addPolygon(obj.getCurrentPoints(), { fill: 'transparent', stroke: 'red', @@ -197,6 +199,8 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { pitch: globalPitch, surfaceId: surfaceId, direction: direction, + isXInversion: xInversion, + isYInversion: yInversion, }) canvas.setActiveObject(batchSurface) setSurfaceShapePattern(batchSurface, roofDisplay.column) @@ -208,6 +212,9 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { // const popupId = uuidv4() // addPopup(popupId, 2, ) + // console.log('xInversion', xInversion) //상하반전 + // console.log('yInversion', yInversion) //좌우반전 + changeSurfaceLineType(batchSurface) if (setIsHidden) setIsHidden(false) @@ -491,18 +498,18 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { } case 10: { points = [ - { x: pointer.x + length1 / 2, y: pointer.y + length4 / 2 }, - { x: pointer.x + length1 / 2 - length1, y: pointer.y + length4 / 2 }, { x: pointer.x + length1 / 2 - length1, y: pointer.y + length4 / 2 - length5 }, - { x: pointer.x + length1 / 2 - length1 + length2, y: pointer.y + length4 / 2 - length5 }, - { - x: pointer.x + length1 / 2 - length1 + length2, - y: pointer.y + length4 / 2 - length5 - (length4 - length5), - }, + { x: pointer.x + length1 / 2 - length1, y: pointer.y + length4 / 2 }, + { x: pointer.x + length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x + length1 / 2 - length1 + length2 + length3, y: pointer.y + length4 / 2 - length5 - (length4 - length5), }, + { + x: pointer.x + length1 / 2 - length1 + length2, + y: pointer.y + length4 / 2 - length5 - (length4 - length5), + }, + { x: pointer.x + length1 / 2 - length1 + length2, y: pointer.y + length4 / 2 - length5 }, ] break } @@ -616,27 +623,27 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { } case 14: { points = [ - { x: pointer.x - length1 / 2 + length2, y: pointer.y + length4 / 2 }, - { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 - length4 }, - { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 }, - { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 + length4 }, - { x: pointer.x - length1 / 2 + length1 - length3, y: pointer.y + length4 / 2 - length4 + length4 }, + { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 }, + { x: pointer.x - length1 / 2 + length2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length2 + (length1 - length2 - length3) / 2, y: pointer.y + length4 / 2 - length4 + length5, }, + { x: pointer.x - length1 / 2 + length1 - length3, y: pointer.y + length4 / 2 - length4 + length4 }, + { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 + length4 }, + { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 }, ] break } case 15: { points = [ - { x: pointer.x - length1 / 2, y: pointer.y + length2 - length2 / 2 }, { x: pointer.x - length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 }, - { x: pointer.x, y: pointer.y + length2 - length2 / 2 - length3 - (length2 - length3) }, - { x: pointer.x + length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 }, + { x: pointer.x - length1 / 2, y: pointer.y + length2 - length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 + length3 }, + { x: pointer.x + length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 }, + { x: pointer.x, y: pointer.y + length2 - length2 / 2 - length3 - (length2 - length3) }, ] break } @@ -644,28 +651,28 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { case 16: { points = [ { - x: pointer.x - length1 / 2, - y: pointer.y + length3 / 2, + x: pointer.x - length1 / 2 + (length1 - length2) / 2, + y: pointer.y + length3 / 2 - (length3 - length4) - length4, }, { x: pointer.x - length1 / 2 + (length1 - length2) / 2, y: pointer.y + length3 / 2 - (length3 - length4), }, { - x: pointer.x - length1 / 2 + (length1 - length2) / 2, - y: pointer.y + length3 / 2 - (length3 - length4) - length4, + x: pointer.x - length1 / 2, + y: pointer.y + length3 / 2, }, { - x: pointer.x - length1 / 2 + (length1 - length2) / 2 + length2, - y: pointer.y + length3 / 2 - (length3 - length4) - length4, + x: pointer.x - length1 / 2 + length1, + y: pointer.y + length3 / 2, }, { x: pointer.x - length1 / 2 + (length1 - length2) / 2 + length2, y: pointer.y + length3 / 2 - (length3 - length4) - length4 + length4, }, { - x: pointer.x - length1 / 2 + length1, - y: pointer.y + length3 / 2, + x: pointer.x - length1 / 2 + (length1 - length2) / 2 + length2, + y: pointer.y + length3 / 2 - (length3 - length4) - length4, }, ] break @@ -676,25 +683,25 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { const topL = (length1 - length2) / 2 / Math.cos((angle * Math.PI) / 180) // 꺽이는부분 윗쪽 길이 points = [ - { - 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 + length1, + y: pointer.y + length3 / 2, + }, + { + 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)), }, { 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)), + x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)), + y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)), }, ] break @@ -1077,7 +1084,10 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { * @param { } polygon */ + //폴리곤, 상하반전, 좌우반전 const changeSurfaceLineType = (polygon) => { + const { isXInversion, isYInversion } = polygon //상하반전, 좌우반전 + polygon.lines.forEach((line) => { line.attributes.type = LINE_TYPE.WALLLINE.GABLE }) @@ -1095,10 +1105,8 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { if (line[coord1] === line[coord2]) { if (line.direction === evaesDirection) { line.attributes.type = LINE_TYPE.WALLLINE.EAVES - line.stroke = 'rgb(47, 0, 255)' } else if (line.direction === ridgeDirection) { line.attributes.type = LINE_TYPE.SUBLINE.RIDGE - line.stroke = 'rgb(44, 255, 2)' } } }) @@ -1118,19 +1126,18 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { : a }) - if ( - (polygon.direction === 'south' && maxLineSorted.direction === 'left') || - (polygon.direction === 'north' && maxLineSorted.direction === 'right') || - (polygon.direction === 'east' && maxLineSorted.direction === 'bottom') || - (polygon.direction === 'west' && maxLineSorted.direction === 'top') - ) { - polygon.lines.forEach((line) => { - if (line.attributes.type === LINE_TYPE.WALLLINE.EAVES) { - line.attributes.type = LINE_TYPE.SUBLINE.RIDGE - } else if (line.attributes.type === LINE_TYPE.SUBLINE.RIDGE) { - line.attributes.type = LINE_TYPE.WALLLINE.EAVES - } - }) + //정렬된 폴리곤이 아니면(대각선이 존재하는 폴리곤일때) + if (!polygon.isSortedPoints) { + //좌우 반전을 했으면 반대로 정의함 + if (isYInversion) { + polygon.lines.forEach((line) => { + if (line.attributes.type === LINE_TYPE.WALLLINE.EAVES) { + line.attributes.type = LINE_TYPE.SUBLINE.RIDGE + } else if (line.attributes.type === LINE_TYPE.SUBLINE.RIDGE) { + line.attributes.type = LINE_TYPE.WALLLINE.EAVES + } + }) + } } if (maxLine.length === 1) { @@ -1141,31 +1148,224 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { : a }) - const isRealEavesLine = polygon.lines.find((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) - if (isRealEavesLine) { - if (polygon.direction === 'south' || polygon.direction === 'north') { - const targetCoord = - polygon.direction === 'south' ? Math.max(maxLineCoord.y1, maxLineCoord.y2) : Math.min(maxLineCoord.y1, maxLineCoord.y2) - const realLineCoord = - polygon.direction === 'south' ? Math.max(isRealEavesLine.y1, isRealEavesLine.y2) : Math.min(isRealEavesLine.y1, isRealEavesLine.y2) + const isRealEavesLine = polygon.lines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) + if (isRealEavesLine.length > 0) { + isRealEavesLine.forEach((line) => { + if (polygon.direction === 'south' || polygon.direction === 'north') { + const targetCoord = + polygon.direction === 'south' ? Math.max(maxLineCoord.y1, maxLineCoord.y2) : Math.min(maxLineCoord.y1, maxLineCoord.y2) + const realLineCoord = polygon.direction === 'south' ? Math.max(line.y1, line.y2) : Math.min(line.y1, line.y2) - if (targetCoord !== realLineCoord) { - isRealEavesLine.attributes.type = LINE_TYPE.SUBLINE.RIDGE - } - } else if (polygon.direction === 'east' || polygon.direction === 'west') { - const targetCoord = polygon.direction === 'east' ? Math.max(maxLineCoord.x1, maxLineCoord.x2) : Math.min(maxLineCoord.x1, maxLineCoord.x2) - const realLineCoord = - polygon.direction === 'east' ? Math.max(isRealEavesLine.x1, isRealEavesLine.x2) : Math.min(isRealEavesLine.x1, isRealEavesLine.x2) + if (targetCoord !== realLineCoord) { + line.attributes.type = LINE_TYPE.SUBLINE.RIDGE + } + } else if (polygon.direction === 'east' || polygon.direction === 'west') { + const targetCoord = + polygon.direction === 'east' ? Math.max(maxLineCoord.x1, maxLineCoord.x2) : Math.min(maxLineCoord.x1, maxLineCoord.x2) + const realLineCoord = polygon.direction === 'east' ? Math.max(line.x1, line.x2) : Math.min(line.x1, line.x2) - if (targetCoord !== realLineCoord) { - isRealEavesLine.attributes.type = LINE_TYPE.SUBLINE.RIDGE + if (targetCoord !== realLineCoord) { + line.attributes.type = LINE_TYPE.SUBLINE.RIDGE + } } - } + }) } } } } + function findCentroid(points) { + let sumX = 0, + sumY = 0 + for (let i = 0; i < points.length; i++) { + sumX += points[i].x + sumY += points[i].y + } + return { x: sumX / points.length, y: sumY / points.length } + } + + // 도형의 포인트를 왼쪽부터 반시계 방향으로 정렬하는 함수 + /** + * 다각형의 점들을 시계 반대 방향으로 정렬하는 함수 + * @param {Array} points - {x, y} 좌표 객체 배열 + * @param {Object} startPoint - 시작점 (제공되지 않으면 가장 왼쪽 아래 점을 사용) + * @returns {Array} 시계 반대 방향으로 정렬된 점들의 배열 + */ + function orderPointsCounterClockwise(points, startPoint = null) { + if (points.length <= 3) { + return points // 점이 3개 이하면 이미 다각형의 모든 점이므로 그대로 반환 + } + + // 시작점이 제공되지 않았다면 가장 왼쪽 아래 점을 찾음 + let start = startPoint + if (!start) { + start = points[0] + for (let i = 1; i < points.length; i++) { + if (points[i].x < start.x || (points[i].x === start.x && points[i].y < start.y)) { + start = points[i] + } + } + } + + // 다각형의 중심점 계산 + let centerX = 0, + centerY = 0 + for (let i = 0; i < points.length; i++) { + centerX += points[i].x + centerY += points[i].y + } + centerX /= points.length + centerY /= points.length + + // 시작점에서 시계 반대 방향으로 각도 계산 + let angles = [] + for (let i = 0; i < points.length; i++) { + // 시작점은 제외 + if (points[i] === start) continue + + // 시작점을 기준으로 각 점의 각도 계산 + let angle = Math.atan2(points[i].y - start.y, points[i].x - start.x) + + // 각도가 음수면 2π를 더해 0~2π 범위로 변환 + if (angle < 0) angle += 2 * Math.PI + + angles.push({ + point: points[i], + angle: angle, + }) + } + + // 각도에 따라 정렬 (시계 반대 방향) + angles.sort((a, b) => a.angle - b.angle) + + // 정렬된 배열 생성 (시작점을 첫 번째로) + let orderedPoints = [start] + for (let i = 0; i < angles.length; i++) { + orderedPoints.push(angles[i].point) + } + + return orderedPoints + } + + /** + * 특정 점에서 시작하여 시계 반대 방향으로 다음 점을 찾는 함수 + * @param {Object} currentPoint - 현재 점 {x, y} + * @param {Array} points - 모든 점들의 배열 + * @param {Array} visited - 방문한 점들의 인덱스 배열 + * @param {Object} prevVector - 이전 벡터 방향 (첫 호출에서는 null) + * @returns {Object} 다음 점의 인덱스와 객체 + */ + function findNextCounterClockwisePoint(currentPoint, points, visited, prevVector = null) { + let minAngle = Infinity + let nextIndex = -1 + + // 이전 벡터가 없으면 (첫 점인 경우) 아래쪽을 향하는 벡터 사용 + if (!prevVector) { + prevVector = { x: 0, y: -1 } + } + + for (let i = 0; i < points.length; i++) { + // 이미 방문했거나 현재 점이면 건너뜀 + if (visited.includes(i) || (points[i].x === currentPoint.x && points[i].y === currentPoint.y)) { + continue + } + + // 현재 점에서 다음 후보 점으로의 벡터 + let vector = { + x: points[i].x - currentPoint.x, + y: points[i].y - currentPoint.y, + } + + // 벡터의 크기 + let magnitude = Math.sqrt(vector.x * vector.x + vector.y * vector.y) + + // 단위 벡터로 정규화 + vector.x /= magnitude + vector.y /= magnitude + + // 이전 벡터와 현재 벡터 사이의 각도 계산 (내적 사용) + let dotProduct = prevVector.x * vector.x + prevVector.y * vector.y + let crossProduct = prevVector.x * vector.y - prevVector.y * vector.x + + // 각도 계산 (atan2 사용) + let angle = Math.atan2(crossProduct, dotProduct) + + // 시계 반대 방향으로 가장 작은 각도를 가진 점 찾기 + // 각도가 음수면 2π를 더해 0~2π 범위로 변환 + if (angle < 0) angle += 2 * Math.PI + + if (angle < minAngle) { + minAngle = angle + nextIndex = i + } + } + + return nextIndex !== -1 ? { index: nextIndex, point: points[nextIndex] } : null + } + + /** + * 다각형의 점들을 시계 반대 방향으로 추적하는 함수 + * @param {Array} points - {x, y} 좌표 객체 배열 + * @param {Object} startPoint - 시작점 (제공되지 않으면 가장 왼쪽 아래 점을 사용) + * @returns {Array} 시계 반대 방향으로 정렬된 점들의 배열 + */ + function tracePolygonCounterClockwise(points, startPoint = null) { + if (points.length <= 3) { + return orderPointsCounterClockwise(points, startPoint) + } + + // 시작점이 제공되지 않았다면 가장 왼쪽 아래 점을 찾음 + let startIndex = 0 + if (!startPoint) { + for (let i = 1; i < points.length; i++) { + if (points[i].x < points[startIndex].x || (points[i].x === points[startIndex].x && points[i].y < points[startIndex].y)) { + startIndex = i + } + } + startPoint = points[startIndex] + } else { + // 시작점이 제공된 경우 해당 점의 인덱스 찾기 + for (let i = 0; i < points.length; i++) { + if (points[i].x === startPoint.x && points[i].y === startPoint.y) { + startIndex = i + break + } + } + } + + // 결과 배열 초기화 + let orderedPoints = [startPoint] + let visited = [startIndex] + + let currentPoint = startPoint + let prevVector = null + + // 모든 점을 방문할 때까지 반복 + while (visited.length < points.length) { + let next = findNextCounterClockwisePoint(currentPoint, points, visited, prevVector) + + if (!next) break // 더 이상 찾을 점이 없으면 종료 + + orderedPoints.push(next.point) + visited.push(next.index) + + // 이전 벡터 업데이트 (현재 점에서 다음 점으로의 벡터) + prevVector = { + x: next.point.x - currentPoint.x, + y: next.point.y - currentPoint.y, + } + + // 벡터 정규화 + let magnitude = Math.sqrt(prevVector.x * prevVector.x + prevVector.y * prevVector.y) + prevVector.x /= magnitude + prevVector.y /= magnitude + + currentPoint = next.point + } + + return orderedPoints + } + return { applySurfaceShape, deleteAllSurfacesAndObjects, diff --git a/src/hooks/useEvent.js b/src/hooks/useEvent.js index d3d270f6..2e5d23b0 100644 --- a/src/hooks/useEvent.js +++ b/src/hooks/useEvent.js @@ -79,6 +79,9 @@ export function useEvent() { // 마우스 위치 기준으로 확대/축소 canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom) + canvas.requestRenderAll() + canvas.calcOffset() + // 이벤트의 기본 동작 방지 (스크롤 방지) opt.e.preventDefault() opt.e.stopPropagation() diff --git a/src/locales/ja.json b/src/locales/ja.json index 618f26c4..9ca0466a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -128,7 +128,7 @@ "modal.module.basic.setting.pitch.module.row.margin": "上下間隔", "modal.module.basic.setting.pitch.module.column.amount": "列数", "modal.module.basic.setting.pitch.module.column.margin": "左右間隔", - "modal.module.basic.setting.prev": "移転", + "modal.module.basic.setting.prev": "前に戻る", "modal.module.basic.setting.passivity.placement": "手動配置", "modal.module.basic.setting.auto.placement": "設定値に自動配置", "plan.menu.module.circuit.setting.circuit.trestle.setting": "回路設定", @@ -1042,5 +1042,6 @@ "wall.line.not.found": "外壁がありません", "roof.line.not.found": "屋根形状がありません", "roof.material.can.not.delete": "割り当てられた配置面があります。", - "module.layout.setup.max.count": "모듈의 최대 단수는 {0}, 최대 열수는 {1} 입니다. (JA)" + "module.layout.setup.max.count": "모듈의 최대 단수는 {0}, 최대 열수는 {1} 입니다. (JA)", + "chidory.can.not.install": "千鳥配置できない工法です。" } diff --git a/src/locales/ko.json b/src/locales/ko.json index 4e2a5589..f61ea784 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1042,5 +1042,6 @@ "wall.line.not.found": "외벽선이 없습니다.", "roof.line.not.found": "지붕형상이 없습니다.", "roof.material.can.not.delete": "할당된 배치면이 있습니다.", - "module.layout.setup.max.count": "모듈의 최대 단수는 {0}, 최대 열수는 {1} 입니다." + "module.layout.setup.max.count": "모듈의 최대 단수는 {0}, 최대 열수는 {1} 입니다.", + "chidory.can.not.install": "치조 불가 공법입니다." }