-
+
{pitchText}
diff --git a/src/hooks/common/useMenu.js b/src/hooks/common/useMenu.js
index 7836b73b..40c1b6a0 100644
--- a/src/hooks/common/useMenu.js
+++ b/src/hooks/common/useMenu.js
@@ -66,6 +66,9 @@ export default function useMenu() {
case MENU.ROOF_COVERING.ROOF_SHAPE_ALLOC:
addPopup(popupId, 1, )
break
+ case MENU.ROOF_COVERING.ALL_REMOVE:
+ deleteAllSurfacesAndObjects()
+ break
}
}
diff --git a/src/hooks/common/useRoofFn.js b/src/hooks/common/useRoofFn.js
index 0635b89e..eb640d76 100644
--- a/src/hooks/common/useRoofFn.js
+++ b/src/hooks/common/useRoofFn.js
@@ -6,6 +6,8 @@ import { POLYGON_TYPE } from '@/common/common'
import { useEvent } from '@/hooks/useEvent'
import { useLine } from '@/hooks/useLine'
import { outerLinePointsState } from '@/store/outerLineAtom'
+import { usePolygon } from '@/hooks/usePolygon'
+import { useText } from '@/hooks/useText'
const ROOF_COLOR = {
0: 'rgb(199,240,213)',
@@ -13,6 +15,7 @@ const ROOF_COLOR = {
2: 'rgb(187,204,255)',
3: 'rgb(228,202,255)',
}
+
export function useRoofFn() {
const canvas = useRecoilValue(canvasState)
const selectedRoofMaterial = useRecoilValue(selectedRoofMaterialSelector)
@@ -20,6 +23,8 @@ export function useRoofFn() {
const { addCanvasMouseEventListener, initEvent } = useEvent()
const resetPoints = useResetRecoilState(outerLinePointsState)
const { addPitchText } = useLine()
+ const { setPolygonLinesActualSize } = usePolygon()
+ const { changeCorridorDimensionText } = useText()
//면형상 선택 클릭시 지붕 패턴 입히기
function setSurfaceShapePattern(polygon, mode = 'onlyBorder', trestleMode = false, roofMaterial, isForceChange = false, isDisplay = false) {
@@ -27,6 +32,9 @@ export function useRoofFn() {
if (!polygon) {
return
}
+ if (polygon.wall) {
+ return
+ }
if (polygon.points.length < 3) {
return
}
@@ -44,6 +52,7 @@ export function useRoofFn() {
let width = (roofMaterial.width || 226) / 10
let height = (roofMaterial.length || 158) / 10
+
const index = roofMaterial.index ?? 0
let roofStyle = 2
const inputPatternSize = { width: width, height: height } //임시 사이즈
@@ -169,6 +178,8 @@ export function useRoofFn() {
polygon.set('fill', null)
polygon.set('fill', pattern)
polygon.roofMaterial = roofMaterial
+ setPolygonLinesActualSize(polygon)
+ changeCorridorDimensionText()
polygon.canvas?.renderAll()
} catch (e) {
console.log(e)
@@ -303,7 +314,15 @@ export function useRoofFn() {
}
function convertAbsolutePoint(area) {
- return area.points.map((p) => fabric.util.transformPoint({ x: p.x - area.pathOffset.x, y: p.y - area.pathOffset.y }, area.calcTransformMatrix()))
+ return area.points.map((p) =>
+ fabric.util.transformPoint(
+ {
+ x: p.x - area.pathOffset.x,
+ y: p.y - area.pathOffset.y,
+ },
+ area.calcTransformMatrix(),
+ ),
+ )
}
const removeOuterLines = (currentMousePos) => {
diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js
index 7a700dc0..933a423e 100644
--- a/src/hooks/module/useModuleBasicSetting.js
+++ b/src/hooks/module/useModuleBasicSetting.js
@@ -63,6 +63,7 @@ export function useModuleBasicSetting(tabNum) {
const { checkModuleDisjointSurface } = useTurf()
useEffect(() => {
+ initEvent()
return () => {
//수동 설치시 초기화
removeMouseEvent('mouse:up')
@@ -139,7 +140,7 @@ export function useModuleBasicSetting(tabNum) {
roof.lines.forEach((line) => {
line.attributes = {
...line.attributes,
- offset: getOffset(offsetObjects.addRoof, line, roof.pitch, roof.from),
+ offset: getOffset(offsetObjects.addRoof, line, roof.roofMaterial.pitch),
}
})
//배치면 설치 영역
@@ -209,9 +210,9 @@ export function useModuleBasicSetting(tabNum) {
const calculateHeightRate = 1 / Math.cos((degree * Math.PI) / 180)
const calculateValue = calculateHeightRate / calculateExpression(degree)
- const eavesResult = from === 'roofCover' ? (data.eavesMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.eavesMargin / 10
- const ridgeResult = from === 'roofCover' ? (data.ridgeMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.ridgeMargin / 10
- const kerabaMargin = from === 'roofCover' && isDiagonal ? data.kerabaMargin / calculateValue / 10 : data.kerabaMargin / 10
+ const eavesResult = +roofSizeSet === 1 ? (data.eavesMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.eavesMargin / 10
+ const ridgeResult = +roofSizeSet === 1 ? (data.ridgeMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.ridgeMargin / 10
+ const kerabaMargin = +roofSizeSet === 1 && isDiagonal ? data.kerabaMargin / calculateValue / 10 : data.kerabaMargin / 10
switch (line.attributes.type) {
case LINE_TYPE.WALLLINE.EAVES:
@@ -232,7 +233,7 @@ export function useModuleBasicSetting(tabNum) {
//가대 상세 데이터 기준으로 모듈 설치 배치면 생성
const makeModuleInstArea = (roof, trestleDetail) => {
//지붕 객체 반환
-
+
if (tabNum == 3) {
if (!roof) {
return
@@ -556,7 +557,7 @@ export function useModuleBasicSetting(tabNum) {
let tmpHeight = flowDirection === 'south' || flowDirection === 'north' ? moduleHeight : moduleWidth
let { width, height } =
- moduleSetupSurface.from === 'roofCover'
+ +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection)
: { width: tmpWidth, height: tmpHeight }
@@ -1056,11 +1057,11 @@ export function useModuleBasicSetting(tabNum) {
let tmpHeight = flowDirection === 'south' || flowDirection === 'north' ? moduleHeight : moduleWidth
width =
- moduleSetupSurface.from === 'roofCover'
+ +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection).width
: tmpWidth
height =
- moduleSetupSurface.from === 'roofCover'
+ +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection).height
: tmpHeight
@@ -1386,11 +1387,11 @@ export function useModuleBasicSetting(tabNum) {
//복시도, 실치수에 따른 모듈 높이 조정
width =
- trestlePolygon.from === 'roofCover'
+ +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(trestlePolygon.roofMaterial.pitch), flowDirection).width
: tmpWidth
height =
- trestlePolygon.from === 'roofCover'
+ +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(trestlePolygon.roofMaterial.pitch), flowDirection).height
: tmpHeight
@@ -2974,11 +2975,11 @@ export function useModuleBasicSetting(tabNum) {
const pointY2 = top
//디버깅
- const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
- stroke: 'red',
- strokeWidth: 1,
- selectable: true,
- })
+ // const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
+ // stroke: 'red',
+ // strokeWidth: 1,
+ // selectable: true,
+ // })
// canvas?.add(finalLine)
// canvas?.renderAll()
@@ -3106,11 +3107,11 @@ export function useModuleBasicSetting(tabNum) {
const pointY2 = coords[2].y + ((coords[2].x - top) / (coords[2].x - coords[1].x)) * (coords[1].y - coords[2].y)
//디버깅용
- const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
- stroke: 'red',
- strokeWidth: 1,
- selectable: true,
- })
+ // const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
+ // stroke: 'red',
+ // strokeWidth: 1,
+ // selectable: true,
+ // })
// canvas?.add(finalLine)
// canvas?.renderAll()
@@ -3308,7 +3309,7 @@ export function useModuleBasicSetting(tabNum) {
let tmpHeight = flowDirection === 'south' || flowDirection === 'north' ? moduleHeight : moduleWidth
let { width, height } =
- moduleSetupSurface.from === 'roofCover'
+ +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection)
: { width: tmpWidth, height: tmpHeight }
@@ -4027,7 +4028,7 @@ export function useModuleBasicSetting(tabNum) {
10
}
- return moduleSetupSurface.from === 'roofCover'
+ return +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurface.roofMaterial.pitch), moduleSetupSurface.direction)
: { width: tmpWidth, height: tmpHeight }
}
diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js
index 05325822..73fb92f8 100644
--- a/src/hooks/module/useTrestle.js
+++ b/src/hooks/module/useTrestle.js
@@ -82,7 +82,6 @@ export const useTrestle = () => {
}
let rackInfos = []
-
if (rack) {
rackInfos = Object.keys(rack).map((key) => {
return { key, value: rack[key] }
@@ -2484,8 +2483,8 @@ export const useTrestle = () => {
// 각도에 따른 길이 반환
function getTrestleLength(length, degree, surface) {
- if (surface.from !== 'roofCover') {
- // 지붕덮개로부터 온게 아니면 그냥 length 리턴
+ if (+roofSizeSet !== 1) {
+ // 복시도 입력이 아닌경우 그냥 길이 return
return length
}
const radians = (degree * Math.PI) / 180
diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js
index e2742ff2..9b060fde 100644
--- a/src/hooks/option/useCanvasSetting.js
+++ b/src/hooks/option/useCanvasSetting.js
@@ -18,7 +18,6 @@ import {
basicSettingState,
correntObjectNoState,
corridorDimensionSelector,
- fetchRoofMaterialsState,
roofMaterialsAtom,
selectedRoofMaterialSelector,
settingModalFirstOptionsState,
@@ -41,6 +40,8 @@ import { useCanvasPopupStatusController } from '@/hooks/common/useCanvasPopupSta
import { v4 as uuidv4 } from 'uuid'
import { useEvent } from '@/hooks/useEvent'
import { logger } from '@/util/logger'
+import { useText } from '@/hooks/useText'
+import { usePolygon } from '@/hooks/usePolygon'
const defaultDotLineGridSetting = {
INTERVAL: {
@@ -118,7 +119,6 @@ export function useCanvasSetting(executeEffect = true) {
const { getRoofMaterialList, getModuleTypeItemList } = useMasterController()
const [roofMaterials, setRoofMaterials] = useRecoilState(roofMaterialsAtom)
const [addedRoofs, setAddedRoofs] = useRecoilState(addedRoofsState)
- const [fetchRoofMaterials, setFetchRoofMaterials] = useRecoilState(fetchRoofMaterialsState)
const setCurrentMenu = useSetRecoilState(currentMenuState)
const resetModuleSelectionData = useResetRecoilState(moduleSelectionDataState) /* 다음으로 넘어가는 최종 데이터 */
@@ -133,6 +133,9 @@ export function useCanvasSetting(executeEffect = true) {
const { addPopup } = usePopup()
const [popupId, setPopupId] = useState(uuidv4())
+ const { changeCorridorDimensionText } = useText()
+ const { setPolygonLinesActualSize } = usePolygon()
+
const SelectOptions = [
{ id: 1, name: getMessage('modal.canvas.setting.grid.dot.line.setting.line.origin'), value: 1 },
{ id: 2, name: '1/2', value: 1 / 2 },
@@ -197,7 +200,7 @@ export function useCanvasSetting(executeEffect = true) {
}
}, [addedRoofs])
- useEffect(() => {
+ /*useEffect(() => {
if (!executeEffect) {
return
}
@@ -212,7 +215,7 @@ export function useCanvasSetting(executeEffect = true) {
setAddedRoofs(newAddedRoofs)
}
setBasicSettings({ ...basicSetting, selectedRoofMaterial: selectedRoofMaterial })
- }, [roofMaterials])
+ }, [roofMaterials])*/
useEffect(() => {
if (!canvas) {
@@ -221,39 +224,13 @@ export function useCanvasSetting(executeEffect = true) {
if (!executeEffect) {
return
}
- const { column } = corridorDimension
- const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText')
- const group = canvas.getObjects().filter((obj) => obj.type === 'group')
- group.forEach((obj) => {
- obj._objects
- .filter((obj2) => obj2.name === 'lengthText')
- .forEach((obj3) => {
- lengthTexts.push(obj3)
- })
- })
-
- switch (column) {
- case 'corridorDimension':
- lengthTexts.forEach((obj) => {
- if (obj.planeSize) {
- obj.set({ text: obj.planeSize.toString() })
- }
- })
- break
- case 'realDimension':
- lengthTexts.forEach((obj) => {
- if (obj.actualSize) {
- obj.set({ text: obj.actualSize.toString() })
- }
- })
- break
- case 'noneDimension':
- lengthTexts.forEach((obj) => {
- obj.set({ text: '' })
- })
- break
+ const roofs = canvasObjects.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
+ if (roofs.length > 0) {
+ roofs.forEach((roof) => {
+ setPolygonLinesActualSize(roof)
+ })
+ changeCorridorDimensionText()
}
- canvas?.renderAll()
}, [corridorDimension])
useEffect(() => {
@@ -448,17 +425,18 @@ export function useCanvasSetting(executeEffect = true) {
}
if (addRoofs.length > 0) {
- setAddedRoofs(addRoofs)
-
- setBasicSettings({
- ...basicSetting,
- roofMaterials: addRoofs[0],
- planNo: roofsRow[0].planNo,
- roofSizeSet: roofsRow[0].roofSizeSet,
- roofAngleSet: roofsRow[0].roofAngleSet,
- roofsData: roofsArray,
- selectedRoofMaterial: addRoofs.find((roof) => roof.selected),
+ setBasicSettings((prev) => {
+ return {
+ ...basicSetting,
+ roofMaterials: addRoofs[0],
+ planNo: roofsRow[0].planNo,
+ roofSizeSet: roofsRow[0].roofSizeSet,
+ roofAngleSet: roofsRow[0].roofAngleSet,
+ roofsData: roofsArray,
+ selectedRoofMaterial: addRoofs.find((roof) => roof.selected),
+ }
})
+ setAddedRoofs(addRoofs)
setCanvasSetting({
...basicSetting,
diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js
index 417286e4..3fef0ee9 100644
--- a/src/hooks/roofcover/useRoofAllocationSetting.js
+++ b/src/hooks/roofcover/useRoofAllocationSetting.js
@@ -28,6 +28,7 @@ import { outerLinePointsState } from '@/store/outerLineAtom'
import { QcastContext } from '@/app/QcastProvider'
import { usePlan } from '@/hooks/usePlan'
import { roofsState } from '@/store/roofAtom'
+import { useText } from '@/hooks/useText'
export function useRoofAllocationSetting(id) {
const canvas = useRecoilValue(canvasState)
@@ -60,6 +61,7 @@ export function useRoofAllocationSetting(id) {
const [moduleSelectionData, setModuleSelectionData] = useRecoilState(moduleSelectionDataState)
const resetPoints = useResetRecoilState(outerLinePointsState)
const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector)
+ const { changeCorridorDimensionText } = useText()
useEffect(() => {
/** 배치면 초기설정에서 선택한 지붕재 배열 설정 */
@@ -127,9 +129,9 @@ export function useRoofAllocationSetting(id) {
}
})
} else {
- if(roofList.length > 0){
+ if (roofList.length > 0) {
roofsArray = roofList
- }else{
+ } else {
roofsArray = [
{
planNo: planNo,
@@ -188,7 +190,6 @@ export function useRoofAllocationSetting(id) {
})
//데이터 동기화
setCurrentRoofList(selectRoofs)
-
})
} catch (error) {
console.error('Data fetching error:', error)
@@ -459,6 +460,10 @@ export function useRoofAllocationSetting(id) {
/** 모듈 선택 데이터 초기화 */
// modifyModuleSelectionData()
setModuleSelectionData({ ...moduleSelectionData, roofConstructions: newRoofList })
+
+ setTimeout(() => {
+ changeCorridorDimensionText('realDimension')
+ }, 500)
}
/**
diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js
index 863de4bf..874526f8 100644
--- a/src/hooks/surface/useSurfaceShapeBatch.js
+++ b/src/hooks/surface/useSurfaceShapeBatch.js
@@ -1,9 +1,8 @@
'use client'
-import { useEffect } from 'react'
-import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
+import { useRecoilValue, useResetRecoilState } from 'recoil'
import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom'
-import { MENU, POLYGON_TYPE, LINE_TYPE } from '@/common/common'
+import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util'
import { degreesToRadians } from '@turf/turf'
import { QPolygon } from '@/components/fabric/QPolygon'
@@ -20,13 +19,12 @@ import { useRoofFn } from '@/hooks/common/useRoofFn'
import { outerLinePointsState } from '@/store/outerLineAtom'
import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom'
import { getBackGroundImage } from '@/lib/imageActions'
-import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placementShape/PlacementSurfaceLineProperty'
-import { v4 as uuidv4 } from 'uuid'
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
+import { useText } from '@/hooks/useText'
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
const { getMessage } = useMessage()
- const { drawDirectionArrow, addPolygon } = usePolygon()
+ const { drawDirectionArrow, addPolygon, addLengthText } = usePolygon()
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
@@ -40,6 +38,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
// const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
const { addPopup, closePopup } = usePopup()
const { setSurfaceShapePattern } = useRoofFn()
+ const { changeCorridorDimensionText } = useText()
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
const { fetchSettings } = useCanvasSetting(false)
@@ -848,7 +847,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
const selection = new fabric.ActiveSelection(selectionArray, {
canvas: canvas,
- draggable: true,
+ // draggable: true,
lockMovementX: false, // X축 이동 허용
lockMovementY: false, // Y축 이동 허용
originX: 'center',
@@ -858,7 +857,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
canvas.setActiveObject(selection)
addCanvasMouseEventListener('mouse:up', (e) => {
- canvas.selection = true
+ canvas.selection = false
canvas.discardActiveObject() // 모든 선택 해제
canvas.requestRenderAll() // 화면 업데이트
@@ -875,10 +874,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
}
})
- canvas.renderAll()
- roof.fire('polygonMoved')
+ // roof.fire('polygonMoved')
+ roof.fire('modified')
drawDirectionArrow(roof)
+ changeCorridorDimensionText()
+ addLengthText(roof)
initEvent()
+ canvas.renderAll()
})
}
}
diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js
index 2acc929f..d991a2cf 100644
--- a/src/hooks/useCanvasEvent.js
+++ b/src/hooks/useCanvasEvent.js
@@ -5,6 +5,8 @@ import { canvasSizeState, canvasState, canvasZoomState, currentMenuState, curren
import { QPolygon } from '@/components/fabric/QPolygon'
import { fontSelector } from '@/store/fontAtom'
import { MENU, POLYGON_TYPE } from '@/common/common'
+import { useText } from '@/hooks/useText'
+import { usePolygon } from '@/hooks/usePolygon'
// 캔버스에 필요한 이벤트
export function useCanvasEvent() {
@@ -15,6 +17,8 @@ export function useCanvasEvent() {
const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState)
const lengthTextOption = useRecoilValue(fontSelector('lengthText'))
const currentMenu = useRecoilValue(currentMenuState)
+ const { changeCorridorDimensionText } = useText()
+ const { setPolygonLinesActualSize } = usePolygon()
useEffect(() => {
canvas?.setZoom(canvasZoom / 100)
@@ -63,6 +67,13 @@ export function useCanvasEvent() {
textObjs.forEach((obj) => {
obj.bringToFront()
})
+
+ if (target.name === POLYGON_TYPE.ROOF) {
+ setTimeout(() => {
+ setPolygonLinesActualSize(target)
+ changeCorridorDimensionText()
+ }, 300)
+ }
}
if (target.name === 'cell') {
@@ -116,48 +127,6 @@ export function useCanvasEvent() {
target.setControlVisible(controlKey, false)
})
})
- /*target.on('editing:exited', () => {
- if (isNaN(target.text.trim())) {
- target.set({ text: previousValue })
- canvas?.renderAll()
- return
- }
- const updatedValue = parseFloat(target.text.trim())
- const targetParent = target.parent
- const points = targetParent.getCurrentPoints()
- const i = target.idx // Assuming target.index gives the index of the point
-
- const startPoint = points[i]
- const endPoint = points[(i + 1) % points.length]
-
- const dx = endPoint.x - startPoint.x
- const dy = endPoint.y - startPoint.y
-
- const currentLength = Math.sqrt(dx * dx + dy * dy)
- const scaleFactor = updatedValue / currentLength
-
- const newEndPoint = {
- x: startPoint.x + dx * scaleFactor,
- y: startPoint.y + dy * scaleFactor,
- }
-
- const newPoints = [...points]
- newPoints[(i + 1) % points.length] = newEndPoint
-
- for (let idx = i + 1; idx < points.length; idx++) {
- if (newPoints[idx].x === endPoint.x) {
- newPoints[idx].x = newEndPoint.x
- } else if (newPoints[idx].y === endPoint.y) {
- newPoints[idx].y = newEndPoint.y
- }
- }
-
- const newPolygon = new QPolygon(newPoints, targetParent.initOptions)
- canvas?.add(newPolygon)
- canvas?.remove(targetParent)
- canvas?.renderAll()
- })*/
-
target.on('moving', (e) => {
target.uuid = uuidv4()
diff --git a/src/hooks/useCirCuitTrestle.js b/src/hooks/useCirCuitTrestle.js
index 12e1b28b..a35d3c8c 100644
--- a/src/hooks/useCirCuitTrestle.js
+++ b/src/hooks/useCirCuitTrestle.js
@@ -10,7 +10,7 @@ import {
selectedModelsState,
seriesState,
} from '@/store/circuitTrestleAtom'
-import { moduleSelectionDataState, selectedModuleState } from '@/store/selectedModuleOptions'
+import { selectedModuleState } from '@/store/selectedModuleOptions'
import { useContext, useEffect } from 'react'
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
import { useMessage } from './useMessage'
@@ -101,7 +101,11 @@ export function useCircuitTrestle(executeEffect = false) {
// result 배열에서 roofSurface 값을 기준으로 순서대로 정렬한다.
- return groupSort(result)
+ if (pcsCheck.division) {
+ return groupSort(result)
+ } else {
+ return result
+ }
}
const groupSort = (arr) => {
diff --git a/src/hooks/useEvent.js b/src/hooks/useEvent.js
index 748e680a..dfbe492c 100644
--- a/src/hooks/useEvent.js
+++ b/src/hooks/useEvent.js
@@ -129,7 +129,7 @@ export function useEvent() {
let arrivalPoint = { x: pointer.x, y: pointer.y }
if (adsorptionPointMode) {
- const roofsPoints = roofs.map((roof) => roof.points).flat()
+ const roofsPoints = roofs.map((roof) => roof.getCurrentPoints()).flat()
roofAdsorptionPoints.current = [...roofsPoints]
const auxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine')
diff --git a/src/hooks/useLine.js b/src/hooks/useLine.js
index 9d1a48eb..58f87e7d 100644
--- a/src/hooks/useLine.js
+++ b/src/hooks/useLine.js
@@ -1,15 +1,18 @@
import { useRecoilValue } from 'recoil'
import {
- ANGLE_TYPE,
canvasState,
currentAngleTypeSelector,
fontFamilyState,
fontSizeState,
+ globalPitchState,
pitchTextSelector,
showAngleUnitSelector,
} from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
-import { getChonByDegree, getDegreeByChon } from '@/util/canvas-util'
+import { basicSettingState } from '@/store/settingAtom'
+import { calcLineActualSize } from '@/util/qpolygon-utils'
+import { getDegreeByChon } from '@/util/canvas-util'
+import { useText } from '@/hooks/useText'
export const useLine = () => {
const canvas = useRecoilValue(canvasState)
@@ -18,6 +21,10 @@ export const useLine = () => {
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const angleUnit = useRecoilValue(showAngleUnitSelector)
+ const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
+ const globalPitch = useRecoilValue(globalPitchState)
+
+ const { changeCorridorDimensionText } = useText()
const addLine = (points = [], options) => {
const line = new QLine(points, {
@@ -151,6 +158,57 @@ export const useLine = () => {
})
}
+ /**
+ * 복도치수, 실제치수에 따라 actualSize를 설정한다.
+ * @param line
+ * @param direction polygon의 방향
+ * @param pitch
+ */
+ const setActualSize = (line, direction, pitch = globalPitch) => {
+ const { x1, y1, x2, y2 } = line
+
+ const isHorizontal = y1 === y2
+ const isVertical = x1 === x2
+ const isDiagonal = !isHorizontal && !isVertical
+ const lineLength = line.getLength()
+
+ line.attributes = { ...line.attributes, planeSize: line.getLength(), actualSize: line.getLength() }
+
+ if (+roofSizeSet === 1) {
+ if (direction === 'south' || direction === 'north') {
+ if (isVertical) {
+ line.attributes = {
+ ...line.attributes,
+ actualSize: calcLineActualSize(line, getDegreeByChon(pitch)),
+ }
+ } else if (isDiagonal) {
+ const yLength = Math.abs(y2 - y1) * 10
+
+ const h = yLength * Math.tan(getDegreeByChon(pitch) * (Math.PI / 180))
+
+ const actualSize = Math.sqrt(h ** 2 + lineLength ** 2)
+ line.attributes = { ...line.attributes, actualSize: actualSize }
+ }
+ } else if (direction === 'west' || direction === 'east') {
+ if (isHorizontal) {
+ line.attributes = {
+ ...line.attributes,
+ actualSize: calcLineActualSize(line, getDegreeByChon(pitch)),
+ }
+ } else if (isDiagonal) {
+ const xLength = Math.abs(x2 - x1) * 10
+
+ const h = xLength * Math.tan(getDegreeByChon(pitch) * (Math.PI / 180))
+
+ const actualSize = Math.sqrt(h ** 2 + lineLength ** 2)
+ line.attributes = { ...line.attributes, actualSize: actualSize }
+ }
+ }
+ }
+
+ line.attributes = { ...line.attributes, actualSize: Number(line.attributes.actualSize.toFixed(0)) }
+ }
+
return {
addLine,
removeLine,
@@ -160,5 +218,6 @@ export const useLine = () => {
removePitchText,
addPitchTextsByOuterLines,
getLengthByLine,
+ setActualSize,
}
}
diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js
index 12d7e188..a1cbc4f0 100644
--- a/src/hooks/usePolygon.js
+++ b/src/hooks/usePolygon.js
@@ -4,7 +4,7 @@ import { fabric } from 'fabric'
import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, getDegreeInOrientation, isPointOnLine } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon'
import { isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils'
-import { flowDisplaySelector } from '@/store/settingAtom'
+import { basicSettingState, flowDisplaySelector } from '@/store/settingAtom'
import { fontSelector } from '@/store/fontAtom'
import { QLine } from '@/components/fabric/QLine'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
@@ -18,6 +18,9 @@ export const usePolygon = () => {
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const globalPitch = useRecoilValue(globalPitchState)
+ const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
+
+ const { setActualSize } = useLine()
const { getLengthByLine } = useLine()
@@ -86,27 +89,30 @@ export const usePolygon = () => {
const maxY = line.top + line.length
const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI
- const text = new fabric.Textbox(planeSize ? planeSize.toString() : length.toString(), {
- left: left,
- top: top,
- fontSize: lengthTextFontOptions.fontSize.value,
- minX,
- maxX,
- minY,
- maxY,
- parentDirection: line.direction,
- parentDegree: degree,
- parentId: polygon.id,
- planeSize: planeSize ?? length,
- actualSize: actualSize ?? length,
- editable: false,
- selectable: true,
- lockRotation: true,
- lockScalingX: true,
- lockScalingY: true,
- parent: polygon,
- name: 'lengthText',
- })
+ const text = new fabric.Textbox(
+ +roofSizeSet === 1 ? (actualSize ? actualSize.toString() : length.toString()) : planeSize ? planeSize.toString() : length.toString(),
+ {
+ left: left,
+ top: top,
+ fontSize: lengthTextFontOptions.fontSize.value,
+ minX,
+ maxX,
+ minY,
+ maxY,
+ parentDirection: line.direction,
+ parentDegree: degree,
+ parentId: polygon.id,
+ planeSize: planeSize ?? length,
+ actualSize: actualSize ?? length,
+ editable: false,
+ selectable: true,
+ lockRotation: true,
+ lockScalingX: true,
+ lockScalingY: true,
+ parent: polygon,
+ name: 'lengthText',
+ },
+ )
polygon.texts.push(text)
canvas.add(text)
})
@@ -921,12 +927,69 @@ export const usePolygon = () => {
}
return !shouldRemove
})
-
+
// 중복된 라인들을 canvas에서 제거
linesToRemove.forEach((line) => {
canvas.remove(line)
})
+ // innerLines가 합쳐졌을 때 polygonLine과 같은 경우 그 polygonLine의 need를 false로 변경
+ const mergeOverlappingInnerLines = (lines) => {
+ const mergedLines = []
+ const processed = new Set()
+
+ lines.forEach((line, index) => {
+ if (processed.has(index)) return
+
+ let currentLine = { ...line }
+ processed.add(index)
+
+ // 현재 라인과 겹치는 다른 라인들을 찾아서 합치기
+ for (let i = index + 1; i < lines.length; i++) {
+ if (processed.has(i)) continue
+
+ const otherLine = lines[i]
+ if (checkLineOverlap(currentLine, otherLine)) {
+ // 두 라인을 합치기 - 가장 긴 범위로 확장
+ const isVertical = Math.abs(currentLine.x1 - currentLine.x2) < 1
+
+ if (isVertical) {
+ const allYPoints = [currentLine.y1, currentLine.y2, otherLine.y1, otherLine.y2]
+ currentLine.y1 = Math.min(...allYPoints)
+ currentLine.y2 = Math.max(...allYPoints)
+ currentLine.x1 = currentLine.x2 = (currentLine.x1 + otherLine.x1) / 2
+ } else {
+ const allXPoints = [currentLine.x1, currentLine.x2, otherLine.x1, otherLine.x2]
+ currentLine.x1 = Math.min(...allXPoints)
+ currentLine.x2 = Math.max(...allXPoints)
+ currentLine.y1 = currentLine.y2 = (currentLine.y1 + otherLine.y1) / 2
+ }
+
+ processed.add(i)
+ }
+ }
+
+ mergedLines.push(currentLine)
+ })
+
+ return mergedLines
+ }
+
+ const mergedInnerLines = mergeOverlappingInnerLines(innerLines)
+
+ // 합쳐진 innerLine과 동일한 polygonLine의 need를 false로 설정
+ polygonLines.forEach((polygonLine) => {
+ mergedInnerLines.forEach((mergedInnerLine) => {
+ const isSameLine =
+ (isSamePoint(polygonLine.startPoint, mergedInnerLine.startPoint) && isSamePoint(polygonLine.endPoint, mergedInnerLine.endPoint)) ||
+ (isSamePoint(polygonLine.startPoint, mergedInnerLine.endPoint) && isSamePoint(polygonLine.endPoint, mergedInnerLine.startPoint))
+
+ if (isSameLine) {
+ polygonLine.need = false
+ }
+ })
+ })
+
canvas.renderAll()
/*polygonLines.forEach((line) => {
@@ -934,6 +997,7 @@ export const usePolygon = () => {
canvas.add(line)
})
canvas.renderAll()*/
+ polygonLines = polygonLines.filter((line) => line.need)
polygonLines.forEach((line) => {
/*const originStroke = line.stroke
@@ -1589,8 +1653,8 @@ export const usePolygon = () => {
const remainingLines = [...allLines] // 사용 가능한 line들의 복사본
// isStart가 true인 line들만 시작점으로 사용
- const startLines = remainingLines.filter(line => line.attributes?.isStart === true)
-
+ const startLines = remainingLines.filter((line) => line.attributes?.isStart === true)
+
startLines.forEach((startLine) => {
// 현재 남아있는 line들로 그래프 생성
const graph = {}
@@ -1615,13 +1679,13 @@ export const usePolygon = () => {
const startPoint = { ...startLine.startPoint } // 시작점
let arrivalPoint = { ...startLine.endPoint } // 도착점
-
+
const roof = getPath(startPoint, arrivalPoint, graph)
if (roof.length > 0) {
roofs.push(roof)
-
+
// 사용된 startLine을 remainingLines에서 제거
- const startLineIndex = remainingLines.findIndex(line => line === startLine)
+ const startLineIndex = remainingLines.findIndex((line) => line === startLine)
if (startLineIndex !== -1) {
remainingLines.splice(startLineIndex, 1)
}
@@ -1675,6 +1739,22 @@ export const usePolygon = () => {
canvas.renderAll()
}
+ /**
+ * 폴리곤의 라인 속성을 복도치수, 실제치수에 따라 actualSize 설정
+ * @param polygon
+ */
+ const setPolygonLinesActualSize = (polygon) => {
+ if (!polygon.lines || polygon.lines.length === 0 || !polygon.roofMaterial) {
+ return
+ }
+
+ polygon.lines.forEach((line) => {
+ setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch)
+ })
+
+ addLengthText(polygon)
+ }
+
return {
addPolygon,
addPolygonByLines,
@@ -1683,5 +1763,6 @@ export const usePolygon = () => {
addLengthText,
splitPolygonWithLines,
splitPolygonWithSeparate,
+ setPolygonLinesActualSize,
}
}
diff --git a/src/hooks/useText.js b/src/hooks/useText.js
new file mode 100644
index 00000000..134ade4a
--- /dev/null
+++ b/src/hooks/useText.js
@@ -0,0 +1,51 @@
+import { useRecoilValue } from 'recoil'
+import { corridorDimensionSelector } from '@/store/settingAtom'
+import { canvasState } from '@/store/canvasAtom'
+
+export function useText() {
+ const canvas = useRecoilValue(canvasState)
+ const corridorDimension = useRecoilValue(corridorDimensionSelector)
+
+ const changeCorridorDimensionText = (columnText) => {
+ let { column } = corridorDimension
+ if (columnText) {
+ column = columnText
+ }
+ const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText')
+ const group = canvas.getObjects().filter((obj) => obj.type === 'group')
+ group.forEach((obj) => {
+ obj._objects
+ .filter((obj2) => obj2.name === 'lengthText')
+ .forEach((obj3) => {
+ lengthTexts.push(obj3)
+ })
+ })
+
+ switch (column) {
+ case 'corridorDimension':
+ lengthTexts.forEach((obj) => {
+ if (obj.planeSize) {
+ obj.set({ text: obj.planeSize.toString() })
+ }
+ })
+ break
+ case 'realDimension':
+ lengthTexts.forEach((obj) => {
+ if (obj.actualSize) {
+ obj.set({ text: obj.actualSize.toString() })
+ }
+ })
+ break
+ case 'noneDimension':
+ lengthTexts.forEach((obj) => {
+ obj.set({ text: '' })
+ })
+ break
+ }
+ canvas?.renderAll()
+ }
+
+ return {
+ changeCorridorDimensionText,
+ }
+}
diff --git a/src/lib/skeletons/Circular/CircularList.ts b/src/lib/skeletons/Circular/CircularList.ts
new file mode 100644
index 00000000..690f5e5f
--- /dev/null
+++ b/src/lib/skeletons/Circular/CircularList.ts
@@ -0,0 +1,113 @@
+import CircularNode from "./CircularNode";
+
+export interface ICircularList {
+ readonly Size: number;
+
+ AddNext(node: CircularNode, newNode: CircularNode): void;
+
+ AddPrevious(node: CircularNode, newNode: CircularNode): void;
+
+ AddLast(node: CircularNode): void;
+
+ Remove(node: CircularNode): void;
+}
+
+export default class CircularList
implements ICircularList {
+ private _first: T = null;
+ private _size: number = 0;
+
+ public AddNext(node: CircularNode, newNode: CircularNode) {
+ if (newNode.List !== null)
+ throw new Error("Node is already assigned to different list!");
+
+ newNode.List = this;
+
+ newNode.Previous = node;
+ newNode.Next = node.Next;
+
+ node.Next.Previous = newNode;
+ node.Next = newNode;
+
+ this._size++;
+ }
+
+ AddPrevious(node: CircularNode, newNode: CircularNode) {
+ if (newNode.List !== null)
+ throw new Error("Node is already assigned to different list!");
+
+ newNode.List = this;
+
+ newNode.Previous = node.Previous;
+ newNode.Next = node;
+
+ node.Previous.Next = newNode;
+ node.Previous = newNode;
+
+ this._size++;
+ }
+
+ AddLast(node: CircularNode) {
+ if (node.List !== null)
+ throw new Error("Node is already assigned to different list!");
+
+ if (this._first === null) {
+ this._first = node as T;
+
+ node.List = this;
+ node.Next = node;
+ node.Previous = node;
+
+ this._size++;
+ } else
+ this.AddPrevious(this._first, node);
+ }
+
+ Remove(node: CircularNode) {
+ if (node.List !== this)
+ throw new Error("Node is not assigned to this list!");
+
+ if (this._size <= 0)
+ throw new Error("List is empty can't remove!");
+
+ node.List = null;
+
+ if (this._size === 1)
+ this._first = null;
+
+ else {
+ if (this._first === node)
+ this._first = this._first.Next;
+
+ node.Previous.Next = node.Next;
+ node.Next.Previous = node.Previous;
+ }
+
+ node.Previous = null;
+ node.Next = null;
+
+ this._size--;
+ }
+
+ public get Size(): number {
+ return this._size;
+ }
+
+ public First(): T {
+ return this._first;
+ }
+
+ public* Iterate(): Generator {
+ let current = this._first;
+ let i = 0;
+
+ while (current !== null) {
+ yield current;
+
+ if (++i === this.Size) {
+ return;
+ }
+
+ current = current.Next;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Circular/CircularNode.ts b/src/lib/skeletons/Circular/CircularNode.ts
new file mode 100644
index 00000000..cfbda42a
--- /dev/null
+++ b/src/lib/skeletons/Circular/CircularNode.ts
@@ -0,0 +1,19 @@
+import {ICircularList} from "./CircularList";
+
+export default class CircularNode {
+ public List: ICircularList = null;
+ public Next: CircularNode = null;
+ public Previous: CircularNode = null;
+
+ public AddNext(node: CircularNode) {
+ this.List.AddNext(this, node);
+ }
+
+ public AddPrevious(node: CircularNode) {
+ this.List.AddPrevious(this, node);
+ }
+
+ public Remove() {
+ this.List.Remove(this);
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Circular/Edge.ts b/src/lib/skeletons/Circular/Edge.ts
new file mode 100644
index 00000000..a7620f57
--- /dev/null
+++ b/src/lib/skeletons/Circular/Edge.ts
@@ -0,0 +1,28 @@
+import CircularNode from "./CircularNode";
+import Vector2d from "../Primitives/Vector2d";
+import LineLinear2d from "../Primitives/LineLinear2d";
+import LineParametric2d from "../Primitives/LineParametric2d";
+
+export default class Edge extends CircularNode {
+ public readonly Begin: Vector2d;
+ public readonly End: Vector2d;
+ public readonly Norm: Vector2d;
+
+ public readonly LineLinear2d: LineLinear2d;
+ public BisectorNext: LineParametric2d = null;
+ public BisectorPrevious: LineParametric2d = null;
+
+ constructor(begin: Vector2d, end: Vector2d) {
+ super();
+
+ this.Begin = begin;
+ this.End = end;
+
+ this.LineLinear2d = new LineLinear2d(begin, end);
+ this.Norm = end.Sub(begin).Normalized();
+ }
+
+ public ToString(): string {
+ return `Edge [p1=${this.Begin}, p2=${this.End}]`;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Circular/Vertex.ts b/src/lib/skeletons/Circular/Vertex.ts
new file mode 100644
index 00000000..2b82a6f0
--- /dev/null
+++ b/src/lib/skeletons/Circular/Vertex.ts
@@ -0,0 +1,39 @@
+import CircularNode from "./CircularNode";
+import Vector2d from "../Primitives/Vector2d";
+import LineParametric2d from "../Primitives/LineParametric2d";
+import Edge from "./Edge";
+import {FaceNode} from "../Path/FaceNode";
+
+export default class Vertex extends CircularNode {
+ readonly RoundDigitCount = 5;
+
+ public Point: Vector2d = null;
+ public readonly Distance: number;
+ public readonly Bisector: LineParametric2d = null;
+
+ public readonly NextEdge: Edge = null;
+ public readonly PreviousEdge: Edge = null;
+
+ public LeftFace: FaceNode = null;
+ public RightFace: FaceNode = null;
+
+ public IsProcessed: boolean;
+
+ constructor(point: Vector2d, distance: number, bisector: LineParametric2d, previousEdge: Edge, nextEdge: Edge) {
+ super();
+
+ this.Point = point;
+ this.Distance = +distance.toFixed(this.RoundDigitCount);
+ this.Bisector = bisector;
+ this.PreviousEdge = previousEdge;
+ this.NextEdge = nextEdge;
+
+ this.IsProcessed = false;
+ }
+
+ public ToString(): string {
+ return "Vertex [v=" + this.Point + ", IsProcessed=" + this.IsProcessed +
+ ", Bisector=" + this.Bisector + ", PreviousEdge=" + this.PreviousEdge +
+ ", NextEdge=" + this.NextEdge;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/EdgeResult.ts b/src/lib/skeletons/EdgeResult.ts
new file mode 100644
index 00000000..c95ba235
--- /dev/null
+++ b/src/lib/skeletons/EdgeResult.ts
@@ -0,0 +1,13 @@
+import Edge from "./Circular/Edge";
+import Vector2d from "./Primitives/Vector2d";
+import {List} from "./Utils";
+
+export default class EdgeResult {
+ public readonly Edge: Edge;
+ public readonly Polygon: List;
+
+ constructor(edge: Edge, polygon: List) {
+ this.Edge = edge;
+ this.Polygon = polygon;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/Chains/ChainType.ts b/src/lib/skeletons/Events/Chains/ChainType.ts
new file mode 100644
index 00000000..70b63170
--- /dev/null
+++ b/src/lib/skeletons/Events/Chains/ChainType.ts
@@ -0,0 +1,7 @@
+enum ChainType {
+ Edge,
+ ClosedEdge,
+ Split
+}
+
+export default ChainType;
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/Chains/EdgeChain.ts b/src/lib/skeletons/Events/Chains/EdgeChain.ts
new file mode 100644
index 00000000..f25efbaf
--- /dev/null
+++ b/src/lib/skeletons/Events/Chains/EdgeChain.ts
@@ -0,0 +1,40 @@
+import IChain from "./IChain";
+import EdgeEvent from "../EdgeEvent";
+import {List} from "../../Utils";
+import Edge from "../../Circular/Edge";
+import Vertex from "../../Circular/Vertex";
+import ChainType from "./ChainType";
+
+export default class EdgeChain implements IChain {
+ private readonly _closed: boolean;
+ public EdgeList: List;
+
+ constructor(edgeList: List) {
+ this.EdgeList = edgeList;
+ this._closed = this.PreviousVertex === this.NextVertex;
+ }
+
+ public get PreviousEdge(): Edge {
+ return this.EdgeList[0].PreviousVertex.PreviousEdge;
+ }
+
+ public get NextEdge(): Edge {
+ return this.EdgeList[this.EdgeList.Count - 1].NextVertex.NextEdge;
+ }
+
+ public get PreviousVertex(): Vertex {
+ return this.EdgeList[0].PreviousVertex;
+ }
+
+ public get NextVertex(): Vertex {
+ return this.EdgeList[this.EdgeList.Count - 1].NextVertex;
+ }
+
+ public get CurrentVertex(): Vertex {
+ return null;
+ }
+
+ public get ChainType(): ChainType {
+ return this._closed ? ChainType.ClosedEdge : ChainType.Edge;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/Chains/IChain.ts b/src/lib/skeletons/Events/Chains/IChain.ts
new file mode 100644
index 00000000..59d22bf5
--- /dev/null
+++ b/src/lib/skeletons/Events/Chains/IChain.ts
@@ -0,0 +1,17 @@
+import Edge from "../../Circular/Edge";
+import Vertex from "../../Circular/Vertex";
+import ChainType from "./ChainType";
+
+export default interface IChain {
+ get PreviousEdge(): Edge;
+
+ get NextEdge(): Edge;
+
+ get PreviousVertex(): Vertex;
+
+ get NextVertex(): Vertex;
+
+ get CurrentVertex(): Vertex;
+
+ get ChainType(): ChainType;
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/Chains/SingleEdgeChain.ts b/src/lib/skeletons/Events/Chains/SingleEdgeChain.ts
new file mode 100644
index 00000000..305cdd2b
--- /dev/null
+++ b/src/lib/skeletons/Events/Chains/SingleEdgeChain.ts
@@ -0,0 +1,40 @@
+import IChain from "./IChain";
+import Edge from "../../Circular/Edge";
+import Vertex from "../../Circular/Vertex";
+import ChainType from "./ChainType";
+
+export default class SingleEdgeChain implements IChain {
+ private readonly _nextVertex: Vertex;
+ private readonly _oppositeEdge: Edge;
+ private readonly _previousVertex: Vertex;
+
+ constructor(oppositeEdge: Edge, nextVertex: Vertex) {
+ this._oppositeEdge = oppositeEdge;
+ this._nextVertex = nextVertex;
+ this._previousVertex = nextVertex.Previous as Vertex;
+ }
+
+ public get PreviousEdge(): Edge {
+ return this._oppositeEdge;
+ }
+
+ public get NextEdge(): Edge {
+ return this._oppositeEdge;
+ }
+
+ public get PreviousVertex(): Vertex {
+ return this._previousVertex;
+ }
+
+ public get NextVertex(): Vertex {
+ return this._nextVertex;
+ }
+
+ public get CurrentVertex(): Vertex {
+ return null;
+ }
+
+ public get ChainType(): ChainType {
+ return ChainType.Split;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/Chains/SplitChain.ts b/src/lib/skeletons/Events/Chains/SplitChain.ts
new file mode 100644
index 00000000..8cc0e522
--- /dev/null
+++ b/src/lib/skeletons/Events/Chains/SplitChain.ts
@@ -0,0 +1,45 @@
+import IChain from "./IChain";
+import Edge from "../../Circular/Edge";
+import Vertex from "../../Circular/Vertex";
+import ChainType from "./ChainType";
+import VertexSplitEvent from "../VertexSplitEvent";
+import SplitEvent from "../SplitEvent";
+
+export default class SplitChain implements IChain {
+ private readonly _splitEvent: SplitEvent;
+
+ constructor(event: SplitEvent) {
+ this._splitEvent = event;
+ }
+
+ public get OppositeEdge(): Edge {
+ if (!(this._splitEvent instanceof VertexSplitEvent))
+ return this._splitEvent.OppositeEdge;
+
+ return null;
+ }
+
+ public get PreviousEdge(): Edge {
+ return this._splitEvent.Parent.PreviousEdge;
+ }
+
+ public get NextEdge(): Edge {
+ return this._splitEvent.Parent.NextEdge;
+ }
+
+ public get PreviousVertex(): Vertex {
+ return this._splitEvent.Parent.Previous as Vertex;
+ }
+
+ public get NextVertex(): Vertex {
+ return this._splitEvent.Parent.Next as Vertex;
+ }
+
+ public get CurrentVertex(): Vertex {
+ return this._splitEvent.Parent;
+ }
+
+ public get ChainType(): ChainType {
+ return ChainType.Split;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/EdgeEvent.ts b/src/lib/skeletons/Events/EdgeEvent.ts
new file mode 100644
index 00000000..c9d53446
--- /dev/null
+++ b/src/lib/skeletons/Events/EdgeEvent.ts
@@ -0,0 +1,27 @@
+import SkeletonEvent from "./SkeletonEvent";
+import Vertex from "../Circular/Vertex";
+import Vector2d from "../Primitives/Vector2d";
+
+export default class EdgeEvent extends SkeletonEvent {
+ public readonly NextVertex: Vertex;
+ public readonly PreviousVertex: Vertex;
+
+ public override get IsObsolete(): boolean {
+ return this.PreviousVertex.IsProcessed || this.NextVertex.IsProcessed;
+ }
+
+ constructor(point: Vector2d, distance: number, previousVertex: Vertex, nextVertex: Vertex) {
+ super(point, distance);
+
+ this.PreviousVertex = previousVertex;
+ this.NextVertex = nextVertex;
+ }
+
+ public override ToString(): string {
+ return "EdgeEvent [V=" + this.V + ", PreviousVertex="
+ + (this.PreviousVertex !== null ? this.PreviousVertex.Point.ToString() : "null") +
+ ", NextVertex="
+ + (this.NextVertex !== null ? this.NextVertex.Point.ToString() : "null") + ", Distance=" +
+ this.Distance + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/MultiEdgeEvent.ts b/src/lib/skeletons/Events/MultiEdgeEvent.ts
new file mode 100644
index 00000000..e990eb71
--- /dev/null
+++ b/src/lib/skeletons/Events/MultiEdgeEvent.ts
@@ -0,0 +1,17 @@
+import SkeletonEvent from "./SkeletonEvent";
+import Vector2d from "../Primitives/Vector2d";
+import EdgeChain from "./Chains/EdgeChain";
+
+export default class MultiEdgeEvent extends SkeletonEvent {
+ public readonly Chain: EdgeChain;
+
+ public override get IsObsolete(): boolean {
+ return false;
+ }
+
+ constructor(point: Vector2d, distance: number, chain: EdgeChain) {
+ super(point, distance);
+
+ this.Chain = chain;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/MultiSplitEvent.ts b/src/lib/skeletons/Events/MultiSplitEvent.ts
new file mode 100644
index 00000000..48df17bf
--- /dev/null
+++ b/src/lib/skeletons/Events/MultiSplitEvent.ts
@@ -0,0 +1,18 @@
+import SkeletonEvent from "./SkeletonEvent";
+import {List} from "../Utils";
+import IChain from "./Chains/IChain";
+import Vector2d from "../Primitives/Vector2d";
+
+export default class MultiSplitEvent extends SkeletonEvent {
+ public readonly Chains: List;
+
+ public override get IsObsolete(): boolean {
+ return false;
+ }
+
+ constructor(point: Vector2d, distance: number, chains: List) {
+ super(point, distance);
+
+ this.Chains = chains;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/PickEvent.ts b/src/lib/skeletons/Events/PickEvent.ts
new file mode 100644
index 00000000..f27e502d
--- /dev/null
+++ b/src/lib/skeletons/Events/PickEvent.ts
@@ -0,0 +1,17 @@
+import SkeletonEvent from "./SkeletonEvent";
+import Vector2d from "../Primitives/Vector2d";
+import EdgeChain from "./Chains/EdgeChain";
+
+export default class PickEvent extends SkeletonEvent {
+ public readonly Chain: EdgeChain;
+
+ public override get IsObsolete(): boolean {
+ return false;
+ }
+
+ constructor(point: Vector2d, distance: number, chain: EdgeChain) {
+ super(point, distance);
+
+ this.Chain = chain;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/SkeletonEvent.ts b/src/lib/skeletons/Events/SkeletonEvent.ts
new file mode 100644
index 00000000..2f1add90
--- /dev/null
+++ b/src/lib/skeletons/Events/SkeletonEvent.ts
@@ -0,0 +1,22 @@
+import Vector2d from "../Primitives/Vector2d";
+
+export default abstract class SkeletonEvent {
+ public V: Vector2d = null;
+
+ public Distance: number;
+
+ public abstract get IsObsolete(): boolean;
+
+ protected constructor(point: Vector2d, distance: number) {
+ this.V = point;
+ this.Distance = distance;
+ }
+
+ public ToString(): string {
+ return "IntersectEntry [V=" + this.V + ", Distance=" + this.Distance + "]";
+ }
+
+ public GetType(): string {
+ return this.constructor.name;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/SplitEvent.ts b/src/lib/skeletons/Events/SplitEvent.ts
new file mode 100644
index 00000000..c7736dba
--- /dev/null
+++ b/src/lib/skeletons/Events/SplitEvent.ts
@@ -0,0 +1,26 @@
+import SkeletonEvent from "./SkeletonEvent";
+import Edge from "../Circular/Edge";
+import Vertex from "../Circular/Vertex";
+import Vector2d from "../Primitives/Vector2d";
+
+export default class SplitEvent extends SkeletonEvent {
+ public readonly OppositeEdge: Edge = null;
+ public readonly Parent: Vertex = null;
+
+ constructor(point: Vector2d, distance: number, parent: Vertex, oppositeEdge: Edge) {
+ super(point, distance);
+
+ this.Parent = parent;
+ this.OppositeEdge = oppositeEdge;
+ }
+
+ public override get IsObsolete(): boolean {
+ return this.Parent.IsProcessed;
+ }
+
+
+ public override ToString(): string {
+ return "SplitEvent [V=" + this.V + ", Parent=" + (this.Parent !== null ? this.Parent.Point.ToString() : "null") +
+ ", Distance=" + this.Distance + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Events/VertexSplitEvent.ts b/src/lib/skeletons/Events/VertexSplitEvent.ts
new file mode 100644
index 00000000..4ea89d71
--- /dev/null
+++ b/src/lib/skeletons/Events/VertexSplitEvent.ts
@@ -0,0 +1,15 @@
+import SplitEvent from "./SplitEvent";
+import Vector2d from "../Primitives/Vector2d";
+import Vertex from "../Circular/Vertex";
+
+export default class VertexSplitEvent extends SplitEvent {
+ constructor(point: Vector2d, distance: number, parent: Vertex) {
+ super(point, distance, parent, null);
+ }
+
+ public override ToString(): string {
+ return "VertexSplitEvent [V=" + this.V + ", Parent=" +
+ (this.Parent !== null ? this.Parent.Point.ToString() : "null")
+ + ", Distance=" + this.Distance + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/LavUtil.ts b/src/lib/skeletons/LavUtil.ts
new file mode 100644
index 00000000..d067dbbe
--- /dev/null
+++ b/src/lib/skeletons/LavUtil.ts
@@ -0,0 +1,56 @@
+import Vertex from "./Circular/Vertex";
+import {List} from "./Utils";
+import CircularList from "./Circular/CircularList";
+
+export default class LavUtil {
+ public static IsSameLav(v1: Vertex, v2: Vertex): boolean {
+ if (v1.List === null || v2.List === null)
+ return false;
+ return v1.List === v2.List;
+ }
+
+ public static RemoveFromLav(vertex: Vertex) {
+ if (vertex === null || vertex.List === null)
+ return;
+ vertex.Remove();
+ }
+
+ public static CutLavPart(startVertex: Vertex, endVertex: Vertex): List {
+ const ret = new List();
+ const size = startVertex.List.Size;
+ let next = startVertex;
+
+ for (let i = 0; i < size; i++) {
+ const current = next;
+ next = current.Next as Vertex;
+ current.Remove();
+ ret.Add(current);
+
+ if (current === endVertex)
+ return ret;
+ }
+
+ throw new Error("End vertex can't be found in start vertex lav");
+ }
+
+ public static MergeBeforeBaseVertex(base: Vertex, merged: Vertex) {
+ const size = merged.List.Size;
+
+ for (let i = 0; i < size; i++) {
+ const nextMerged = merged.Next as Vertex;
+ nextMerged.Remove();
+
+ base.AddPrevious(nextMerged);
+ }
+ }
+
+ public static MoveAllVertexToLavEnd(vertex: Vertex, newLaw: CircularList) {
+ const size = vertex.List.Size;
+ for (let i = 0; i < size; i++) {
+ const ver = vertex;
+ vertex = vertex.Next as Vertex;
+ ver.Remove();
+ newLaw.AddLast(ver);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Path/FaceNode.ts b/src/lib/skeletons/Path/FaceNode.ts
new file mode 100644
index 00000000..891704fa
--- /dev/null
+++ b/src/lib/skeletons/Path/FaceNode.ts
@@ -0,0 +1,24 @@
+import PathQueueNode from "./PathQueueNode";
+import Vertex from "../Circular/Vertex";
+import FaceQueue from "./FaceQueue";
+
+export class FaceNode extends PathQueueNode {
+ public readonly Vertex: Vertex = null;
+
+ constructor(vertex: Vertex) {
+ super();
+ this.Vertex = vertex;
+ }
+
+ public get FaceQueue(): FaceQueue {
+ return this.List;
+ }
+
+ public get IsQueueUnconnected(): boolean {
+ return this.FaceQueue.IsUnconnected;
+ }
+
+ public QueueClose() {
+ this.FaceQueue.Close();
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Path/FaceQueue.ts b/src/lib/skeletons/Path/FaceQueue.ts
new file mode 100644
index 00000000..ae6d7a11
--- /dev/null
+++ b/src/lib/skeletons/Path/FaceQueue.ts
@@ -0,0 +1,24 @@
+import PathQueue from "./PathQueue";
+import {FaceNode} from "./FaceNode";
+import PathQueueNode from "./PathQueueNode";
+import Edge from "../Circular/Edge";
+
+export default class FaceQueue extends PathQueue {
+ public Edge: Edge = null;
+ public Closed: boolean = false;
+
+ public get IsUnconnected(): boolean {
+ return this.Edge === null;
+ }
+
+ public override AddPush(node: PathQueueNode, newNode: PathQueueNode) {
+ if (this.Closed)
+ throw new Error("Can't add node to closed FaceQueue");
+
+ super.AddPush(node, newNode);
+ }
+
+ public Close() {
+ this.Closed = true;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Path/FaceQueueUtil.ts b/src/lib/skeletons/Path/FaceQueueUtil.ts
new file mode 100644
index 00000000..d9313c65
--- /dev/null
+++ b/src/lib/skeletons/Path/FaceQueueUtil.ts
@@ -0,0 +1,39 @@
+import {FaceNode} from "./FaceNode";
+
+export default class FaceQueueUtil {
+ public static ConnectQueues(firstFace: FaceNode, secondFace: FaceNode) {
+ if (firstFace.List === null)
+ throw new Error("firstFace.list cannot be null.");
+ if (secondFace.List === null)
+ throw new Error("secondFace.list cannot be null.");
+
+ if (firstFace.List === secondFace.List) {
+ if (!firstFace.IsEnd || !secondFace.IsEnd)
+ throw new Error("try to connect the same list not on end nodes");
+
+ if (firstFace.IsQueueUnconnected || secondFace.IsQueueUnconnected)
+ throw new Error("can't close node queue not conected with edges");
+
+ firstFace.QueueClose();
+ return;
+ }
+
+ if (!firstFace.IsQueueUnconnected && !secondFace.IsQueueUnconnected)
+ throw new Error(
+ "can't connect two diffrent queues if each of them is connected to edge");
+
+ if (!firstFace.IsQueueUnconnected) {
+ const qLeft = secondFace.FaceQueue;
+ this.MoveNodes(firstFace, secondFace);
+ qLeft.Close();
+ } else {
+ const qRight = firstFace.FaceQueue;
+ this.MoveNodes(secondFace, firstFace);
+ qRight.Close();
+ }
+ }
+
+ private static MoveNodes(firstFace: FaceNode, secondFace: FaceNode) {
+ firstFace.AddQueue(secondFace);
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Path/PathQueue.ts b/src/lib/skeletons/Path/PathQueue.ts
new file mode 100644
index 00000000..2e619599
--- /dev/null
+++ b/src/lib/skeletons/Path/PathQueue.ts
@@ -0,0 +1,103 @@
+import PathQueueNode from "./PathQueueNode";
+
+export default class PathQueue> {
+ public Size: number = 0;
+ public First: PathQueueNode = null;
+
+ public AddPush(node: PathQueueNode, newNode: PathQueueNode) {
+ if (newNode.List !== null)
+ throw new Error("Node is already assigned to different list!");
+
+ if (node.Next !== null && node.Previous !== null)
+ throw new Error("Can't push new node. Node is inside a Quere. " +
+ "New node can by added only at the end of queue.");
+
+ newNode.List = this;
+ this.Size++;
+
+ if (node.Next === null) {
+ newNode.Previous = node;
+ newNode.Next = null;
+
+ node.Next = newNode;
+ } else {
+ newNode.Previous = null;
+ newNode.Next = node;
+
+ node.Previous = newNode;
+ }
+ }
+
+ public AddFirst(node: T) {
+ if (node.List !== null)
+ throw new Error("Node is already assigned to different list!");
+
+ if (this.First === null) {
+ this.First = node;
+
+ node.List = this;
+ node.Next = null;
+ node.Previous = null;
+
+ this.Size++;
+ } else
+ throw new Error("First element already exist!");
+ }
+
+ public Pop(node: PathQueueNode): PathQueueNode {
+ if (node.List !== this)
+ throw new Error("Node is not assigned to this list!");
+
+ if (this.Size <= 0)
+ throw new Error("List is empty can't remove!");
+
+ if (!node.IsEnd)
+ throw new Error("Can pop only from end of queue!");
+
+ node.List = null;
+
+ let previous: PathQueueNode = null;
+
+ if (this.Size === 1)
+ this.First = null;
+ else {
+ if (this.First === node) {
+ if (node.Next !== null)
+ this.First = node.Next;
+ else if (node.Previous !== null)
+ this.First = node.Previous;
+ else
+ throw new Error("Ups ?");
+ }
+ if (node.Next !== null) {
+ node.Next.Previous = null;
+ previous = node.Next;
+ } else if (node.Previous !== null) {
+ node.Previous.Next = null;
+ previous = node.Previous;
+ }
+ }
+
+ node.Previous = null;
+ node.Next = null;
+
+ this.Size--;
+
+ return previous;
+ }
+
+ public* Iterate(): Generator {
+ let current: T = (this.First !== null ? this.First.FindEnd() : null);
+ let i = 0;
+
+ while (current !== null)
+ {
+ yield current;
+
+ if (++i === this.Size)
+ return;
+
+ current = current.Next;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Path/PathQueueNode.ts b/src/lib/skeletons/Path/PathQueueNode.ts
new file mode 100644
index 00000000..c1ec25de
--- /dev/null
+++ b/src/lib/skeletons/Path/PathQueueNode.ts
@@ -0,0 +1,51 @@
+import PathQueue from "./PathQueue";
+
+export default class PathQueueNode> {
+ public List: PathQueue = null;
+ public Next: PathQueueNode = null;
+ public Previous: PathQueueNode = null;
+
+ public get IsEnd(): boolean {
+ return this.Next === null || this.Previous === null;
+ }
+
+ public AddPush(node: PathQueueNode) {
+ this.List.AddPush(this, node);
+ }
+
+ public AddQueue(queue: PathQueueNode): PathQueueNode {
+ if (this.List === queue.List)
+ return null;
+
+ let currentQueue: PathQueueNode = this;
+
+ let current = queue;
+
+ while (current !== null) {
+ const next = current.Pop();
+
+ currentQueue.AddPush(current);
+ currentQueue = current;
+
+ current = next;
+ }
+
+ return currentQueue;
+ }
+
+ public FindEnd(): PathQueueNode {
+ if (this.IsEnd)
+ return this;
+
+ let current: PathQueueNode = this;
+
+ while (current.Previous !== null)
+ current = current.Previous;
+
+ return current;
+ }
+
+ public Pop(): PathQueueNode {
+ return this.List.Pop(this);
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Primitives/LineLinear2d.ts b/src/lib/skeletons/Primitives/LineLinear2d.ts
new file mode 100644
index 00000000..fbefcc07
--- /dev/null
+++ b/src/lib/skeletons/Primitives/LineLinear2d.ts
@@ -0,0 +1,41 @@
+import Vector2d from "./Vector2d";
+
+export default class LineLinear2d {
+ public A: number;
+ public B: number;
+ public C: number;
+
+ constructor(pP1: Vector2d = Vector2d.Empty, pP2: Vector2d = Vector2d.Empty) {
+ this.A = pP1.Y - pP2.Y;
+ this.B = pP2.X - pP1.X;
+ this.C = pP1.X * pP2.Y - pP2.X * pP1.Y;
+ }
+
+ public SetFromCoefficients(a: number, b: number, c: number): LineLinear2d {
+ this.A = a;
+ this.B = b;
+ this.C = c;
+
+ return this;
+ }
+
+ public Collide(pLine: LineLinear2d): Vector2d {
+ return LineLinear2d.Collide(this, pLine);
+ }
+
+ public static Collide(pLine1: LineLinear2d, pLine2: LineLinear2d): Vector2d {
+ return LineLinear2d.CollideCoeff(pLine1.A, pLine1.B, pLine1.C, pLine2.A, pLine2.B, pLine2.C);
+ }
+
+ public static CollideCoeff(A1: number, B1: number, C1: number, A2: number, B2: number, C2: number): Vector2d {
+ const WAB = A1 * B2 - A2 * B1;
+ const WBC = B1 * C2 - B2 * C1;
+ const WCA = C1 * A2 - C2 * A1;
+
+ return WAB === 0 ? Vector2d.Empty : new Vector2d(WBC / WAB, WCA / WAB);
+ }
+
+ public Contains(point: Vector2d): boolean {
+ return Math.abs((point.X * this.A + point.Y * this.B + this.C)) < Number.EPSILON;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Primitives/LineParametric2d.ts b/src/lib/skeletons/Primitives/LineParametric2d.ts
new file mode 100644
index 00000000..ed70353f
--- /dev/null
+++ b/src/lib/skeletons/Primitives/LineParametric2d.ts
@@ -0,0 +1,47 @@
+import Vector2d from "./Vector2d";
+import LineLinear2d from "./LineLinear2d";
+import PrimitiveUtils from "./PrimitiveUtils";
+
+export default class LineParametric2d {
+ public static readonly Empty: LineParametric2d = new LineParametric2d(Vector2d.Empty, Vector2d.Empty);
+
+ public A: Vector2d = null;
+ public U: Vector2d = null;
+
+ constructor(pA: Vector2d, pU: Vector2d) {
+ this.A = pA;
+ this.U = pU;
+ }
+
+ public CreateLinearForm(): LineLinear2d {
+ const x = this.A.X;
+ const y = this.A.Y;
+
+ const B = -this.U.X;
+ const A = this.U.Y;
+
+ const C = -(A * x + B * y);
+
+ return new LineLinear2d().SetFromCoefficients(A, B, C);
+ }
+
+ public static Collide(ray: LineParametric2d, line: LineLinear2d, epsilon: number): Vector2d {
+ const collide = LineLinear2d.Collide(ray.CreateLinearForm(), line);
+ if (collide.Equals(Vector2d.Empty)) {
+ return Vector2d.Empty;
+ }
+
+ const collideVector = collide.Sub(ray.A);
+ return ray.U.Dot(collideVector) < epsilon ? Vector2d.Empty : collide;
+ }
+
+ public IsOnLeftSite(point: Vector2d, epsilon: number): boolean {
+ const direction = point.Sub(this.A);
+ return PrimitiveUtils.OrthogonalRight(this.U).Dot(direction) < epsilon;
+ }
+
+ public IsOnRightSite(point: Vector2d, epsilon: number): boolean {
+ const direction = point.Sub(this.A);
+ return PrimitiveUtils.OrthogonalRight(this.U).Dot(direction) > -epsilon;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Primitives/PrimitiveUtils.ts b/src/lib/skeletons/Primitives/PrimitiveUtils.ts
new file mode 100644
index 00000000..0520026d
--- /dev/null
+++ b/src/lib/skeletons/Primitives/PrimitiveUtils.ts
@@ -0,0 +1,241 @@
+import Vector2d from "./Vector2d";
+import LineParametric2d from "./LineParametric2d";
+import {List} from "../Utils";
+
+class IntersectPoints {
+ public readonly Intersect: Vector2d = null;
+ public readonly IntersectEnd: Vector2d = null;
+
+ constructor(intersect?: Vector2d, intersectEnd?: Vector2d) {
+ if (!intersect) {
+ intersect = Vector2d.Empty;
+ }
+
+ if (!intersectEnd) {
+ intersectEnd = Vector2d.Empty;
+ }
+
+ this.Intersect = intersect;
+ this.IntersectEnd = intersectEnd;
+ }
+}
+
+
+export default class PrimitiveUtils {
+ public static FromTo(begin: Vector2d, end: Vector2d): Vector2d {
+ return new Vector2d(end.X - begin.X, end.Y - begin.Y);
+ }
+
+ public static OrthogonalLeft(v: Vector2d): Vector2d {
+ return new Vector2d(-v.Y, v.X);
+ }
+
+ public static OrthogonalRight(v: Vector2d): Vector2d {
+ return new Vector2d(v.Y, -v.X);
+ }
+
+ public static OrthogonalProjection(unitVector: Vector2d, vectorToProject: Vector2d): Vector2d {
+ const n = new Vector2d(unitVector.X, unitVector.Y).Normalized();
+
+ const px = vectorToProject.X;
+ const py = vectorToProject.Y;
+
+ const ax = n.X;
+ const ay = n.Y;
+
+ return new Vector2d(px * ax * ax + py * ax * ay, px * ax * ay + py * ay * ay);
+ }
+
+ public static BisectorNormalized(norm1: Vector2d, norm2: Vector2d): Vector2d {
+ const e1v = PrimitiveUtils.OrthogonalLeft(norm1);
+ const e2v = PrimitiveUtils.OrthogonalLeft(norm2);
+
+ if (norm1.Dot(norm2) > 0)
+ return e1v.Add(e2v);
+
+ let ret = new Vector2d(norm1.X, norm1.Y);
+ ret.Negate();
+ ret = ret.Add(norm2);
+
+ if (e1v.Dot(norm2) < 0)
+ ret.Negate();
+
+ return ret;
+ }
+
+ private static readonly SmallNum = 0.00000001;
+
+ private static readonly Empty: IntersectPoints = new IntersectPoints();
+
+ public static IsPointOnRay(point: Vector2d, ray: LineParametric2d, epsilon: number): boolean {
+ const rayDirection = new Vector2d(ray.U.X, ray.U.Y).Normalized();
+
+ const pointVector = point.Sub(ray.A);
+
+ let dot = rayDirection.Dot(pointVector);
+
+ if (dot < epsilon)
+ return false;
+
+ const x = rayDirection.X;
+ rayDirection.X = rayDirection.Y;
+ rayDirection.Y = -x;
+
+ dot = rayDirection.Dot(pointVector);
+
+ return -epsilon < dot && dot < epsilon;
+ }
+
+ public static IntersectRays2D(r1: LineParametric2d, r2: LineParametric2d): IntersectPoints {
+ const s1p0 = r1.A;
+ const s1p1 = r1.A.Add(r1.U);
+
+ const s2p0 = r2.A;
+
+ const u = r1.U;
+ const v = r2.U;
+
+ const w = s1p0.Sub(s2p0);
+ const d = PrimitiveUtils.Perp(u, v);
+
+ if (Math.abs(d) < PrimitiveUtils.SmallNum) {
+ if (PrimitiveUtils.Perp(u, w) !== 0 || PrimitiveUtils.Perp(v, w) !== 0)
+ return PrimitiveUtils.Empty;
+
+ const du = PrimitiveUtils.Dot(u, u);
+ const dv = PrimitiveUtils.Dot(v, v);
+
+ if (du === 0 && dv === 0) {
+ if (s1p0.NotEquals(s2p0))
+ return PrimitiveUtils.Empty;
+
+ return new IntersectPoints(s1p0);
+ }
+ if (du === 0) {
+ if (!PrimitiveUtils.InCollinearRay(s1p0, s2p0, v))
+ return PrimitiveUtils.Empty;
+
+ return new IntersectPoints(s1p0);
+ }
+ if (dv === 0) {
+ if (!PrimitiveUtils.InCollinearRay(s2p0, s1p0, u))
+ return PrimitiveUtils.Empty;
+
+ return new IntersectPoints(s2p0);
+ }
+
+ let t0, t1;
+ var w2 = s1p1.Sub(s2p0);
+ if (v.X !== 0) {
+ t0 = w.X / v.X;
+ t1 = w2.X / v.X;
+ } else {
+ t0 = w.Y / v.Y;
+ t1 = w2.Y / v.Y;
+ }
+ if (t0 > t1) {
+ const t = t0;
+ t0 = t1;
+ t1 = t;
+ }
+ if (t1 < 0)
+ return PrimitiveUtils.Empty;
+
+ t0 = t0 < 0 ? 0 : t0;
+
+ if (t0 === t1) {
+ let I0 = new Vector2d(v.X, v.Y);
+ I0 = I0.MultiplyScalar(t0);
+ I0 = I0.Add(s2p0);
+
+ return new IntersectPoints(I0);
+ }
+
+ let I_0 = new Vector2d(v.X, v.Y);
+ I_0 = I_0.MultiplyScalar(t0);
+ I_0 = I_0.Add(s2p0);
+
+ let I1 = new Vector2d(v.X, v.Y);
+ I1 = I1.MultiplyScalar(t1);
+ I1 = I1.Add(s2p0);
+
+ return new IntersectPoints(I_0, I1);
+ }
+
+ const sI = PrimitiveUtils.Perp(v, w) / d;
+ if (sI < 0 /* || sI > 1 */)
+ return PrimitiveUtils.Empty;
+
+ const tI = PrimitiveUtils.Perp(u, w) / d;
+ if (tI < 0 /* || tI > 1 */)
+ return PrimitiveUtils.Empty;
+
+ let IO = new Vector2d(u.X, u.Y);
+ IO = IO.MultiplyScalar(sI);
+ IO = IO.Add(s1p0);
+
+ return new IntersectPoints(IO);
+ }
+
+ private static InCollinearRay(p: Vector2d, rayStart: Vector2d, rayDirection: Vector2d): boolean {
+ const collideVector = p.Sub(rayStart);
+ const dot = rayDirection.Dot(collideVector);
+
+ return !(dot < 0);
+ }
+
+ private static Dot(u: Vector2d, v: Vector2d): number {
+ return u.Dot(v);
+ }
+
+ private static Perp(u: Vector2d, v: Vector2d): number {
+ return u.X * v.Y - u.Y * v.X;
+ }
+
+ public static IsClockwisePolygon(polygon: List): boolean {
+ return PrimitiveUtils.Area(polygon) < 0;
+ }
+
+ private static Area(polygon: List): number {
+ const n = polygon.Count;
+ let A = 0;
+ for (let p = n - 1, q = 0; q < n; p = q++)
+ A += polygon[p].X * polygon[q].Y - polygon[q].X * polygon[p].Y;
+
+ return A * 0.5;
+ }
+
+ public static MakeCounterClockwise(polygon: List): List {
+ if (PrimitiveUtils.IsClockwisePolygon(polygon))
+ polygon.Reverse();
+
+ return polygon;
+ }
+
+ public static IsPointInsidePolygon(point: Vector2d, points: List): boolean {
+ const numpoints = points.Count;
+
+ if (numpoints < 3)
+ return false;
+
+ let it = 0;
+ const first = points[it];
+ let oddNodes = false;
+
+ for (let i = 0; i < numpoints; i++) {
+ const node1 = points[it];
+ it++;
+ const node2 = i === numpoints - 1 ? first : points[it];
+
+ const x = point.X;
+ const y = point.Y;
+
+ if (node1.Y < y && node2.Y >= y || node2.Y < y && node1.Y >= y) {
+ if (node1.X + (y - node1.Y) / (node2.Y - node1.Y) * (node2.X - node1.X) < x)
+ oddNodes = !oddNodes;
+ }
+ }
+
+ return oddNodes;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Primitives/PriorityQueue.ts b/src/lib/skeletons/Primitives/PriorityQueue.ts
new file mode 100644
index 00000000..5010defa
--- /dev/null
+++ b/src/lib/skeletons/Primitives/PriorityQueue.ts
@@ -0,0 +1,63 @@
+import {IComparer, List} from "../Utils";
+
+export default class PriorityQueue {
+ private readonly _comparer: IComparer = null;
+ private readonly _heap: List = null;
+
+ constructor(capacity: number, comparer: IComparer) {
+ this._heap = new List(capacity);
+ this._comparer = comparer;
+ }
+
+ public Clear() {
+ this._heap.Clear();
+ }
+
+ public Add(item: T) {
+ let n = this._heap.Count;
+ this._heap.Add(item);
+ while (n !== 0) {
+ const p = Math.floor(n / 2);
+ if (this._comparer.Compare(this._heap[n], (this._heap[p])) >= 0) break;
+ const tmp: T = this._heap[n];
+ this._heap[n] = this._heap[p];
+ this._heap[p] = tmp;
+ n = p;
+ }
+ }
+
+ get Count(): number {
+ return this._heap.Count;
+ }
+
+ get Empty(): boolean {
+ return this._heap.Count === 0;
+ }
+
+ public Peek(): T {
+ return !this._heap.Any() ? null : this._heap[0];
+ }
+
+ public Next(): T {
+ const val: T = this._heap[0];
+ const nMax = this._heap.Count - 1;
+ this._heap[0] = this._heap[nMax];
+ this._heap.RemoveAt(nMax);
+
+ let p = 0;
+ while (true) {
+ let c = p * 2;
+ if (c >= nMax) break;
+
+ if (c + 1 < nMax && this._comparer.Compare(this._heap[c + 1], this._heap[c]) < 0) c++;
+
+ if (this._comparer.Compare(this._heap[p], (this._heap[c])) <= 0) break;
+
+ const tmp: T = this._heap[p];
+ this._heap[p] = this._heap[c];
+ this._heap[c] = tmp;
+ p = c;
+ }
+ return val;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Primitives/Vector2d.ts b/src/lib/skeletons/Primitives/Vector2d.ts
new file mode 100644
index 00000000..eb2359e1
--- /dev/null
+++ b/src/lib/skeletons/Primitives/Vector2d.ts
@@ -0,0 +1,61 @@
+export default class Vector2d {
+ public static Empty: Vector2d = new Vector2d(Number.MIN_VALUE, Number.MIN_VALUE);
+
+ public X: number = 0;
+ public Y: number = 0;
+
+ constructor(x: number, y: number) {
+ this.X = x;
+ this.Y = y;
+ }
+
+ public Negate() {
+ this.X = -this.X;
+ this.Y = -this.Y;
+ }
+
+ public DistanceTo(var1: Vector2d): number {
+ const var2 = this.X - var1.X;
+ const var4 = this.Y - var1.Y;
+ return Math.sqrt(var2 * var2 + var4 * var4);
+ }
+
+ public Normalized(): Vector2d {
+ const var1 = 1 / Math.sqrt(this.X * this.X + this.Y * this.Y);
+ return new Vector2d(this.X * var1, this.Y * var1);
+ }
+
+ public Dot(var1: Vector2d): number {
+ return this.X * var1.X + this.Y * var1.Y;
+ }
+
+ public DistanceSquared(var1: Vector2d): number {
+ const var2 = this.X - var1.X;
+ const var4 = this.Y - var1.Y;
+ return var2 * var2 + var4 * var4;
+ }
+
+ public Add(v: Vector2d): Vector2d {
+ return new Vector2d(this.X + v.X, this.Y + v.Y);
+ }
+
+ public Sub(v: Vector2d): Vector2d {
+ return new Vector2d(this.X - v.X, this.Y - v.Y);
+ }
+
+ public MultiplyScalar(scale: number): Vector2d {
+ return new Vector2d(this.X * scale, this.Y * scale);
+ }
+
+ public Equals(v: Vector2d): boolean {
+ return this.X === v.X && this.Y === v.Y;
+ }
+
+ public NotEquals(v: Vector2d): boolean {
+ return !this.Equals(v);
+ }
+
+ public ToString(): string {
+ return `${this.X}, ${this.Y}`;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/Skeleton.ts b/src/lib/skeletons/Skeleton.ts
new file mode 100644
index 00000000..6f013afd
--- /dev/null
+++ b/src/lib/skeletons/Skeleton.ts
@@ -0,0 +1,13 @@
+import Vector2d from "./Primitives/Vector2d";
+import EdgeResult from "./EdgeResult";
+import {Dictionary, List} from "./Utils";
+
+export class Skeleton {
+ public readonly Edges: List = null;
+ public readonly Distances: Dictionary = null;
+
+ constructor(edges: List, distances: Dictionary) {
+ this.Edges = edges;
+ this.Distances = distances;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/SkeletonBuilder.ts b/src/lib/skeletons/SkeletonBuilder.ts
new file mode 100644
index 00000000..01ddb95b
--- /dev/null
+++ b/src/lib/skeletons/SkeletonBuilder.ts
@@ -0,0 +1,994 @@
+import {Skeleton} from "./Skeleton";
+import {HashSet, List, IComparer, Dictionary, GeoJSONMultipolygon} from "./Utils";
+import Vector2d from "./Primitives/Vector2d";
+import PriorityQueue from "./Primitives/PriorityQueue";
+import Edge from "./Circular/Edge";
+import Vertex from "./Circular/Vertex";
+import CircularList from "./Circular/CircularList";
+import FaceQueue from "./Path/FaceQueue";
+import SkeletonEvent from "./Events/SkeletonEvent";
+import FaceQueueUtil from "./Path/FaceQueueUtil";
+import LavUtil from "./LavUtil";
+import IChain from "./Events/Chains/IChain";
+import PrimitiveUtils from "./Primitives/PrimitiveUtils";
+import LineParametric2d from "./Primitives/LineParametric2d";
+import {FaceNode} from "./Path/FaceNode";
+import MultiEdgeEvent from "./Events/MultiEdgeEvent";
+import EdgeEvent from "./Events/EdgeEvent";
+import PickEvent from "./Events/PickEvent";
+import MultiSplitEvent from "./Events/MultiSplitEvent";
+import SingleEdgeChain from "./Events/Chains/SingleEdgeChain";
+import SplitChain from "./Events/Chains/SplitChain";
+import SplitEvent from "./Events/SplitEvent";
+import VertexSplitEvent from "./Events/VertexSplitEvent";
+import EdgeChain from "./Events/Chains/EdgeChain";
+import LineLinear2d from "./Primitives/LineLinear2d";
+import EdgeResult from "./EdgeResult";
+import ChainType from "./Events/Chains/ChainType";
+
+export default class SkeletonBuilder {
+ private static readonly SplitEpsilon = 1e-10;
+
+ public static BuildFromGeoJSON(multipolygon: GeoJSONMultipolygon): Skeleton {
+ const allEdges: List = new List();
+ const allDistances: Dictionary = new Dictionary();
+
+ for (const polygon of multipolygon) {
+ if (polygon.length > 0) {
+ const outer = this.ListFromCoordinatesArray(polygon[0]);
+ const holes: List> = new List();
+
+ for (let i = 1; i < polygon.length; i++) {
+ holes.Add(this.ListFromCoordinatesArray(polygon[i]));
+ }
+
+ const skeleton = this.Build(outer, holes);
+
+ for (const edge of skeleton.Edges) {
+ allEdges.Add(edge);
+ }
+
+ for (const [key, distance] of skeleton.Distances.entries()) {
+ allDistances.Add(key, distance);
+ }
+ }
+ }
+
+ return new Skeleton(allEdges, allDistances);
+ }
+
+ private static ListFromCoordinatesArray(arr: [number, number][]): List {
+ const list: List = new List();
+
+ for (const [x, y] of arr) {
+ list.Add(new Vector2d(x, y));
+ }
+
+ return list;
+ }
+
+ public static Build(polygon: List, holes: List> = null): Skeleton {
+ polygon = this.InitPolygon(polygon);
+ holes = this.MakeClockwise(holes);
+
+ const queue = new PriorityQueue(3, new SkeletonEventDistanseComparer());
+ const sLav = new HashSet>();
+ const faces = new List();
+ const edges = new List();
+
+ this.InitSlav(polygon, sLav, edges, faces);
+
+ if (holes !== null) {
+ for (const inner of holes) {
+ this.InitSlav(inner, sLav, edges, faces);
+ }
+ }
+
+ this.InitEvents(sLav, queue, edges);
+
+ let count = 0;
+ while (!queue.Empty) {
+ count = this.AssertMaxNumberOfInteraction(count);
+ const levelHeight = queue.Peek().Distance;
+
+ for (const event of this.LoadAndGroupLevelEvents(queue)) {
+ if (event.IsObsolete)
+ continue;
+
+ if (event instanceof EdgeEvent)
+ throw new Error("All edge@events should be converted to MultiEdgeEvents for given level");
+ if (event instanceof SplitEvent)
+ throw new Error("All split events should be converted to MultiSplitEvents for given level");
+ if (event instanceof MultiSplitEvent)
+ this.MultiSplitEvent(event, sLav, queue, edges);
+ else if (event instanceof PickEvent)
+ this.PickEvent(event);
+ else if (event instanceof MultiEdgeEvent)
+ this.MultiEdgeEvent(event, queue, edges);
+ else
+ throw new Error("Unknown event type: " + event.GetType());
+ }
+
+ this.ProcessTwoNodeLavs(sLav);
+ this.RemoveEventsUnderHeight(queue, levelHeight);
+ this.RemoveEmptyLav(sLav);
+ }
+
+ return this.AddFacesToOutput(faces);
+ }
+
+ private static InitPolygon(polygon: List): List {
+ if (polygon === null)
+ throw new Error("polygon can't be null");
+
+ if (polygon[0].Equals(polygon[polygon.Count - 1]))
+ throw new Error("polygon can't start and end with the same point");
+
+ return this.MakeCounterClockwise(polygon);
+ }
+
+ private static ProcessTwoNodeLavs(sLav: HashSet>) {
+ for (const lav of sLav) {
+ if (lav.Size === 2) {
+ const first = lav.First();
+ const last = first.Next as Vertex;
+
+ FaceQueueUtil.ConnectQueues(first.LeftFace, last.RightFace);
+ FaceQueueUtil.ConnectQueues(first.RightFace, last.LeftFace);
+
+ first.IsProcessed = true;
+ last.IsProcessed = true;
+
+ LavUtil.RemoveFromLav(first);
+ LavUtil.RemoveFromLav(last);
+ }
+ }
+ }
+
+ private static RemoveEmptyLav(sLav: HashSet>) {
+ sLav.RemoveWhere(circularList => circularList.Size === 0);
+ }
+
+ private static MultiEdgeEvent(event: MultiEdgeEvent, queue: PriorityQueue, edges: List) {
+ const center = event.V;
+ const edgeList = event.Chain.EdgeList;
+
+ const previousVertex = event.Chain.PreviousVertex;
+ previousVertex.IsProcessed = true;
+
+ const nextVertex = event.Chain.NextVertex;
+ nextVertex.IsProcessed = true;
+
+ const bisector = this.CalcBisector(center, previousVertex.PreviousEdge, nextVertex.NextEdge);
+ const edgeVertex = new Vertex(center, event.Distance, bisector, previousVertex.PreviousEdge,
+ nextVertex.NextEdge);
+
+ this.AddFaceLeft(edgeVertex, previousVertex);
+
+ this.AddFaceRight(edgeVertex, nextVertex);
+
+ previousVertex.AddPrevious(edgeVertex);
+
+ this.AddMultiBackFaces(edgeList, edgeVertex);
+
+ this.ComputeEvents(edgeVertex, queue, edges);
+ }
+
+ private static AddMultiBackFaces(edgeList: List, edgeVertex: Vertex) {
+ for (const edgeEvent of edgeList) {
+ const leftVertex = edgeEvent.PreviousVertex;
+ leftVertex.IsProcessed = true;
+ LavUtil.RemoveFromLav(leftVertex);
+
+ const rightVertex = edgeEvent.NextVertex;
+ rightVertex.IsProcessed = true;
+ LavUtil.RemoveFromLav(rightVertex);
+
+ this.AddFaceBack(edgeVertex, leftVertex, rightVertex);
+ }
+ }
+
+ private static PickEvent(event: PickEvent) {
+ const center = event.V;
+ const edgeList = event.Chain.EdgeList;
+
+ const vertex = new Vertex(center, event.Distance, LineParametric2d.Empty, null, null);
+ vertex.IsProcessed = true;
+
+ this.AddMultiBackFaces(edgeList, vertex);
+ }
+
+ private static MultiSplitEvent(event: MultiSplitEvent, sLav: HashSet>, queue: PriorityQueue, edges: List) {
+ const chains = event.Chains;
+ const center = event.V;
+
+ this.CreateOppositeEdgeChains(sLav, chains, center);
+
+ chains.Sort(new ChainComparer(center));
+
+ let lastFaceNode: FaceNode = null;
+
+ let edgeListSize = chains.Count;
+ for (let i = 0; i < edgeListSize; i++) {
+ const chainBegin = chains[i];
+ const chainEnd = chains[(i + 1) % edgeListSize];
+
+ const newVertex = this.CreateMultiSplitVertex(chainBegin.NextEdge, chainEnd.PreviousEdge, center, event.Distance);
+
+ const beginNextVertex = chainBegin.NextVertex;
+ const endPreviousVertex = chainEnd.PreviousVertex;
+
+ this.CorrectBisectorDirection(newVertex.Bisector, beginNextVertex, endPreviousVertex, chainBegin.NextEdge, chainEnd.PreviousEdge);
+
+ if (LavUtil.IsSameLav(beginNextVertex, endPreviousVertex)) {
+ const lavPart = LavUtil.CutLavPart(beginNextVertex, endPreviousVertex);
+
+ const lav = new CircularList();
+ sLav.Add(lav);
+ lav.AddLast(newVertex);
+ for (const vertex of lavPart)
+ lav.AddLast(vertex);
+ } else {
+ LavUtil.MergeBeforeBaseVertex(beginNextVertex, endPreviousVertex);
+ endPreviousVertex.AddNext(newVertex);
+ }
+
+ this.ComputeEvents(newVertex, queue, edges);
+ lastFaceNode = this.AddSplitFaces(lastFaceNode, chainBegin, chainEnd, newVertex);
+ }
+
+ edgeListSize = chains.Count;
+ for (let i = 0; i < edgeListSize; i++) {
+ const chainBegin = chains[i];
+ const chainEnd = chains[(i + 1) % edgeListSize];
+
+ LavUtil.RemoveFromLav(chainBegin.CurrentVertex);
+ LavUtil.RemoveFromLav(chainEnd.CurrentVertex);
+
+ if (chainBegin.CurrentVertex !== null)
+ chainBegin.CurrentVertex.IsProcessed = true;
+ if (chainEnd.CurrentVertex !== null)
+ chainEnd.CurrentVertex.IsProcessed = true;
+ }
+ }
+
+ private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) {
+ const beginEdge2 = beginNextVertex.PreviousEdge;
+ const endEdge2 = endPreviousVertex.NextEdge;
+
+ if (beginEdge !== beginEdge2 || endEdge !== endEdge2)
+ throw new Error();
+
+ if (beginEdge.Norm.Dot(endEdge.Norm) < -0.97) {
+ const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized();
+ const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized();
+ const bisectorPrediction = this.CalcVectorBisector(n1, n2);
+
+ if (bisector.U.Dot(bisectorPrediction) < 0)
+ bisector.U.Negate();
+ }
+ }
+
+ private static AddSplitFaces(lastFaceNode: FaceNode, chainBegin: IChain, chainEnd: IChain, newVertex: Vertex): FaceNode {
+ if (chainBegin instanceof SingleEdgeChain) {
+ if (lastFaceNode === null) {
+ const beginVertex = this.CreateOppositeEdgeVertex(newVertex);
+
+ newVertex.RightFace = beginVertex.RightFace;
+ lastFaceNode = beginVertex.LeftFace;
+ } else {
+ if (newVertex.RightFace !== null)
+ throw new Error("newVertex.RightFace should be null");
+
+ newVertex.RightFace = lastFaceNode;
+ lastFaceNode = null;
+ }
+ } else {
+ const beginVertex = chainBegin.CurrentVertex;
+ this.AddFaceRight(newVertex, beginVertex);
+ }
+
+ if (chainEnd instanceof SingleEdgeChain) {
+ if (lastFaceNode === null) {
+ const endVertex = this.CreateOppositeEdgeVertex(newVertex);
+
+ newVertex.LeftFace = endVertex.LeftFace;
+ lastFaceNode = endVertex.LeftFace;
+ } else {
+ if (newVertex.LeftFace !== null)
+ throw new Error("newVertex.LeftFace should be null.");
+ newVertex.LeftFace = lastFaceNode;
+
+ lastFaceNode = null;
+ }
+ } else {
+ const endVertex = chainEnd.CurrentVertex;
+ this.AddFaceLeft(newVertex, endVertex);
+ }
+ return lastFaceNode;
+ }
+
+ private static CreateOppositeEdgeVertex(newVertex: Vertex): Vertex {
+ const vertex = new Vertex(newVertex.Point, newVertex.Distance, newVertex.Bisector, newVertex.PreviousEdge, newVertex.NextEdge);
+
+ const fn = new FaceNode(vertex);
+ vertex.LeftFace = fn;
+ vertex.RightFace = fn;
+
+ const rightFace = new FaceQueue();
+ rightFace.AddFirst(fn);
+
+ return vertex;
+ }
+
+ private static CreateOppositeEdgeChains(sLav: HashSet>, chains: List, center: Vector2d) {
+ const oppositeEdges = new HashSet();
+
+ const oppositeEdgeChains = new List();
+ const chainsForRemoval = new List();
+
+ for (const chain of chains) {
+ if (chain instanceof SplitChain) {
+ const splitChain = chain;
+ const oppositeEdge = splitChain.OppositeEdge;
+
+ if (oppositeEdge !== null && !oppositeEdges.Contains(oppositeEdge)) {
+ const nextVertex = this.FindOppositeEdgeLav(sLav, oppositeEdge, center);
+
+ if (nextVertex !== null)
+ oppositeEdgeChains.Add(new SingleEdgeChain(oppositeEdge, nextVertex));
+ else {
+ this.FindOppositeEdgeLav(sLav, oppositeEdge, center);
+ chainsForRemoval.Add(chain);
+ }
+ oppositeEdges.Add(oppositeEdge);
+ }
+ }
+ }
+
+ for (let chain of chainsForRemoval)
+ chains.Remove(chain);
+
+ chains.AddRange(oppositeEdgeChains);
+ }
+
+ private static CreateMultiSplitVertex(nextEdge: Edge, previousEdge: Edge, center: Vector2d, distance: number): Vertex {
+ const bisector = this.CalcBisector(center, previousEdge, nextEdge);
+ return new Vertex(center, distance, bisector, previousEdge, nextEdge);
+ }
+
+ private static CreateChains(cluster: List): List {
+ const edgeCluster = new List();
+ const splitCluster = new List();
+ const vertexEventsParents = new HashSet();
+
+ for (const skeletonEvent of cluster) {
+ if (skeletonEvent instanceof EdgeEvent)
+ edgeCluster.Add(skeletonEvent);
+ else {
+ if (skeletonEvent instanceof VertexSplitEvent) {
+
+ } else if (skeletonEvent instanceof SplitEvent) {
+ const splitEvent = skeletonEvent;
+ vertexEventsParents.Add(splitEvent.Parent);
+ splitCluster.Add(splitEvent);
+ }
+ }
+ }
+
+ for (let skeletonEvent of cluster) {
+ if (skeletonEvent instanceof VertexSplitEvent) {
+ const vertexEvent = skeletonEvent;
+ if (!vertexEventsParents.Contains(vertexEvent.Parent)) {
+ vertexEventsParents.Add(vertexEvent.Parent);
+ splitCluster.Add(vertexEvent);
+ }
+ }
+ }
+
+ const edgeChains = new List();
+
+ while (edgeCluster.Count > 0)
+ edgeChains.Add(new EdgeChain(this.CreateEdgeChain(edgeCluster)));
+
+ const chains = new List(edgeChains.Count);
+ for (const edgeChain of edgeChains)
+ chains.Add(edgeChain);
+
+ splitEventLoop:
+ while (splitCluster.Any()) {
+ const split = splitCluster[0];
+ splitCluster.RemoveAt(0);
+
+ for (const chain of edgeChains) {
+ if (this.IsInEdgeChain(split, chain))
+ continue splitEventLoop; //goto splitEventLoop;
+ }
+
+ chains.Add(new SplitChain(split));
+ }
+
+ return chains;
+ }
+
+ private static IsInEdgeChain(split: SplitEvent, chain: EdgeChain): boolean {
+ const splitParent = split.Parent;
+ const edgeList = chain.EdgeList;
+
+ return edgeList.Any(edgeEvent => edgeEvent.PreviousVertex === splitParent || edgeEvent.NextVertex === splitParent);
+ }
+
+ private static CreateEdgeChain(edgeCluster: List): List {
+ const edgeList = new List();
+
+ edgeList.Add(edgeCluster[0]);
+ edgeCluster.RemoveAt(0);
+
+ loop:
+ for (; ;) {
+ const beginVertex = edgeList[0].PreviousVertex;
+ const endVertex = edgeList[edgeList.Count - 1].NextVertex;
+
+ for (let i = 0; i < edgeCluster.Count; i++) {
+ const edge = edgeCluster[i];
+ if (edge.PreviousVertex === endVertex) {
+ edgeCluster.RemoveAt(i);
+ edgeList.Add(edge);
+ //goto loop;
+ continue loop;
+
+ }
+ if (edge.NextVertex === beginVertex) {
+ edgeCluster.RemoveAt(i);
+ edgeList.Insert(0, edge);
+ //goto loop;
+ continue loop;
+ }
+ }
+ break;
+ }
+
+ return edgeList;
+ }
+
+ private static RemoveEventsUnderHeight(queue: PriorityQueue, levelHeight: number) {
+ while (!queue.Empty) {
+ if (queue.Peek().Distance > levelHeight + this.SplitEpsilon)
+ break;
+ queue.Next();
+ }
+ }
+
+ private static LoadAndGroupLevelEvents(queue: PriorityQueue): List {
+ const levelEvents = this.LoadLevelEvents(queue);
+ return this.GroupLevelEvents(levelEvents);
+ }
+
+ private static GroupLevelEvents(levelEvents: List): List {
+ const ret = new List();
+
+ const parentGroup = new HashSet();
+
+ while (levelEvents.Count > 0) {
+ parentGroup.Clear();
+
+ const event = levelEvents[0];
+ levelEvents.RemoveAt(0);
+ const eventCenter = event.V;
+ const distance = event.Distance;
+
+ this.AddEventToGroup(parentGroup, event);
+
+ const cluster = new List();
+ cluster.Add(event);
+
+ for (let j = 0; j < levelEvents.Count; j++) {
+ const test = levelEvents[j];
+
+ if (this.IsEventInGroup(parentGroup, test)) {
+ const item = levelEvents[j];
+ levelEvents.RemoveAt(j);
+ cluster.Add(item);
+ this.AddEventToGroup(parentGroup, test);
+ j--;
+ } else if (eventCenter.DistanceTo(test.V) < this.SplitEpsilon) {
+ const item = levelEvents[j];
+ levelEvents.RemoveAt(j);
+ cluster.Add(item);
+ this.AddEventToGroup(parentGroup, test);
+ j--;
+ }
+ }
+
+ ret.Add(this.CreateLevelEvent(eventCenter, distance, cluster));
+ }
+ return ret;
+ }
+
+ private static IsEventInGroup(parentGroup: HashSet, event: SkeletonEvent): boolean {
+ if (event instanceof SplitEvent)
+ return parentGroup.Contains((event).Parent);
+ if (event instanceof EdgeEvent)
+ return parentGroup.Contains((event).PreviousVertex)
+ || parentGroup.Contains((event).NextVertex);
+ return false;
+ }
+
+ private static AddEventToGroup(parentGroup: HashSet, event: SkeletonEvent) {
+ if (event instanceof SplitEvent)
+ parentGroup.Add((event).Parent);
+ else if (event instanceof EdgeEvent) {
+ parentGroup.Add((event).PreviousVertex);
+ parentGroup.Add((event).NextVertex);
+ }
+ }
+
+ private static CreateLevelEvent(eventCenter: Vector2d, distance: number, eventCluster: List): SkeletonEvent {
+ const chains = this.CreateChains(eventCluster);
+
+ if (chains.Count === 1) {
+ const chain = chains[0];
+ if (chain.ChainType === ChainType.ClosedEdge)
+ return new PickEvent(eventCenter, distance, chain);
+ if (chain.ChainType === ChainType.Edge)
+ return new MultiEdgeEvent(eventCenter, distance, chain);
+ if (chain.ChainType === ChainType.Split)
+ return new MultiSplitEvent(eventCenter, distance, chains);
+ }
+
+ if (chains.Any(chain => chain.ChainType === ChainType.ClosedEdge))
+ throw new Error("Found closed chain of events for single point, but found more then one chain");
+ return new MultiSplitEvent(eventCenter, distance, chains);
+ }
+
+ private static LoadLevelEvents(queue: PriorityQueue): List {
+ const level = new List();
+ let levelStart: SkeletonEvent;
+
+ do {
+ levelStart = queue.Empty ? null : queue.Next();
+ }
+ while (levelStart !== null && levelStart.IsObsolete);
+
+
+ if (levelStart === null || levelStart.IsObsolete)
+ return level;
+
+ const levelStartHeight = levelStart.Distance;
+
+ level.Add(levelStart);
+
+ let event: SkeletonEvent;
+ while ((event = queue.Peek()) !== null &&
+ Math.abs(event.Distance - levelStartHeight) < this.SplitEpsilon) {
+ const nextLevelEvent = queue.Next();
+ if (!nextLevelEvent.IsObsolete)
+ level.Add(nextLevelEvent);
+ }
+ return level;
+ }
+
+ private static AssertMaxNumberOfInteraction(count: number): number {
+ count++;
+ if (count > 10000)
+ throw new Error("Too many interaction: bug?");
+ return count;
+ }
+
+ private static MakeClockwise(holes: List>): List> {
+ if (holes === null)
+ return null;
+
+ const ret = new List>(holes.Count);
+ for (const hole of holes) {
+ if (PrimitiveUtils.IsClockwisePolygon(hole))
+ ret.Add(hole);
+ else {
+ hole.Reverse();
+ ret.Add(hole);
+ }
+ }
+ return ret;
+ }
+
+ private static MakeCounterClockwise(polygon: List): List {
+ return PrimitiveUtils.MakeCounterClockwise(polygon);
+ }
+
+ private static InitSlav(polygon: List, sLav: HashSet>, edges: List, faces: List) {
+ const edgesList = new CircularList();
+
+ const size = polygon.Count;
+ for (let i = 0; i < size; i++) {
+ const j = (i + 1) % size;
+ edgesList.AddLast(new Edge(polygon[i], polygon[j]));
+ }
+
+ for (const edge of edgesList.Iterate()) {
+ const nextEdge = edge.Next as Edge;
+ const bisector = this.CalcBisector(edge.End, edge, nextEdge);
+
+ edge.BisectorNext = bisector;
+ nextEdge.BisectorPrevious = bisector;
+ edges.Add(edge);
+ }
+
+ const lav = new CircularList();
+ sLav.Add(lav);
+
+ for (const edge of edgesList.Iterate()) {
+ const nextEdge = edge.Next as Edge;
+ const vertex = new Vertex(edge.End, 0, edge.BisectorNext, edge, nextEdge);
+ lav.AddLast(vertex);
+ }
+
+ for (const vertex of lav.Iterate()) {
+ const next = vertex.Next as Vertex;
+ const rightFace = new FaceNode(vertex);
+
+ const faceQueue = new FaceQueue();
+ faceQueue.Edge = (vertex.NextEdge);
+
+ faceQueue.AddFirst(rightFace);
+ faces.Add(faceQueue);
+ vertex.RightFace = rightFace;
+
+ const leftFace = new FaceNode(next);
+ rightFace.AddPush(leftFace);
+ next.LeftFace = leftFace;
+ }
+ }
+
+ private static AddFacesToOutput(faces: List): Skeleton {
+ const edgeOutputs = new List();
+ const distances = new Dictionary();
+
+ for (const face of faces) {
+ if (face.Size > 0) {
+ const faceList = new List();
+
+ for (const fn of face.Iterate()) {
+ const point = fn.Vertex.Point;
+
+ faceList.Add(point);
+
+ if (!distances.ContainsKey(point))
+ distances.Add(point, fn.Vertex.Distance);
+ }
+
+ edgeOutputs.Add(new EdgeResult(face.Edge, faceList));
+ }
+ }
+ return new Skeleton(edgeOutputs, distances);
+ }
+
+ private static InitEvents(sLav: HashSet>, queue: PriorityQueue, edges: List) {
+ for (const lav of sLav) {
+ for (const vertex of lav.Iterate())
+ this.ComputeSplitEvents(vertex, edges, queue, -1);
+ }
+
+ for (const lav of sLav) {
+ for (const vertex of lav.Iterate()) {
+ const nextVertex = vertex.Next as Vertex;
+ this.ComputeEdgeEvents(vertex, nextVertex, queue);
+ }
+ }
+ }
+
+ private static ComputeSplitEvents(vertex: Vertex, edges: List, queue: PriorityQueue, distanceSquared: number) {
+ const source = vertex.Point;
+ const oppositeEdges = this.CalcOppositeEdges(vertex, edges);
+
+ for (const oppositeEdge of oppositeEdges) {
+ const point = oppositeEdge.Point;
+
+ if (Math.abs(distanceSquared - (-1)) > this.SplitEpsilon) {
+ if (source.DistanceSquared(point) > distanceSquared + this.SplitEpsilon) {
+ continue;
+ }
+ }
+
+ if (oppositeEdge.OppositePoint.NotEquals(Vector2d.Empty)) {
+ queue.Add(new VertexSplitEvent(point, oppositeEdge.Distance, vertex));
+ continue;
+ }
+ queue.Add(new SplitEvent(point, oppositeEdge.Distance, vertex, oppositeEdge.OppositeEdge));
+ }
+ }
+
+ private static ComputeEvents(vertex: Vertex, queue: PriorityQueue, edges: List) {
+ const distanceSquared = this.ComputeCloserEdgeEvent(vertex, queue);
+ this.ComputeSplitEvents(vertex, edges, queue, distanceSquared);
+ }
+
+ private static ComputeCloserEdgeEvent(vertex: Vertex, queue: PriorityQueue): number {
+ const nextVertex = vertex.Next as Vertex;
+ const previousVertex = vertex.Previous as Vertex;
+
+ const point = vertex.Point;
+
+ const point1 = this.ComputeIntersectionBisectors(vertex, nextVertex);
+ const point2 = this.ComputeIntersectionBisectors(previousVertex, vertex);
+
+ if (point1.Equals(Vector2d.Empty) && point2.Equals(Vector2d.Empty))
+ return -1;
+
+ let distance1 = Number.MAX_VALUE;
+ let distance2 = Number.MAX_VALUE;
+
+ if (point1.NotEquals(Vector2d.Empty))
+ distance1 = point.DistanceSquared(point1);
+ if (point2.NotEquals(Vector2d.Empty))
+ distance2 = point.DistanceSquared(point2);
+
+ if (Math.abs(distance1 - this.SplitEpsilon) < distance2)
+ queue.Add(this.CreateEdgeEvent(point1, vertex, nextVertex));
+ if (Math.abs(distance2 - this.SplitEpsilon) < distance1)
+ queue.Add(this.CreateEdgeEvent(point2, previousVertex, vertex));
+
+ return distance1 < distance2 ? distance1 : distance2;
+ }
+
+ private static CreateEdgeEvent(point: Vector2d, previousVertex: Vertex, nextVertex: Vertex): SkeletonEvent {
+ return new EdgeEvent(point, this.CalcDistance(point, previousVertex.NextEdge), previousVertex, nextVertex);
+ }
+
+ private static ComputeEdgeEvents(previousVertex: Vertex, nextVertex: Vertex, queue: PriorityQueue) {
+ const point = this.ComputeIntersectionBisectors(previousVertex, nextVertex);
+ if (point.NotEquals(Vector2d.Empty))
+ queue.Add(this.CreateEdgeEvent(point, previousVertex, nextVertex));
+ }
+
+ private static CalcOppositeEdges(vertex: Vertex, edges: List): List {
+ const ret = new List();
+
+ for (const edgeEntry of edges) {
+ const edge = edgeEntry.LineLinear2d;
+
+ if (this.EdgeBehindBisector(vertex.Bisector, edge))
+ continue;
+
+ const candidatePoint = this.CalcCandidatePointForSplit(vertex, edgeEntry);
+ if (candidatePoint !== null)
+ ret.Add(candidatePoint);
+ }
+
+ ret.Sort(new SplitCandidateComparer());
+ return ret;
+ }
+
+ private static EdgeBehindBisector(bisector: LineParametric2d, edge: LineLinear2d): boolean {
+ return LineParametric2d.Collide(bisector, edge, this.SplitEpsilon).Equals(Vector2d.Empty);
+ }
+
+ private static CalcCandidatePointForSplit(vertex: Vertex, edge: Edge): SplitCandidate {
+ const vertexEdge = this.ChoseLessParallelVertexEdge(vertex, edge);
+ if (vertexEdge === null)
+ return null;
+
+ const vertexEdteNormNegate = vertexEdge.Norm;
+ const edgesBisector = this.CalcVectorBisector(vertexEdteNormNegate, edge.Norm);
+ const edgesCollide = vertexEdge.LineLinear2d.Collide(edge.LineLinear2d);
+
+ if (edgesCollide.Equals(Vector2d.Empty))
+ throw new Error("Ups this should not happen");
+
+ const edgesBisectorLine = new LineParametric2d(edgesCollide, edgesBisector).CreateLinearForm();
+
+ const candidatePoint = LineParametric2d.Collide(vertex.Bisector, edgesBisectorLine, this.SplitEpsilon);
+
+ if (candidatePoint.Equals(Vector2d.Empty))
+ return null;
+
+ if (edge.BisectorPrevious.IsOnRightSite(candidatePoint, this.SplitEpsilon)
+ && edge.BisectorNext.IsOnLeftSite(candidatePoint, this.SplitEpsilon)) {
+ const distance = this.CalcDistance(candidatePoint, edge);
+
+ if (edge.BisectorPrevious.IsOnLeftSite(candidatePoint, this.SplitEpsilon))
+ return new SplitCandidate(candidatePoint, distance, null, edge.Begin);
+ if (edge.BisectorNext.IsOnRightSite(candidatePoint, this.SplitEpsilon))
+ return new SplitCandidate(candidatePoint, distance, null, edge.Begin);
+
+ return new SplitCandidate(candidatePoint, distance, edge, Vector2d.Empty);
+ }
+
+ return null;
+ }
+
+ private static ChoseLessParallelVertexEdge(vertex: Vertex, edge: Edge): Edge {
+ const edgeA = vertex.PreviousEdge;
+ const edgeB = vertex.NextEdge;
+
+ let vertexEdge = edgeA;
+
+ const edgeADot = Math.abs(edge.Norm.Dot(edgeA.Norm));
+ const edgeBDot = Math.abs(edge.Norm.Dot(edgeB.Norm));
+
+ if (edgeADot + edgeBDot >= 2 - this.SplitEpsilon)
+ return null;
+
+ if (edgeADot > edgeBDot)
+ vertexEdge = edgeB;
+
+ return vertexEdge;
+ }
+
+ private static ComputeIntersectionBisectors(vertexPrevious: Vertex, vertexNext: Vertex): Vector2d {
+ const bisectorPrevious = vertexPrevious.Bisector;
+ const bisectorNext = vertexNext.Bisector;
+
+ const intersectRays2d = PrimitiveUtils.IntersectRays2D(bisectorPrevious, bisectorNext);
+ const intersect = intersectRays2d.Intersect;
+
+ if (vertexPrevious.Point.Equals(intersect) || vertexNext.Point.Equals(intersect))
+ return Vector2d.Empty;
+
+ return intersect;
+ }
+
+ private static FindOppositeEdgeLav(sLav: HashSet>, oppositeEdge: Edge, center: Vector2d): Vertex {
+ const edgeLavs = this.FindEdgeLavs(sLav, oppositeEdge, null);
+ return this.ChooseOppositeEdgeLav(edgeLavs, oppositeEdge, center);
+ }
+
+ private static ChooseOppositeEdgeLav(edgeLavs: List, oppositeEdge: Edge, center: Vector2d): Vertex {
+ if (!edgeLavs.Any())
+ return null;
+
+ if (edgeLavs.Count === 1)
+ return edgeLavs[0];
+
+ const edgeStart = oppositeEdge.Begin;
+ const edgeNorm = oppositeEdge.Norm;
+ const centerVector = center.Sub(edgeStart);
+ const centerDot = edgeNorm.Dot(centerVector);
+ for (const end of edgeLavs) {
+ const begin = end.Previous as Vertex;
+
+ const beginVector = begin.Point.Sub(edgeStart);
+ const endVector = end.Point.Sub(edgeStart);
+
+ const beginDot = edgeNorm.Dot(beginVector);
+ const endDot = edgeNorm.Dot(endVector);
+
+ if (beginDot < centerDot && centerDot < endDot ||
+ beginDot > centerDot && centerDot > endDot)
+ return end;
+ }
+
+ for (const end of edgeLavs) {
+ const size = end.List.Size;
+ const points = new List(size);
+ let next = end;
+ for (let i = 0; i < size; i++) {
+ points.Add(next.Point);
+ next = next.Next as Vertex;
+ }
+ if (PrimitiveUtils.IsPointInsidePolygon(center, points))
+ return end;
+ }
+ throw new Error("Could not find lav for opposite edge, it could be correct but need some test data to check.");
+ }
+
+ private static FindEdgeLavs(sLav: HashSet>, oppositeEdge: Edge, skippedLav: CircularList): List {
+ const edgeLavs = new List();
+ for (const lav of sLav) {
+ if (lav === skippedLav)
+ continue;
+
+ const vertexInLav = this.GetEdgeInLav(lav, oppositeEdge);
+ if (vertexInLav !== null)
+ edgeLavs.Add(vertexInLav);
+ }
+ return edgeLavs;
+ }
+
+ private static GetEdgeInLav(lav: CircularList, oppositeEdge: Edge): Vertex {
+ for (const node of lav.Iterate())
+ if (oppositeEdge === node.PreviousEdge ||
+ oppositeEdge === node.Previous.Next)
+ return node;
+
+ return null;
+ }
+
+ private static AddFaceBack(newVertex: Vertex, va: Vertex, vb: Vertex) {
+ const fn = new FaceNode(newVertex);
+ va.RightFace.AddPush(fn);
+ FaceQueueUtil.ConnectQueues(fn, vb.LeftFace);
+ }
+
+ private static AddFaceRight(newVertex: Vertex, vb: Vertex) {
+ const fn = new FaceNode(newVertex);
+ vb.RightFace.AddPush(fn);
+ newVertex.RightFace = fn;
+ }
+
+ private static AddFaceLeft(newVertex: Vertex, va: Vertex) {
+ const fn = new FaceNode(newVertex);
+ va.LeftFace.AddPush(fn);
+ newVertex.LeftFace = fn;
+ }
+
+ private static CalcDistance(intersect: Vector2d, currentEdge: Edge): number {
+ const edge = currentEdge.End.Sub(currentEdge.Begin);
+ const vector = intersect.Sub(currentEdge.Begin);
+
+ const pointOnVector = PrimitiveUtils.OrthogonalProjection(edge, vector);
+ return vector.DistanceTo(pointOnVector);
+ }
+
+ private static CalcBisector(p: Vector2d, e1: Edge, e2: Edge): LineParametric2d {
+ const norm1 = e1.Norm;
+ const norm2 = e2.Norm;
+
+ const bisector = this.CalcVectorBisector(norm1, norm2);
+ return new LineParametric2d(p, bisector);
+ }
+
+ private static CalcVectorBisector(norm1: Vector2d, norm2: Vector2d): Vector2d {
+ return PrimitiveUtils.BisectorNormalized(norm1, norm2);
+ }
+}
+
+class SkeletonEventDistanseComparer implements IComparer {
+ public Compare(left: SkeletonEvent, right: SkeletonEvent): number {
+ if (left.Distance > right.Distance)
+ return 1;
+ if (left.Distance < right.Distance)
+ return -1;
+
+ return 0;
+ }
+}
+
+class ChainComparer implements IComparer {
+ private readonly _center: Vector2d;
+
+ constructor(center: Vector2d) {
+ this._center = center;
+ }
+
+ public Compare(x: IChain, y: IChain): number {
+ if (x === y)
+ return 0;
+
+ const angle1 = ChainComparer.Angle(this._center, x.PreviousEdge.Begin);
+ const angle2 = ChainComparer.Angle(this._center, y.PreviousEdge.Begin);
+
+ return angle1 > angle2 ? 1 : -1;
+ }
+
+ private static Angle(p0: Vector2d, p1: Vector2d): number {
+ const dx = p1.X - p0.X;
+ const dy = p1.Y - p0.Y;
+ return Math.atan2(dy, dx);
+ }
+}
+
+class SplitCandidateComparer implements IComparer {
+ public Compare(left: SplitCandidate, right: SplitCandidate): number {
+ if (left.Distance > right.Distance)
+ return 1;
+ if (left.Distance < right.Distance)
+ return -1;
+
+ return 0;
+ }
+}
+
+class SplitCandidate {
+ public readonly Distance: number;
+ public readonly OppositeEdge: Edge = null;
+ public readonly OppositePoint: Vector2d = null;
+ public readonly Point: Vector2d = null;
+
+ constructor(point: Vector2d, distance: number, oppositeEdge: Edge, oppositePoint: Vector2d) {
+ this.Point = point;
+ this.Distance = distance;
+ this.OppositeEdge = oppositeEdge;
+ this.OppositePoint = oppositePoint;
+ }
+}
+
diff --git a/src/lib/skeletons/Utils.ts b/src/lib/skeletons/Utils.ts
new file mode 100644
index 00000000..cfd3ff8b
--- /dev/null
+++ b/src/lib/skeletons/Utils.ts
@@ -0,0 +1,133 @@
+function insertInArray(array: Array, index: number, item: T): Array {
+ const items = Array.prototype.slice.call(arguments, 2);
+
+ return [].concat(array.slice(0, index), items, array.slice(index));
+}
+
+export interface IComparable {
+ CompareTo(other: T): number;
+}
+
+export interface IComparer {
+ Compare(a: T, b: T): number;
+}
+
+export type GeoJSONMultipolygon = [number, number][][][];
+
+export class List extends Array {
+ constructor(capacity = 0) {
+ super();
+ }
+
+ public Add(item: T) {
+ this.push(item);
+ }
+
+ public Insert(index: number, item: T) {
+ const newArr = insertInArray(this, index, item);
+
+ this.length = newArr.length;
+
+ for(let i = 0; i < newArr.length; i++) {
+ this[i] = newArr[i];
+ }
+ }
+
+ public Reverse() {
+ this.reverse();
+ }
+
+ public Clear() {
+ this.length = 0;
+ }
+
+ get Count(): number {
+ return this.length;
+ }
+
+ public Any(filter?: (item: T) => boolean): boolean {
+ if (!filter) {
+ filter = T => true;
+ }
+
+ for (const item of this) {
+ if (filter(item)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public RemoveAt(index: number) {
+ this.splice(index, 1);
+ }
+
+ public Remove(itemToRemove: T) {
+ const newArr = this.filter(item => item !== itemToRemove);
+
+ this.length = newArr.length;
+
+ for(let i = 0; i < newArr.length; i++) {
+ this[i] = newArr[i];
+ }
+ }
+
+ public AddRange(list: List) {
+ for (const item of list) {
+ this.Add(item);
+ }
+ }
+
+ public Sort(comparer: IComparer) {
+ this.sort(comparer.Compare.bind(comparer));
+ }
+}
+
+export class HashSet implements Iterable {
+ private Set: Set;
+
+ constructor() {
+ this.Set = new Set();
+ }
+
+ public Add(item: T) {
+ this.Set.add(item);
+ }
+
+ public Remove(item: T) {
+ this.Set.delete(item);
+ }
+
+ public RemoveWhere(filter: (item: T) => boolean) {
+ for (const item of this.Set.values()) {
+ if (filter(item)) {
+ this.Set.delete(item);
+ }
+ }
+ }
+
+ public Contains(item: T): boolean {
+ return this.Set.has(item);
+ }
+
+ public Clear() {
+ this.Set.clear();
+ }
+
+ public* [Symbol.iterator](): Generator {
+ for (const item of this.Set.values()) {
+ yield item;
+ }
+ }
+}
+
+export class Dictionary extends Map {
+ public ContainsKey(key: T1): boolean {
+ return this.has(key);
+ }
+
+ public Add(key: T1, value: T2) {
+ return this.set(key, value);
+ }
+}
\ No newline at end of file
diff --git a/src/lib/skeletons/index.ts b/src/lib/skeletons/index.ts
new file mode 100644
index 00000000..b1f2cdb5
--- /dev/null
+++ b/src/lib/skeletons/index.ts
@@ -0,0 +1,12 @@
+// Types
+export type { GeoJSONMultipolygon, List } from "./Utils";
+export type { Skeleton } from "./Skeleton";
+
+// Values
+export { default as Vector2d } from "./Primitives/Vector2d";
+export { default as SkeletonBuilder } from "./SkeletonBuilder";
+export { default as EdgeResult } from "./EdgeResult";
+export { default as Edge } from "./Circular/Edge";
+export { default as Vertex } from "./Circular/Vertex";
+
+
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 44f723fd..5aa83d3f 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -57,6 +57,7 @@
"modal.movement.flow.line.top.right": "高さ変更:上、右",
"plan.menu.roof.cover.outline.edit.offset": "外壁の編集とオフセット",
"plan.menu.roof.cover.roof.surface.alloc": "屋根面の割り当て",
+ "plan.menu.roof.cover.roof.surface.all.remove": "伏せ図全削除",
"plan.menu.roof.cover.roof.shape.edit": "屋根形状編集",
"plan.menu.roof.cover.auxiliary.line.drawing": "補助線の作成",
"modal.cover.outline.drawing": "外壁線の作成",
diff --git a/src/locales/ko.json b/src/locales/ko.json
index 83ed3f2e..35caabfd 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -57,6 +57,7 @@
"modal.movement.flow.line.top.right": "높이변경 : 위, 오른쪽",
"plan.menu.roof.cover.outline.edit.offset": "외벽선 편집 및 오프셋",
"plan.menu.roof.cover.roof.surface.alloc": "지붕면 할당",
+ "plan.menu.roof.cover.roof.surface.all.remove": "배치면 전체 삭제",
"plan.menu.roof.cover.roof.shape.edit": "지붕형상 편집",
"plan.menu.roof.cover.auxiliary.line.drawing": "보조선 작성",
"modal.cover.outline.drawing": "외벽선 작성",
diff --git a/src/store/menuAtom.js b/src/store/menuAtom.js
index fdb50461..007e7950 100644
--- a/src/store/menuAtom.js
+++ b/src/store/menuAtom.js
@@ -23,9 +23,19 @@ export const menusState = atom({
},
{ type: 'outline', name: 'plan.menu.roof.cover', icon: 'con02', title: MENU.ROOF_COVERING.DEFAULT },
{ type: 'surface', name: 'plan.menu.placement.surface', icon: 'con03', title: MENU.BATCH_CANVAS.DEFAULT },
- { type: 'module', name: 'plan.menu.module.circuit.setting', icon: 'con04', title: MENU.MODULE_CIRCUIT_SETTING.DEFAULT },
+ {
+ type: 'module',
+ name: 'plan.menu.module.circuit.setting',
+ icon: 'con04',
+ title: MENU.MODULE_CIRCUIT_SETTING.DEFAULT,
+ },
{ type: 'estimate', name: 'plan.menu.estimate', icon: 'con06', title: MENU.ESTIMATE.DEFAULT },
- { type: 'simulation', name: 'plan.menu.simulation', icon: 'con05', title: MENU.POWER_GENERATION_SIMULATION.DEFAULT },
+ {
+ type: 'simulation',
+ name: 'plan.menu.simulation',
+ icon: 'con05',
+ title: MENU.POWER_GENERATION_SIMULATION.DEFAULT,
+ },
],
})
@@ -37,16 +47,17 @@ export const subMenusState = atom({
// 지붕덮개
{ id: 0, name: 'plan.menu.roof.cover.outline.drawing', menu: MENU.ROOF_COVERING.EXTERIOR_WALL_LINE },
{ id: 1, name: 'plan.menu.roof.cover.roof.shape.setting', menu: MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS },
- // {
- // id: 2,
- // name: 'plan.menu.roof.cover.roof.shape.passivity.setting',
- // menu: MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS,
- // },
+ {
+ id: 2,
+ name: 'plan.menu.roof.cover.roof.shape.passivity.setting',
+ menu: MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS,
+ },
{ id: 3, name: 'plan.menu.roof.cover.auxiliary.line.drawing', menu: MENU.ROOF_COVERING.HELP_LINE_DRAWING },
{ id: 4, name: 'plan.menu.roof.cover.eaves.kerava.edit', menu: MENU.ROOF_COVERING.EAVES_KERAVA_EDIT },
{ id: 5, name: 'plan.menu.roof.cover.movement.shape.updown', menu: MENU.ROOF_COVERING.MOVEMENT_SHAPE_UPDOWN },
{ id: 6, name: 'plan.menu.roof.cover.outline.edit.offset', menu: MENU.ROOF_COVERING.OUTLINE_EDIT_OFFSET },
{ id: 7, name: 'plan.menu.roof.cover.roof.surface.alloc', menu: MENU.ROOF_COVERING.ROOF_SHAPE_ALLOC },
+ { id: 8, name: 'plan.menu.roof.cover.roof.surface.all.remove', menu: MENU.ROOF_COVERING.ALL_REMOVE },
],
surface: [
// 배치면
diff --git a/src/styles/calc.scss b/src/styles/calc.scss
new file mode 100644
index 00000000..f3977c65
--- /dev/null
+++ b/src/styles/calc.scss
@@ -0,0 +1,156 @@
+// Variables
+$colors: (
+ 'dark-700': #1f2937,
+ 'dark-600': #334155,
+ 'dark-500': #4b5563,
+ 'dark-400': #6b7280,
+ 'dark-300': #374151,
+ 'primary': #10b981,
+ 'primary-dark': #059669,
+ 'danger': #ef4444,
+ 'danger-dark': #dc2626,
+ 'warning': #f59e0b,
+ 'warning-dark': #d97706,
+ 'border': #475569
+);
+
+// Mixins
+@mixin button-styles {
+ padding: 0.125rem;
+ border-radius: 0.5rem;
+ font-weight: bold;
+ font-size: 0.625rem;
+ color: white;
+ border: none;
+ cursor: pointer;
+ transition: all 0.1s ease-in-out;
+
+ &:active {
+ transform: scale(0.95);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+}
+
+// Calculator Input Wrapper
+.calculator-input-wrapper {
+ position: relative;
+ width: 100%;
+ display: inline-block;
+
+ // Input Field
+ .calculator-input {
+ width: 100%;
+ padding: 0.5rem 1rem;
+ background-color: map-get($colors, 'dark-600');
+ border: 1px solid map-get($colors, 'border');
+ color: white;
+ font-weight: bold;
+ border-radius: 0.5rem;
+ text-align: right;
+ cursor: pointer;
+ font-size: 0.625rem;
+ box-sizing: border-box;
+
+ &:focus {
+ outline: none;
+ border-color: map-get($colors, 'primary');
+ box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
+ }
+ }
+
+ // Keypad Container
+ .keypad-container {
+ position: absolute;
+ top: calc(100% + 2px);
+ left: 0;
+ right: 0;
+ width: 120px;
+ background-color: map-get($colors, 'dark-700');
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ padding: 0.5rem;
+ z-index: 1000;
+ animation: fadeIn 0.15s ease-out;
+ border: 1px solid map-get($colors, 'border');
+ box-sizing: border-box;
+
+ // Keypad Grid
+ .keypad-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.125rem;
+
+ // Button Base
+ button {
+ @include button-styles;
+ }
+
+ // Button Types
+ .btn-number {
+ background-color: map-get($colors, 'dark-500');
+
+ &:hover {
+ background-color: map-get($colors, 'dark-400');
+ }
+ }
+
+ .btn-operator {
+ background-color: map-get($colors, 'warning');
+
+ &:hover {
+ background-color: map-get($colors, 'warning-dark');
+ }
+ }
+
+ .btn-clear {
+ background-color: map-get($colors, 'danger');
+ grid-column: span 2;
+
+ &:hover {
+ background-color: map-get($colors, 'danger-dark');
+ }
+ }
+
+ .btn-delete {
+ background-color: map-get($colors, 'primary');
+ grid-column: span 2;
+
+ &:hover {
+ background-color: map-get($colors, 'primary-dark');
+ }
+ }
+
+ .btn-equals {
+ background-color: map-get($colors, 'warning');
+
+ &:hover {
+ background-color: map-get($colors, 'warning-dark');
+ }
+ }
+
+ .btn-zero {
+ grid-column: span 1;
+ }
+ }
+ }
+}
+
+// Animations
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+// Responsive Design
+@media (max-width: 640px) {
+ .keypad-grid button {
+ padding: 0.5rem;
+ font-size: 0.875rem;
+ }
+}
\ No newline at end of file
diff --git a/src/util/calc-utils.js b/src/util/calc-utils.js
new file mode 100644
index 00000000..49e76721
--- /dev/null
+++ b/src/util/calc-utils.js
@@ -0,0 +1,172 @@
+export const createCalculator = (options = {}) => {
+ const state = {
+ currentOperand: '',
+ previousOperand: '',
+ operation: undefined,
+ shouldResetDisplay: false,
+ allowNegative: options.allowNegative ?? true,
+ allowDecimal: options.allowDecimal ?? true,
+ allowZero: options.allowZero ?? true,
+ decimalPlaces: options.decimalPlaces ?? null,
+ }
+
+ // Expose state for debugging and direct access
+ const getState = () => ({ ...state })
+
+ const clear = () => {
+ state.currentOperand = ''
+ state.previousOperand = ''
+ state.operation = undefined
+ state.shouldResetDisplay = false
+ return state.currentOperand
+ }
+
+ const deleteNumber = () => {
+ if (state.currentOperand.length <= 1) {
+ state.currentOperand = '0'
+ } else {
+ state.currentOperand = state.currentOperand.toString().slice(0, -1)
+ }
+ return state.currentOperand
+ }
+
+ const appendNumber = (number) => {
+ if (number === '.' && !state.allowDecimal) return state.currentOperand
+ if (number === '.' && state.currentOperand.includes('.')) return state.currentOperand
+
+ if (state.shouldResetDisplay) {
+ state.currentOperand = number.toString()
+ state.shouldResetDisplay = false
+ } else {
+ if (state.currentOperand === '0' && number !== '.') {
+ state.currentOperand = number.toString()
+ } else {
+ state.currentOperand = state.currentOperand.toString() + number.toString()
+ }
+ }
+ return state.currentOperand
+ }
+
+ const chooseOperation = (operation) => {
+ if (operation === '-' && state.currentOperand === '0' && state.previousOperand === '' && state.allowNegative) {
+ state.currentOperand = '-'
+ return state.currentOperand
+ }
+
+ if (state.currentOperand === '' || state.currentOperand === '-') return state.currentOperand
+
+ // If there's a previous operation, compute it first
+ if (state.previousOperand !== '') {
+ compute()
+ }
+
+ state.operation = operation
+ state.previousOperand = state.currentOperand
+ state.currentOperand = ''
+ return state.previousOperand + state.operation
+ }
+
+ const compute = () => {
+ // If there's no operation, return the current value
+ if (!state.operation) return parseFloat(state.currentOperand || '0')
+
+ // If there's no current operand but we have a previous one, use previous as current
+ if (state.currentOperand === '' && state.previousOperand !== '') {
+ state.currentOperand = state.previousOperand
+ }
+
+ const prev = parseFloat(state.previousOperand)
+ const current = parseFloat(state.currentOperand)
+
+ if (isNaN(prev) || isNaN(current)) return 0
+
+ let result
+ switch (state.operation) {
+ case '+':
+ result = prev + current
+ break
+ case '-':
+ result = prev - current
+ break
+ case '×':
+ result = prev * current
+ break
+ case '÷':
+ if (current === 0) {
+ state.currentOperand = 'Error'
+ return 0
+ }
+ result = prev / current
+ break
+ default:
+ return parseFloat(state.currentOperand || '0')
+ }
+
+ // Apply formatting and constraints
+ if (state.decimalPlaces !== null) {
+ result = Number(result.toFixed(state.decimalPlaces))
+ }
+
+ if (!state.allowDecimal) {
+ result = Math.round(result)
+ }
+
+ if (!state.allowNegative && result < 0) {
+ result = 0
+ }
+
+ if (!state.allowZero && result === 0) {
+ result = 1
+ }
+
+ // Update state
+ state.currentOperand = result.toString()
+ state.previousOperand = ''
+ state.operation = undefined
+ state.shouldResetDisplay = true
+
+ return result
+ }
+
+ // Getter methods for the calculator state
+ const getCurrentOperand = () => state.currentOperand
+ const getPreviousOperand = () => state.previousOperand
+ const getOperation = () => state.operation
+ const getDisplayValue = () => {
+ if (state.operation && state.previousOperand) {
+ return `${state.previousOperand} ${state.operation} ${state.currentOperand || ''}`.trim()
+ }
+ return state.currentOperand
+ }
+
+ return {
+ // Core calculator methods
+ clear,
+ delete: deleteNumber, // Alias for deleteNumber for compatibility
+ deleteNumber,
+ appendNumber,
+ chooseOperation,
+ compute,
+
+ // State getters
+ getDisplayValue,
+ getCurrentOperand,
+ getPreviousOperand,
+ getOperation,
+
+ // Direct state access (for debugging)
+ getState,
+
+ // Direct property access (for compatibility with CalcInput.jsx)
+ get currentOperand() { return state.currentOperand },
+ get previousOperand() { return state.previousOperand },
+ get operation() { return state.operation },
+ get shouldResetDisplay() { return state.shouldResetDisplay },
+
+ // Setter for direct property access (if needed)
+ set currentOperand(value) { state.currentOperand = value },
+ set previousOperand(value) { state.previousOperand = value },
+ set operation(value) { state.operation = value },
+ set shouldResetDisplay(value) { state.shouldResetDisplay = value }
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..2a8f023b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "target": "es2015",
+ "downlevelIteration": true,
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "noEmit": true,
+ "incremental": true,
+ "module": "esnext",
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}