400 lines
10 KiB
TypeScript
400 lines
10 KiB
TypeScript
import { ClipType, PolyFillType } from 'js-angusj-clipper/web'
|
|
import type { Paths } from 'js-angusj-clipper/web'
|
|
import type { SubjectInput } from 'js-angusj-clipper/web/clipFunctions'
|
|
import { Box2 } from '../Box2'
|
|
import { clipperCpp } from '../ClipperCpp'
|
|
import type { CompareVectorFn } from '../ComparePoint'
|
|
import { ComparePoint } from '../ComparePoint'
|
|
import { ConvexHull2D } from '../ConvexHull2D'
|
|
import type { NestFiler } from '../Filer'
|
|
import type { Point } from '../Vector2'
|
|
import { equaln } from '../Util'
|
|
import { Vector2 } from '../Vector2'
|
|
import { PlaceType } from '../../confClass' //'../../vo/enums/PlaceType'
|
|
import { NestCache } from './NestCache'
|
|
import type { Part } from './Part'
|
|
import { PartGroup } from './Part'
|
|
import type { Path } from './Path'
|
|
import { Area, TranslatePath } from './Path'
|
|
|
|
/**
|
|
* 当零件尝试放置后容器的状态,用于尝试放置,并且尝试贪心
|
|
*/
|
|
interface PartPlacedContainerState
|
|
{
|
|
p: Point
|
|
area?: number
|
|
hull?: Point[]
|
|
box?: Box2
|
|
}
|
|
|
|
/**
|
|
* 排料零件的容器,用来放置零件
|
|
* 它是一块大板
|
|
* 也可以是一个网洞
|
|
* 也可以是余料
|
|
*/
|
|
export class Container
|
|
{
|
|
ParentId: number = -1// 容器bin来源 -1表示默认的bin,大于等于0表示来自板件网洞
|
|
ChildrenIndex: number = 0// 网洞的索引位置
|
|
ParentM: Point// 在旋转网洞时,和非旋转网洞时表现不一样. (网洞相对于父零件的位置,如果是使用旋转网洞则表示网洞的最下角位置)
|
|
|
|
PlacedParts: Part[] = []
|
|
// 放置状态
|
|
PlacedArea = 0
|
|
PlacedBox: Box2
|
|
PlacedHull: Point[]
|
|
|
|
StatusKey: string
|
|
|
|
CompartePoint: CompareVectorFn
|
|
constructor(protected BinPath?: Path, private _PlaceType = PlaceType.Box, compare = 'xy')
|
|
{
|
|
if (BinPath)
|
|
this.StatusKey = `${this.BinPath.Id.toString()},${this._PlaceType}`
|
|
|
|
this.CompartePoint = ComparePoint(compare)
|
|
}
|
|
|
|
get UseRatio(): number
|
|
{
|
|
return this.PlacedBox.area / this.BinPath.Area
|
|
}
|
|
|
|
private _NotPuts: Set<number>[] = []// 已经无法放置的pathId
|
|
|
|
private PrePut(part: Part): PartPlacedContainerState
|
|
{
|
|
// 无法容纳
|
|
if (this.BinPath.Area - this.PlacedArea < part.State.Contour.Area)
|
|
return
|
|
|
|
let cacheKey = `${this.StatusKey},${part.State.Contour.Id}`
|
|
let cacheP = NestCache.PositionCache[cacheKey]
|
|
// 读取缓存位置
|
|
if (cacheP)
|
|
{
|
|
NestCache.count1++
|
|
return this.Calc(part, cacheP)
|
|
}
|
|
|
|
let binNfp = this.BinPath.GetInsideNFP(part.State.Contour)
|
|
if (!binNfp || binNfp.length === 0)
|
|
return
|
|
|
|
// 首个
|
|
if (this.PlacedParts.length === 0)
|
|
{
|
|
let p = this.GetFarLeftP(binNfp)
|
|
NestCache.PositionCache[cacheKey] = p
|
|
return this.Calc(part, p)
|
|
}
|
|
|
|
// 快速退出
|
|
let noSet = NestCache.NoPutCache[this.StatusKey]
|
|
if (noSet)
|
|
this._NotPuts.push(noSet)
|
|
for (let set of this._NotPuts)
|
|
{
|
|
if (set.has(part.State.Contour.Id))
|
|
{
|
|
NestCache.count2++
|
|
return
|
|
}
|
|
}
|
|
|
|
let finalNfp = this.GetNFPs(part, binNfp)
|
|
if (!finalNfp || finalNfp.length === 0)
|
|
{
|
|
if (noSet)
|
|
noSet.add(part.State.Contour.Id)
|
|
else
|
|
{
|
|
noSet = new Set([part.State.Contour.Id])
|
|
NestCache.NoPutCache[this.StatusKey] = noSet
|
|
}
|
|
return
|
|
}
|
|
|
|
// 选择合适的放置点
|
|
let minArea: number = Number.POSITIVE_INFINITY
|
|
let translate: Point
|
|
let bestBox: Box2
|
|
let bestHull: Point[]
|
|
|
|
let tempVec = new Vector2()
|
|
for (let nfp of finalNfp)
|
|
{
|
|
for (let p of nfp)
|
|
{
|
|
tempVec.set(p.x * 1e-4, p.y * 1e-4)
|
|
switch (this._PlaceType)
|
|
{
|
|
case PlaceType.Box:
|
|
{
|
|
let box2 = part.State.Contour.BoundingBox.clone()
|
|
box2.translate(tempVec)
|
|
let rectBox = this.PlacedBox.clone().union(box2)
|
|
let size = rectBox.getSize()
|
|
let area = size.x * size.y
|
|
if (area < minArea || ((equaln(area, minArea, 1)) && this.CompartePoint(p, translate) === -1))
|
|
{
|
|
translate = p
|
|
minArea = area
|
|
bestBox = rectBox
|
|
}
|
|
break
|
|
}
|
|
case PlaceType.Hull:
|
|
{
|
|
let pts = TranslatePath(part.State.Contour.Points, tempVec)
|
|
let nhull = ConvexHull2D([...pts, ...this.PlacedHull])
|
|
let area = Math.abs(Area(nhull))
|
|
if (area < minArea || ((equaln(area, minArea, 1)) && this.CompartePoint(p, translate) === -1))
|
|
{
|
|
translate = p
|
|
minArea = area
|
|
bestHull = nhull
|
|
}
|
|
break
|
|
}
|
|
case PlaceType.Gravity:
|
|
{
|
|
if (!translate || this.CompartePoint(p, translate) === -1)
|
|
translate = p
|
|
|
|
break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (translate)
|
|
{
|
|
NestCache.PositionCache[cacheKey] = translate
|
|
if (!bestBox)
|
|
bestBox = this.PlacedBox.clone().union(part.State.Contour.BoundingBox.clone().translate({ x: translate.x * 1e-4, y: translate.y * 1e-4 }))
|
|
return { p: translate, area: minArea, box: bestBox, hull: bestHull }
|
|
}
|
|
}
|
|
|
|
private Calc(part: Part, p: Point): PartPlacedContainerState
|
|
{
|
|
let d: PartPlacedContainerState = { p }
|
|
|
|
let m: Point = { x: p.x * 1e-4, y: p.y * 1e-4 }
|
|
|
|
if (this.PlacedBox)
|
|
d.box = this.PlacedBox.clone().union(part.State.Contour.BoundingBox.clone().translate(m))
|
|
else
|
|
d.box = part.State.Contour.BoundingBox.clone().translate(m)
|
|
|
|
// 凸包
|
|
if (this._PlaceType === PlaceType.Hull)
|
|
{
|
|
if (!this.PlacedHull)
|
|
{
|
|
d.hull = TranslatePath(part.State.Contour.Points, m)
|
|
d.area = part.State.Contour.Area
|
|
}
|
|
else
|
|
{
|
|
d.hull = ConvexHull2D([...this.PlacedHull, ...TranslatePath(part.State.Contour.Points, m)])
|
|
d.area = Area(d.hull)
|
|
}
|
|
|
|
d.area = Math.abs(d.area)
|
|
}
|
|
else
|
|
{
|
|
d.area = d.box.area
|
|
}
|
|
return d
|
|
}
|
|
|
|
PutPart(p: Part, greedy = false): boolean
|
|
{
|
|
let bestD: PartPlacedContainerState
|
|
if (greedy)
|
|
{
|
|
let bestI: number
|
|
for (let i = 0; i < p.RotatedStates.length; i++)
|
|
{
|
|
p.StateIndex = i
|
|
let d = this.PrePut(p)
|
|
if (d && (!bestD || bestD.area > d.area
|
|
|| (
|
|
this._PlaceType === PlaceType.Hull
|
|
&& equaln(bestD.area, d.area, 0.1)
|
|
&& d.box.area < bestD.box.area
|
|
)
|
|
))
|
|
{
|
|
bestD = d
|
|
bestI = i
|
|
}
|
|
}
|
|
if (bestD)
|
|
p.StateIndex = bestI
|
|
}
|
|
else
|
|
bestD = this.PrePut(p)
|
|
|
|
if (bestD)
|
|
{
|
|
p.PlacePosition = bestD.p
|
|
this.PlacedBox = bestD.box ?? this.PlacedBox
|
|
this.PlacedHull = bestD.hull
|
|
this.AppendPart(p, false)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
protected GetNFPs(part: Part, binNfp: Point[][]): Paths
|
|
{
|
|
// 合并(零件和所有已经放置零件的NFP)
|
|
let nfps: SubjectInput[] = []
|
|
for (let placedPart of this.PlacedParts)
|
|
{
|
|
let nfp = placedPart.State.Contour.GetOutsideNFP(part.State.Contour)
|
|
if (!nfp)
|
|
return
|
|
for (let n of nfp)
|
|
{
|
|
let nnfp = TranslatePath(n, placedPart.PlacePosition)
|
|
nfps.push({ data: nnfp, closed: true })
|
|
}
|
|
}
|
|
// 合并nfp
|
|
let combinedNfp = clipperCpp.lib.clipToPaths({
|
|
subjectInputs: nfps,
|
|
clipType: ClipType.Union,
|
|
subjectFillType: PolyFillType.NonZero,
|
|
})
|
|
|
|
combinedNfp = clipperCpp.lib.cleanPolygons(combinedNfp, 100)
|
|
|
|
if (combinedNfp.length === 0)
|
|
return
|
|
|
|
// 减去nfp
|
|
let finalNfp = clipperCpp.lib.clipToPaths({
|
|
subjectInputs: [{ data: binNfp, closed: true }],
|
|
clipInputs: [{ data: combinedNfp }],
|
|
clipType: ClipType.Difference,
|
|
subjectFillType: PolyFillType.NonZero,
|
|
})
|
|
finalNfp = clipperCpp.lib.cleanPolygons(finalNfp, 100)
|
|
return finalNfp
|
|
}
|
|
|
|
/**
|
|
* 将Part添加的Placed列表
|
|
* @param part 零件,已经计算了放置状态
|
|
* @param [calc] 是否计算当前的容器状态?
|
|
*/
|
|
private AppendPart(part: Part, calc = true): void
|
|
{
|
|
this.StatusKey += `,${part.State.Contour.Id}`
|
|
this.PlacedParts.push(part)
|
|
this.PlacedArea += part.State.Contour.Area
|
|
let m = { x: part.PlacePosition.x * 1e-4, y: part.PlacePosition.y * 1e-4 }
|
|
if (calc)
|
|
{
|
|
// 凸包
|
|
if (this._PlaceType === PlaceType.Hull)
|
|
{
|
|
if (!this.PlacedHull)
|
|
this.PlacedHull = TranslatePath(part.State.Contour.Points, m)
|
|
else
|
|
this.PlacedHull = ConvexHull2D(this.PlacedHull.concat(TranslatePath(part.State.Contour.Points, m)))
|
|
}
|
|
}
|
|
if (calc || this._PlaceType !== PlaceType.Box)
|
|
{
|
|
if (this.PlacedBox)
|
|
this.PlacedBox.union(part.State.Contour.BoundingBox.clone().translate(m))
|
|
else
|
|
this.PlacedBox = part.State.Contour.BoundingBox.clone().translate(m)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 得到最左边的点
|
|
*/
|
|
protected GetFarLeftP(nfp: Point[][]): Point
|
|
{
|
|
let leftP: Point
|
|
for (let path of nfp)
|
|
{
|
|
for (let p of path)
|
|
{
|
|
if (!leftP || this.CompartePoint(p, leftP) === -1)
|
|
leftP = p
|
|
}
|
|
}
|
|
return leftP
|
|
}
|
|
|
|
// #region -------------------------File-------------------------
|
|
// 对象从文件中读取数据,初始化自身
|
|
ReadFile(file: NestFiler, parts: Part[])
|
|
{
|
|
this.ParentId = file.Read()
|
|
this.ChildrenIndex = file.Read()
|
|
this.ParentM = file.Read()
|
|
let count = file.Read() as number
|
|
this.PlacedParts = []
|
|
for (let i = 0; i < count; i++)
|
|
{
|
|
let index = file.Read() as number
|
|
let part = parts[index]
|
|
part.StateIndex = file.Read()
|
|
part.PlacePosition = file.Read()
|
|
this.PlacedParts.push(part)
|
|
}
|
|
|
|
if (!this.PlacedBox)
|
|
this.PlacedBox = new Box2()
|
|
this.PlacedBox.min.fromArray(file.Read())
|
|
this.PlacedBox.max.fromArray(file.Read())
|
|
|
|
return this
|
|
}
|
|
|
|
// 对象将自身数据写入到文件.
|
|
WriteFile(file: NestFiler)
|
|
{
|
|
file.Write(this.ParentId)
|
|
file.Write(this.ChildrenIndex)
|
|
file.Write(this.ParentM)
|
|
|
|
let parts: Part[] = []
|
|
for (let p of this.PlacedParts)
|
|
{
|
|
if (p instanceof PartGroup)
|
|
parts.push(...p.Export())
|
|
else
|
|
parts.push(p)
|
|
}
|
|
|
|
file.Write(parts.length)
|
|
for (let p of parts)
|
|
{
|
|
file.Write(p.Id)
|
|
file.Write(p.StateIndex)
|
|
file.Write(p.PlacePosition)
|
|
}
|
|
|
|
if (!this.PlacedBox)
|
|
this.PlacedBox = new Box2()
|
|
file.Write(this.PlacedBox.min.toArray())
|
|
file.Write(this.PlacedBox.max.toArray())
|
|
}
|
|
// #endregion
|
|
}
|