diff --git a/package.json b/package.json index 14b1299b..7be9b3f3 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,14 @@ }, "devDependencies": { "@turf/turf": "^7.0.0", + "@types/node": "^24.3.0", + "@types/react": "^19.1.11", "convertapi": "^1.14.0", "postcss": "^8", "prettier": "^3.3.3", "react-color-palette": "^7.2.2", "sass": "^1.77.8", - "tailwindcss": "^3.4.1" + "tailwindcss": "^3.4.1", + "typescript": "^5.9.2" } } diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index 7881712e..77254d5d 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -2,6 +2,7 @@ import { fabric } from 'fabric' import { v4 as uuidv4 } from 'uuid' import { getDirectionByPoint } from '@/util/canvas-util' import { calcLinePlaneSize } from '@/util/qpolygon-utils' +import { logger } from '@/util/logger' export const QLine = fabric.util.createClass(fabric.Line, { type: 'QLine', @@ -69,7 +70,14 @@ export const QLine = fabric.util.createClass(fabric.Line, { }, setLength() { - this.length = calcLinePlaneSize(this) / 10 + // Ensure all required properties are valid numbers + const { x1, y1, x2, y2 } = this; + if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) { + logger.error('Invalid coordinates in QLine:', { x1, y1, x2, y2 }); + this.length = 0; + return; + } + this.length = calcLinePlaneSize({ x1, y1, x2, y2 }) / 10; }, addLengthText() { diff --git a/src/components/floor-plan/modal/basic/step/Placement.jsx b/src/components/floor-plan/modal/basic/step/Placement.jsx index e96cf5c0..091129a8 100644 --- a/src/components/floor-plan/modal/basic/step/Placement.jsx +++ b/src/components/floor-plan/modal/basic/step/Placement.jsx @@ -169,6 +169,7 @@ const Placement = forwardRef((props, refs) => {
+ {moduleData.header.map((data) => ( ))} + {selectedModules?.itemList && @@ -216,7 +218,7 @@ const Placement = forwardRef((props, refs) => { className="input-origin block" name="row" value={props.layoutSetup[index]?.row ?? 1} - defaultValue={0} + //defaultValue={0} onChange={(e) => handleLayoutSetup(e, item.itemId, index)} /> @@ -228,7 +230,7 @@ const Placement = forwardRef((props, refs) => { className="input-origin block" name="col" value={props.layoutSetup[index]?.col ?? 1} - defaultValue={0} + //defaultValue={0} onChange={(e) => handleLayoutSetup(e, item.itemId, index)} /> diff --git a/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx b/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx index 7c9d4f51..32364844 100644 --- a/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx +++ b/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx @@ -86,7 +86,7 @@ export default function RoofAllocationSetting(props) { return (
- +
{pitchText} 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/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..8244b4cd --- /dev/null +++ b/src/lib/skeletons/index.ts @@ -0,0 +1,9 @@ +import {GeoJSONMultipolygon, List} from "./Utils"; +import Vector2d from "./Primitives/Vector2d"; +import SkeletonBuilder from "./SkeletonBuilder"; +import { Skeleton } from "./Skeleton"; +import EdgeResult from "./EdgeResult"; +import Edge from "./Circular/Edge"; +import Vertex from "./Circular/Vertex"; + +export {SkeletonBuilder, List, Vector2d, GeoJSONMultipolygon, Skeleton, EdgeResult, Edge, Vertex}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..e2d762e2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "es2015", + "downlevelIteration": true, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, // 추가 + "moduleResolution": "bundler", // 변경: "node" → "bundler" + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": ".", // 추가 + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +}
{data.type === 'check' ? ( @@ -181,6 +182,7 @@ const Placement = forwardRef((props, refs) => { )}