feat:提交
This commit is contained in:
399
tests/dev1/dataHandle/common/core/Container.ts
Normal file
399
tests/dev1/dataHandle/common/core/Container.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
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
|
||||
}
|
5
tests/dev1/dataHandle/common/core/GNestConfig.ts
Normal file
5
tests/dev1/dataHandle/common/core/GNestConfig.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const GNestConfig = {
|
||||
RotateHole: true, // 在旋转零件的时候旋转网洞
|
||||
UsePartGroup: false, // 如果开启这个特性,将在第一次放置零件时,尝试计算完全对插的板件,并且使用它.(基因注册,模范夫妻)
|
||||
UseOffsetSimplify: true,
|
||||
}
|
274
tests/dev1/dataHandle/common/core/Individual.ts
Normal file
274
tests/dev1/dataHandle/common/core/Individual.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { arrayLast, arrayRemoveOnce } from '../ArrayExt'
|
||||
import type { NestFiler } from '../Filer'
|
||||
import { RandomIndex } from '../Random'
|
||||
import type { PlaceType } from '../../vo/enums/PlaceType'
|
||||
import { Container } from './Container'
|
||||
import { GNestConfig } from './GNestConfig'
|
||||
import { DefaultComparePointKeys } from './NestDatabase'
|
||||
import type { Part } from './Part'
|
||||
import type { Path } from './Path'
|
||||
|
||||
/**
|
||||
* 个体(表示某一次优化的结果,或者还没开始优化的状态)
|
||||
* 个体是由一堆零件组成的,零件可以有不同的状态。
|
||||
*
|
||||
* 个体单独变异
|
||||
* 可以是某个零件的旋转状态发生改变
|
||||
* 可以是零件的顺序发生改变
|
||||
*
|
||||
* 个体交配(感觉暂时不需要)
|
||||
* 个体和其他个体进行基因交换
|
||||
*/
|
||||
export class Individual {
|
||||
constructor(public Parts?: Part[], public mutationRate = 0.5, private bin?: Path, private OddmentsBins: Path[] = [], private ComparePointKeys: string[] = DefaultComparePointKeys) { }
|
||||
|
||||
Fitness: number // (评估健康) 大板个数-1 + 最后一块板的利用率
|
||||
Containers: Container[] // 大板列表(已排
|
||||
HoleContainers: Container[]// 网洞列表
|
||||
OddmentsContainers: Container[]// 余料列表
|
||||
/**
|
||||
* 评估健康程度
|
||||
*/
|
||||
Evaluate(bestCount: number, greedy = false, type?: PlaceType) {
|
||||
if (GNestConfig.RotateHole)
|
||||
return this.EvaluateOfUseRotateHole(bestCount, greedy, type)
|
||||
|
||||
this.Containers = []
|
||||
this.HoleContainers = []
|
||||
// 初始化余料列表
|
||||
this.OddmentsContainers = this.OddmentsBins.map(path => new Container(path, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)]))
|
||||
this.InitHoleContainers()
|
||||
|
||||
// 余料优先
|
||||
let parts = this.Parts.filter(p => !this.OddmentsContainers.some(odd => odd.PutPart(p)))
|
||||
|
||||
// 网洞优先
|
||||
parts = parts.filter(p => !this.HoleContainers.some(hole => hole.PutPart(p)))
|
||||
|
||||
while (parts.length > 0) {
|
||||
if (this.Containers.length > bestCount) {
|
||||
this.Fitness = Math.ceil(bestCount) + 1
|
||||
return
|
||||
}
|
||||
const container = new Container(this.bin, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)])
|
||||
if (this.mutationRate > 0.5) // 大板优先,可以提高收敛速度
|
||||
{
|
||||
const maxP = parts.reduce((preP, curP) => preP.State.Contour.Area > curP.State.Contour.Area ? preP : curP)
|
||||
|
||||
const PutGroupF = (): boolean => {
|
||||
// 基因注册
|
||||
const nextPartIndex = parts.findIndex(p => p !== maxP)
|
||||
if (nextPartIndex !== -1) {
|
||||
const nextPart = parts[nextPartIndex]
|
||||
const groups = maxP.ParseGroup(nextPart, this.bin)
|
||||
for (const group of groups) {
|
||||
if (container.PutPart(group)) {
|
||||
parts.splice(nextPartIndex, 1)
|
||||
arrayRemoveOnce(parts, maxP)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (GNestConfig.UsePartGroup && PutGroupF()) { }
|
||||
else if (container.PutPart(maxP, greedy)) { arrayRemoveOnce(parts, maxP) }
|
||||
}
|
||||
parts = parts.filter(p => !container.PutPart(p, greedy))
|
||||
if (!greedy)// 如果没有贪心,排完后在贪心一下
|
||||
parts = parts.filter(p => !container.PutPart(p, true))
|
||||
this.Containers.push(container)
|
||||
}
|
||||
this.Fitness = this.Containers.length - 1 + arrayLast(this.Containers)?.UseRatio ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 在网洞利用时,保持纹路正确
|
||||
*
|
||||
* @param bestCount
|
||||
* @param greedy
|
||||
* @param type
|
||||
*/
|
||||
private EvaluateOfUseRotateHole(bestCount: number, greedy = false, type?: PlaceType) {
|
||||
// 初始化余料列表
|
||||
this.OddmentsContainers = this.OddmentsBins.map(path => new Container(path, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)]))
|
||||
|
||||
this.Containers = []
|
||||
this.HoleContainers = []
|
||||
let parts = this.Parts.concat()
|
||||
while (parts.length > 0) {
|
||||
if (this.Containers.length > bestCount)// 提前结束,已经超过最佳用板量
|
||||
{
|
||||
this.Fitness = Math.ceil(bestCount) + 1
|
||||
return
|
||||
}
|
||||
|
||||
const container = new Container(this.bin, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)])
|
||||
const holes: Container[] = []
|
||||
const PutPart = (part: Part, greedy: boolean): boolean => {
|
||||
// 先放置在网洞内
|
||||
const isOk
|
||||
= this.OddmentsContainers.some(oc => oc.PutPart(part, greedy)) // 余料
|
||||
|| holes.some((h) => {
|
||||
const isOk = h.PutPart(part, greedy)
|
||||
return isOk
|
||||
}) // 网洞
|
||||
|| container.PutPart(part, greedy) // 大板
|
||||
|
||||
if (isOk)
|
||||
this.AppendHoles(part, holes)
|
||||
|
||||
return isOk
|
||||
}
|
||||
|
||||
if (this.mutationRate > 0.5) // 大板优先,可以提高收敛速度
|
||||
{
|
||||
const maxP = parts.reduce((preP, curP) => preP.State.Contour.Area > curP.State.Contour.Area ? preP : curP)
|
||||
|
||||
const PutGroupF = (): boolean => {
|
||||
// 基因注册
|
||||
const nextPartIndex = parts.findIndex(p => p !== maxP)
|
||||
if (nextPartIndex !== -1) {
|
||||
const nextPart = parts[nextPartIndex]
|
||||
const groups = maxP.ParseGroup(nextPart, this.bin)
|
||||
for (const group of groups) {
|
||||
if (PutPart(group, greedy)) {
|
||||
parts.splice(nextPartIndex, 1)
|
||||
arrayRemoveOnce(parts, maxP)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (GNestConfig.UsePartGroup && PutGroupF()) { }
|
||||
else if (PutPart(maxP, greedy)) { arrayRemoveOnce(parts, maxP) }
|
||||
}
|
||||
|
||||
parts = parts.filter(p => !PutPart(p, greedy))
|
||||
if (!greedy)// 如果没有贪心,排完后在贪心一下
|
||||
parts = parts.filter(p => !PutPart(p, true))
|
||||
|
||||
if (container.PlacedParts.length)
|
||||
this.Containers.push(container)
|
||||
}
|
||||
this.Fitness = this.Containers.length - 1 + (arrayLast(this.Containers)?.UseRatio ?? 0)
|
||||
}
|
||||
|
||||
private AppendHoles(part: Part<any, any>, holes: Container[]) {
|
||||
for (let i = 0; i < part.Holes.length; i++) {
|
||||
const hole = part.Holes[i]
|
||||
const container = new Container(hole.Contour)
|
||||
container.ParentId = part.Id
|
||||
container.ChildrenIndex = i
|
||||
container.ParentM = GNestConfig.RotateHole ? hole.MinPoint : hole.OrigionMinPoint
|
||||
this.HoleContainers.push(container)
|
||||
holes.push(container)
|
||||
}
|
||||
}
|
||||
|
||||
private InitHoleContainers() {
|
||||
if (GNestConfig.RotateHole)
|
||||
return
|
||||
for (const part of this.Parts) {
|
||||
for (let i = 0; i < part.Holes.length; i++) {
|
||||
const hole = part.Holes[i]
|
||||
const container = new Container(hole.Contour)
|
||||
container.ParentId = part.Id
|
||||
container.ChildrenIndex = i
|
||||
container.ParentM = GNestConfig.RotateHole ? hole.MinPoint : hole.OrigionMinPoint
|
||||
this.HoleContainers.push(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Clone() {
|
||||
const p = new Individual(this.Parts.map(p => p.Clone()), this.mutationRate, this.bin, this.OddmentsBins, this.ComparePointKeys)
|
||||
p.Mutate()
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* 突变
|
||||
*/
|
||||
Mutate() {
|
||||
if (this.mutationRate > 0.5) {
|
||||
for (let i = 0; i < this.Parts.length; i++) {
|
||||
const rand = Math.random()
|
||||
if (rand < this.mutationRate * 0.5) {
|
||||
// 和下一个调换顺序
|
||||
const j = i + 1
|
||||
if (j < this.Parts.length)
|
||||
[this.Parts[i], this.Parts[j]] = [this.Parts[j], this.Parts[i]]
|
||||
}
|
||||
if (rand < this.mutationRate)
|
||||
this.Parts[i].Mutate()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 洗牌
|
||||
const rand = Math.random()
|
||||
if (rand < 0.5) {
|
||||
const index = RandomIndex(this.Parts.length - 2)
|
||||
const count = Math.ceil(RandomIndex(this.Parts.length - 2 - index) * this.mutationRate * 2) || 1
|
||||
const parts = this.Parts.splice(index, count)
|
||||
this.Parts.push(...parts)
|
||||
this.Parts[index].Mutate()
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < this.Parts.length; i++) {
|
||||
const rand = Math.random()
|
||||
if (rand < this.mutationRate) {
|
||||
this.Parts[i].Mutate()
|
||||
i += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mutationRate > 0.2)
|
||||
this.mutationRate -= 0.004
|
||||
return this
|
||||
}
|
||||
|
||||
// #region -------------------------File-------------------------
|
||||
|
||||
// 对象从文件中读取数据,初始化自身
|
||||
ReadFile(file: NestFiler) {
|
||||
const ver = file.Read()// ver
|
||||
|
||||
this.Fitness = file.Read()
|
||||
let count = file.Read() as number
|
||||
this.Containers = []
|
||||
for (let i = 0; i < count; i++)
|
||||
this.Containers.push(new Container().ReadFile(file, this.Parts))
|
||||
|
||||
count = file.Read()
|
||||
this.HoleContainers = []
|
||||
for (let i = 0; i < count; i++)
|
||||
this.HoleContainers.push(new Container().ReadFile(file, this.Parts))
|
||||
|
||||
count = file.Read()
|
||||
this.OddmentsContainers = []
|
||||
for (let i = 0; i < count; i++)
|
||||
this.OddmentsContainers.push(new Container().ReadFile(file, this.Parts))
|
||||
}
|
||||
|
||||
// 对象将自身数据写入到文件.
|
||||
WriteFile(f: NestFiler) {
|
||||
f.Write(1)// ver
|
||||
f.Write(this.Fitness)
|
||||
f.Write(this.Containers.length)
|
||||
for (const c of this.Containers)
|
||||
c.WriteFile(f)
|
||||
|
||||
f.Write(this.HoleContainers.length)
|
||||
for (const c of this.HoleContainers)
|
||||
c.WriteFile(f)
|
||||
|
||||
f.Write(this.OddmentsContainers.length)
|
||||
for (const c of this.OddmentsContainers)
|
||||
c.WriteFile(f)
|
||||
}
|
||||
// #endregion
|
||||
}
|
37
tests/dev1/dataHandle/common/core/NestCache.ts
Normal file
37
tests/dev1/dataHandle/common/core/NestCache.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Point } from '../Vector2'
|
||||
import { Path } from './Path'
|
||||
|
||||
export class NestCache
|
||||
{
|
||||
static count1 = 0
|
||||
static count2 = 0// noset
|
||||
|
||||
static PositionCache: { [key: string]: Point } = {}
|
||||
static NoPutCache: { [key: string]: Set<number> } = {}
|
||||
private static CacheRect = new Map<string, Path>()
|
||||
|
||||
/**
|
||||
* 用于创建原点在0点的矩形路径
|
||||
*/
|
||||
static CreatePath(x: number, y: number, knifRadius = 3.5): Path
|
||||
{
|
||||
let minX = -knifRadius
|
||||
let maxX = x + knifRadius
|
||||
let minY = -knifRadius
|
||||
let maxY = y + knifRadius
|
||||
return new Path([
|
||||
{ x: minX, y: minY },
|
||||
{ x: maxX, y: minY },
|
||||
{ x: maxX, y: maxY },
|
||||
{ x: minX, y: maxY },
|
||||
])
|
||||
}
|
||||
|
||||
static Clear()
|
||||
{
|
||||
this.count1 = 0
|
||||
this.count2 = 0
|
||||
this.CacheRect.clear()
|
||||
this.PositionCache = {}
|
||||
}
|
||||
}
|
82
tests/dev1/dataHandle/common/core/NestDatabase.ts
Normal file
82
tests/dev1/dataHandle/common/core/NestDatabase.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NestFiler } from '../Filer'
|
||||
import { Part } from './Part'
|
||||
import { Path } from './Path'
|
||||
import { PathGeneratorSingle } from './PathGenerator'
|
||||
|
||||
export const DefaultComparePointKeys = ['xy', 'yx']
|
||||
|
||||
/**
|
||||
* 排料数据库,用这个类来序列化需要排料的数据
|
||||
* 用于在Work间传输
|
||||
*/
|
||||
export class NestDatabase {
|
||||
Bin: Path // 默认的容器
|
||||
OddmentsBins: Path[]// 余料容器列表
|
||||
Paths: Path[] // 所有的Path都在这里
|
||||
Parts: Part[] // 所有的零件
|
||||
ComparePointKeys: string[] = DefaultComparePointKeys// 用来决定零件靠边模式
|
||||
|
||||
// #region -------------------------File-------------------------
|
||||
// 对象从文件中读取数据,初始化自身
|
||||
ReadFile(file: NestFiler) {
|
||||
const ver = file.Read()
|
||||
let count = file.Read() as number
|
||||
this.Paths = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const path = new Path()
|
||||
path.ReadFile(file)
|
||||
this.Paths.push(path)
|
||||
}
|
||||
this.Bin = this.Paths[file.Read()]
|
||||
PathGeneratorSingle.paths = this.Paths
|
||||
count = file.Read()
|
||||
this.Parts = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const part = new Part()
|
||||
part.ReadFile(file)
|
||||
this.Parts.push(part)
|
||||
}
|
||||
|
||||
count = file.Read()
|
||||
this.OddmentsBins = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const path = new Path()
|
||||
path.ReadFile(file)
|
||||
this.OddmentsBins.push(path)
|
||||
}
|
||||
|
||||
if (ver > 1)
|
||||
this.ComparePointKeys = file.Read()
|
||||
return this
|
||||
}
|
||||
|
||||
// 对象将自身数据写入到文件.
|
||||
WriteFile(file: NestFiler) {
|
||||
file.Write(2)
|
||||
file.Write(this.Paths.length)
|
||||
for (const path of this.Paths)
|
||||
path.WriteFile(file)
|
||||
|
||||
file.Write(this.Bin.Id)
|
||||
file.Write(this.Parts.length)
|
||||
for (const part of this.Parts)
|
||||
part.WriteFile(file)
|
||||
|
||||
if (!this.OddmentsBins)
|
||||
this.OddmentsBins = []
|
||||
file.Write(this.OddmentsBins.length)
|
||||
for (const path of this.OddmentsBins)
|
||||
path.WriteFile(file)
|
||||
|
||||
file.Write(this.ComparePointKeys)
|
||||
|
||||
return this
|
||||
}
|
||||
// #endregion
|
||||
|
||||
get File() {
|
||||
const f = new NestFiler()
|
||||
this.WriteFile(f)
|
||||
return f
|
||||
}
|
||||
}
|
163
tests/dev1/dataHandle/common/core/OptimizeMachine.ts
Normal file
163
tests/dev1/dataHandle/common/core/OptimizeMachine.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { arrayRemoveIf } from '../ArrayExt'
|
||||
import { clipperCpp } from '../ClipperCpp'
|
||||
import { Sleep } from '../Sleep'
|
||||
import { Individual } from './Individual'
|
||||
import { NestCache } from './NestCache'
|
||||
import { DefaultComparePointKeys } from './NestDatabase'
|
||||
import type { Part } from './Part'
|
||||
import type { Path } from './Path'
|
||||
|
||||
/**
|
||||
* 优化器
|
||||
* 配置优化器
|
||||
* 放入零件
|
||||
* 按下启动
|
||||
* 按下暂停
|
||||
* 清理机台
|
||||
*/
|
||||
export class OptimizeMachine {
|
||||
// 配置
|
||||
Config: {
|
||||
PopulationCount: number// 种群个数
|
||||
}
|
||||
|
||||
Bin: Path // 默认的容器
|
||||
OddmentsBins: Path[]// 余料容器列表
|
||||
Parts: Part[] // 所有的零件
|
||||
ComparePointKeys: string[] = DefaultComparePointKeys// 用来决定零件靠边模式
|
||||
|
||||
// //计算重复的零件 TODO:需要对相同的零件提取出来
|
||||
// PartCount: number[] = [];
|
||||
|
||||
private _IsSuspend = false
|
||||
|
||||
protected _Individuals: Individual[]// 个体列表
|
||||
|
||||
constructor() {
|
||||
this.Config = { PopulationCount: 50 }
|
||||
}
|
||||
|
||||
// 放入零件
|
||||
PutParts(parts: Part[]) {
|
||||
if (globalThis.document)
|
||||
parts = parts.slice()
|
||||
arrayRemoveIf(parts, p => p.RotatedStates.length === 0)
|
||||
this.Parts = parts
|
||||
|
||||
// //计算重复的零件(暂时不用)
|
||||
// for (let part of parts)
|
||||
// {
|
||||
// let count = this.PartCount[part.Id];
|
||||
// this.PartCount[part.Id] = count === undefined ? 1 : (count + 1);
|
||||
// }
|
||||
}
|
||||
|
||||
callBack: (i: Individual) => Promise<void>
|
||||
|
||||
// 启动
|
||||
async Start() {
|
||||
if (this.Parts.length === 0)
|
||||
return
|
||||
console.log(this.Parts.length)
|
||||
this._IsSuspend = false
|
||||
NestCache.Clear()
|
||||
this.Parts.sort((p1, p2) => p2.State.Contour.Area - p1.State.Contour.Area)
|
||||
this._Individuals = [new Individual(this.Parts, 0.8, this.Bin, this.OddmentsBins, this.ComparePointKeys)]
|
||||
for (let i = 1; i < this.Config.PopulationCount; i++) {
|
||||
const parts = this.Parts.map(p => p.Clone())
|
||||
if (i < 3) {
|
||||
for (let i = parts.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[parts[i], parts[j]] = [parts[j], parts[i]]
|
||||
parts[i].Mutate()
|
||||
}
|
||||
}
|
||||
this._Individuals.push(new Individual(parts, 0.8, this.Bin, this.OddmentsBins, this.ComparePointKeys))
|
||||
}
|
||||
// 2.执行
|
||||
await this.Run()
|
||||
}
|
||||
|
||||
calcCount = 0
|
||||
best = Number.POSITIVE_INFINITY
|
||||
bestP: Individual
|
||||
bestCount = 0
|
||||
private async Run() {
|
||||
console.time('1')
|
||||
if (this.Parts.length === 0)
|
||||
return
|
||||
// 开始自然选择
|
||||
while (!this._IsSuspend) // 实验停止信号
|
||||
{
|
||||
const goBack = this.calcCount - this.bestCount > 8000
|
||||
// 1.适应环境(放置零件)
|
||||
for (let i = 0; i < this._Individuals.length; i++) {
|
||||
if (globalThis.document || !clipperCpp.lib)
|
||||
await Sleep(0)
|
||||
const p = this._Individuals[i]
|
||||
if (this.calcCount < 1000 || this.calcCount % 1000 === 0)
|
||||
p.Evaluate(this.best, true, i % 3)
|
||||
else
|
||||
p.Evaluate(this.best)
|
||||
|
||||
if (!this.bestP || p.Fitness < this.bestP.Fitness) {
|
||||
this.bestP = p
|
||||
this.best = p.Fitness
|
||||
await this.callBack(p)
|
||||
}
|
||||
if (goBack)
|
||||
p.mutationRate = 0.5
|
||||
}
|
||||
|
||||
this.calcCount += this._Individuals.length
|
||||
if (this.calcCount % 100 === 0) {
|
||||
await Sleep(0)
|
||||
}
|
||||
|
||||
this._Individuals.sort((i1, i2) => i1.Fitness - i2.Fitness)
|
||||
const bestP = this._Individuals[0]
|
||||
// //回调最好的
|
||||
// if (bestP.Fitness < this.best)
|
||||
// {
|
||||
// this.best = bestP.Fitness;
|
||||
// this.bestP = bestP;
|
||||
// // console.timeLog("1", this.best, this.calcCount, NestCache.count1, NestCache.count2);
|
||||
// if (this.callBack)
|
||||
// await this.callBack(bestP);
|
||||
// }
|
||||
|
||||
// 自然选择
|
||||
this._Individuals.splice(-10)// 杀死它
|
||||
for (let i = 0; i < 4; i++)
|
||||
this._Individuals.push(bestP.Clone())
|
||||
this._Individuals.push(this.bestP.Clone())
|
||||
for (let i = 0; i < 3; i++)
|
||||
this._Individuals.push(this._Individuals[1].Clone())
|
||||
for (let i = 0; i < 2; i++)
|
||||
this._Individuals.push(this._Individuals[2].Clone())
|
||||
// 全部突变
|
||||
for (const p of this._Individuals) {
|
||||
if (p.Fitness !== undefined)
|
||||
p.Mutate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停
|
||||
Suspend() {
|
||||
console.timeEnd('1')
|
||||
console.log(this.best, this.calcCount, NestCache.count1)
|
||||
this._IsSuspend = true
|
||||
console.log('暂停')
|
||||
}
|
||||
|
||||
// 继续
|
||||
Continue() {
|
||||
this._IsSuspend = false
|
||||
this.Run()
|
||||
}
|
||||
|
||||
// 清理机台
|
||||
Clear() {
|
||||
}
|
||||
}
|
238
tests/dev1/dataHandle/common/core/ParseOddments.ts
Normal file
238
tests/dev1/dataHandle/common/core/ParseOddments.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { ClipType, EndType, JoinType, PolyFillType } from 'js-angusj-clipper/web'
|
||||
import type { ClipInput } from 'js-angusj-clipper/web'
|
||||
import { Box2 } from '..//Box2'
|
||||
import { clipperCpp } from '../ClipperCpp'
|
||||
import type { Container } from './Container'
|
||||
import { NestCache } from './NestCache'
|
||||
import { Path, PathScale, TranslatePath, TranslatePath_Self } from './Path'
|
||||
|
||||
const SquarePath = NestCache.CreatePath(60, 60, 0)
|
||||
const CanPutPaths = [
|
||||
NestCache.CreatePath(200, 200, 0),
|
||||
NestCache.CreatePath(600, 100, 0),
|
||||
NestCache.CreatePath(100, 600, 0),
|
||||
]
|
||||
|
||||
/**
|
||||
* 分析排料结果的余料
|
||||
* @param container 排料结果的容器
|
||||
* @param binPath 容器的bin
|
||||
* @param [knifeRadius] 刀半径(以便我们再次偏移)
|
||||
* @param squarePath 使用一个正方形路径来简化余料轮廓
|
||||
* @param canPutPaths 使用可以放置的路径列表来测试余料是否可用,如果可用,则保留
|
||||
* @returns Path[] 轮廓的位置存储在OrigionMinPoint中
|
||||
*/
|
||||
export function ParseOddments(container: Container, binPath: Path, knifeRadius: number = 3.5, squarePath: Path = SquarePath, canPutPaths: Path[] = CanPutPaths): Path[]
|
||||
{
|
||||
// 构建轮廓数据
|
||||
let partPaths: ClipInput[] = container.PlacedParts.map((part) =>
|
||||
{
|
||||
// 直接在这里偏移,而不缓存,应该没有性能问题
|
||||
let newPts = clipperCpp.lib.offsetToPaths({
|
||||
delta: knifeRadius * 1e4,
|
||||
offsetInputs: [{ data: part.State.Contour.BigIntPoints, joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
|
||||
})[0]
|
||||
|
||||
let path = TranslatePath(newPts, { x: part.PlacePosition.x - 5e3, y: part.PlacePosition.y - 5e3 })// 因为移动了0.5,0.5,所以这里也要移动0.5
|
||||
return { data: path }
|
||||
})
|
||||
// console.log('构建轮廓数据', partPaths)
|
||||
// 所有的余料(使用布尔差集)
|
||||
let oddmentsPolygon = clipperCpp.lib.clipToPolyTree({
|
||||
subjectInputs: [{ data: binPath.BigIntPoints, closed: true }],
|
||||
clipInputs: partPaths,
|
||||
clipType: ClipType.Difference,
|
||||
subjectFillType: PolyFillType.NonZero,
|
||||
})
|
||||
|
||||
// 现在我们用树状结构,应该不会自交了?(文档写了,返回的结果不可能重叠或者自交)
|
||||
// 简化结果,避免自交
|
||||
// oddmentsPolygon = clipperCpp.lib.simplifyPolygons(oddmentsPolygon);
|
||||
|
||||
function CreatePolygon(minx: number, miny: number, maxx: number, maxy: number)
|
||||
{
|
||||
return [
|
||||
{ x: minx, y: miny },
|
||||
{ x: maxx, y: miny },
|
||||
{ x: maxx, y: maxy },
|
||||
{ x: minx, y: maxy },
|
||||
]
|
||||
}
|
||||
|
||||
let clipedPaths: Path[] = []// 已经减去网洞投影的余料轮廓列表
|
||||
|
||||
// 由于手动排版可能造成余料网洞,我们将网洞的盒子投影,然后裁剪余料,避免余料有网洞
|
||||
for (let node of oddmentsPolygon.childs)
|
||||
{
|
||||
let nodePolygon = node.contour
|
||||
// 减去网洞
|
||||
if (node.childs.length)
|
||||
{
|
||||
let box = new Box2().setFromPoints(nodePolygon)
|
||||
|
||||
let childBoxPolygon = node.childs.map((cnode) =>
|
||||
{
|
||||
let cbox = new Box2().setFromPoints(cnode.contour)
|
||||
let type = 0// 0左1右2上3下
|
||||
let minDist = Number.POSITIVE_INFINITY
|
||||
|
||||
let letftDist = cbox.min.x - box.min.x
|
||||
let rightDist = box.max.x - cbox.max.x
|
||||
let topDist = box.max.y - cbox.max.y
|
||||
let downDist = cbox.min.y - box.min.y
|
||||
|
||||
if (rightDist < letftDist)
|
||||
{
|
||||
type = 1
|
||||
minDist = rightDist
|
||||
}
|
||||
|
||||
if (topDist < minDist)
|
||||
{
|
||||
type = 2
|
||||
minDist = topDist
|
||||
}
|
||||
|
||||
if (downDist < minDist)
|
||||
type = 3
|
||||
|
||||
if (type === 0)
|
||||
return CreatePolygon(box.min.x, cbox.min.y, cbox.max.x, cbox.max.y)
|
||||
if (type === 1)
|
||||
return CreatePolygon(cbox.min.x, cbox.min.y, box.max.x, cbox.max.y)
|
||||
if (type === 2)
|
||||
return CreatePolygon(cbox.min.x, cbox.min.y, cbox.max.x, box.max.y)
|
||||
if (type === 3)
|
||||
return CreatePolygon(cbox.min.x, box.min.y, cbox.max.x, cbox.max.y)
|
||||
})
|
||||
|
||||
let splits = clipperCpp.lib.clipToPaths({
|
||||
subjectInputs: [{ data: nodePolygon, closed: true }],
|
||||
clipInputs: childBoxPolygon.map((polygon) => { return { data: polygon } }),
|
||||
clipType: ClipType.Difference,
|
||||
subjectFillType: PolyFillType.NonZero,
|
||||
})
|
||||
|
||||
for (let p of splits)
|
||||
clipedPaths.push(new Path(PathScale(p, 1e-4)))
|
||||
}
|
||||
else
|
||||
clipedPaths.push(new Path(node.contour.map((p) => { return { x: p.x * 1e-4, y: p.y * 1e-4 } })))
|
||||
}
|
||||
|
||||
let OddmentsPaths: Path[] = []
|
||||
for (let polygonPath of clipedPaths)
|
||||
{
|
||||
// 先获取内部的nfp
|
||||
let insideNFPS = polygonPath.GetInsideNFP(squarePath)
|
||||
|
||||
if (!insideNFPS)
|
||||
continue
|
||||
|
||||
let beferPolygons: ClipInput[] = []
|
||||
|
||||
for (let nfp of insideNFPS)
|
||||
{
|
||||
let nfpPath = new Path(PathScale(nfp, 1e-4))
|
||||
// 通过内部nfp还原实际轮廓
|
||||
let sumPolygons = clipperCpp.lib.minkowskiSumPath(nfpPath.BigIntPoints, squarePath.BigIntPoints, true)
|
||||
sumPolygons = clipperCpp.lib.simplifyPolygons(sumPolygons)
|
||||
|
||||
for (let poly of sumPolygons)
|
||||
{
|
||||
if (clipperCpp.lib.area(poly) < 0)
|
||||
continue// 移除内部的,无意义的
|
||||
|
||||
let tempPath = new Path(poly.map((p) => { return { x: p.x * 1e-4, y: p.y * 1e-4 } }))// 这里new一个新的,下面就复用这个
|
||||
if (canPutPaths.some(p => tempPath.GetInsideNFP(p)?.length))// 能塞的下指定的轮廓才会被留下
|
||||
{
|
||||
if (beferPolygons.length)
|
||||
{
|
||||
// 移动到实际位置
|
||||
TranslatePath_Self(poly, (polygonPath.OrigionMinPoint.x + nfpPath.OrigionMinPoint.x) * 1e4, (polygonPath.OrigionMinPoint.y + nfpPath.OrigionMinPoint.y) * 1e4)
|
||||
|
||||
// 在这里裁剪之前的余料轮廓
|
||||
let tree = clipperCpp.lib.clipToPolyTree({
|
||||
subjectInputs: [{ data: poly, closed: true }],
|
||||
clipInputs: beferPolygons,
|
||||
clipType: ClipType.Difference,
|
||||
subjectFillType: PolyFillType.NonZero,
|
||||
})
|
||||
|
||||
for (let node of tree.childs)
|
||||
{
|
||||
if (node.childs.length)
|
||||
continue
|
||||
|
||||
tempPath = new Path(node.contour.map((p) => { return { x: p.x * 1e-4, y: p.y * 1e-4 } }))
|
||||
|
||||
// 继续简化
|
||||
tempPath = SimplifyPathOfSqPath(tempPath, squarePath)
|
||||
|
||||
if (!tempPath)
|
||||
continue
|
||||
|
||||
OddmentsPaths.push(tempPath)
|
||||
|
||||
// 偏移2把刀
|
||||
let offsetedPolygon = clipperCpp.lib.offsetToPaths({
|
||||
delta: knifeRadius * 2e4,
|
||||
offsetInputs: [{ data: node.contour, joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
|
||||
})[0]
|
||||
beferPolygons.push({ data: offsetedPolygon })// 用于裁剪后续的余料
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 设置轮廓的位置
|
||||
tempPath.OrigionMinPoint.x += nfpPath.OrigionMinPoint.x + polygonPath.OrigionMinPoint.x
|
||||
tempPath.OrigionMinPoint.y += nfpPath.OrigionMinPoint.y + polygonPath.OrigionMinPoint.y
|
||||
OddmentsPaths.push(tempPath)
|
||||
|
||||
// 将余料轮廓加入到裁剪轮廓中,用于裁剪后续的余料
|
||||
if (insideNFPS.length)
|
||||
{
|
||||
// 移动到实际位置
|
||||
TranslatePath_Self(poly, (polygonPath.OrigionMinPoint.x + nfpPath.OrigionMinPoint.x) * 1e4, (polygonPath.OrigionMinPoint.y + nfpPath.OrigionMinPoint.y) * 1e4)
|
||||
// 偏移2把刀
|
||||
let offsetedPolygon = clipperCpp.lib.offsetToPaths({
|
||||
delta: knifeRadius * 2e4,
|
||||
offsetInputs: [{ data: poly, joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
|
||||
})[0]
|
||||
beferPolygons.push({ data: offsetedPolygon })// 用于裁剪后续的余料
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log('ParseOddments end', OddmentsPaths)
|
||||
return OddmentsPaths
|
||||
}
|
||||
|
||||
// 使用矩形轮廓来简化余料轮廓(通常一进一出)
|
||||
function SimplifyPathOfSqPath(polygonPath: Path, sqPath: Path): Path | undefined
|
||||
{
|
||||
// 先获取内部的nfp
|
||||
let insideNFPS = polygonPath.GetInsideNFP(sqPath)
|
||||
if (insideNFPS.length > 0)// 目前一般只有1个,不知道会不会有多个
|
||||
{
|
||||
let nfp = insideNFPS[0]
|
||||
let nfpPath = new Path(PathScale(nfp, 1e-4))
|
||||
// 通过内部nfp还原实际轮廓
|
||||
let sumPolygons = clipperCpp.lib.minkowskiSumPath(nfpPath.BigIntPoints, sqPath.BigIntPoints, true)
|
||||
sumPolygons = clipperCpp.lib.simplifyPolygons(sumPolygons)
|
||||
|
||||
for (let poly of sumPolygons)// 通常是一个内部的+一个外部的
|
||||
{
|
||||
if (clipperCpp.lib.area(poly) < 0)
|
||||
continue// 移除内部的,无意义的
|
||||
|
||||
let tempPath = new Path(PathScale(poly, 1e-4))
|
||||
|
||||
tempPath.OrigionMinPoint.x += nfpPath.OrigionMinPoint.x + polygonPath.OrigionMinPoint.x
|
||||
tempPath.OrigionMinPoint.y += nfpPath.OrigionMinPoint.y + polygonPath.OrigionMinPoint.y
|
||||
return tempPath
|
||||
}
|
||||
}
|
||||
}
|
394
tests/dev1/dataHandle/common/core/Part.ts
Normal file
394
tests/dev1/dataHandle/common/core/Part.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import type { Box2 } from '../Box2'
|
||||
import type { NestFiler } from '../Filer'
|
||||
import type { Point } from '../Vector2'
|
||||
import { RandomIndex } from '../Random'
|
||||
import { FixIndex } from '../Util'
|
||||
import { Vector2 } from '../Vector2'
|
||||
import { GNestConfig } from './GNestConfig'
|
||||
import { NestCache } from './NestCache'
|
||||
import { PartState } from './PartState'
|
||||
import { Path } from './Path'
|
||||
import { PathGeneratorSingle } from './PathGenerator'
|
||||
|
||||
const EmptyArray = []
|
||||
|
||||
/**
|
||||
* 零件类
|
||||
* 零件类可以绑定数据,也存在位置和旋转状态的信息
|
||||
*
|
||||
* 初始化零件:
|
||||
* 传入零件的轮廓,刀半径,包围容器(或者为空?)
|
||||
* 初始化用于放置的轮廓。将轮廓首点移动到0点,记录移动的点P。
|
||||
*
|
||||
* 零件放置位置:
|
||||
* 表示零件轮廓首点的位置。
|
||||
*
|
||||
* 零件的旋转:
|
||||
* 表示零件轮廓按照首点(0)旋转。
|
||||
*
|
||||
* 还原零件的放置状态:
|
||||
* 同样将零件移动到0点
|
||||
* 同样将零件旋转
|
||||
* 同样将零件移动到指定的位置
|
||||
* 零件可能处于容器中,变换到容器坐标系
|
||||
*
|
||||
*/
|
||||
export class Part<T = any, Matrix = any>
|
||||
{
|
||||
Id: number// 用于确定Part的唯一性,并且有助于在其他Work中还原
|
||||
private _Holes: PartState[] = []
|
||||
_RotateHoles: PartState[][] = []
|
||||
// 零件的不同旋转状态,这个数组会在所有的状态间共享,以便快速切换状态
|
||||
StateIndex = 0 // 旋转状态
|
||||
RotatedStates: PartState[] = []// 可能的旋转状态列表
|
||||
PlacePosition: Point // 放置位置(相对于容器的位置)
|
||||
|
||||
HolePosition: Point//
|
||||
|
||||
// #region 临时数据(不会被序列化到优化线程)
|
||||
UserData: T// 只用于还原零件的显示状态(或者关联到实际数据)
|
||||
Parent: Part// 如果这个零件放置在了网洞中,这个Parent表示这个网洞所属的零件,我们可以得到这个零件的放置信息,并且可以从Container中的ParentM得到网洞相对于零件的位置
|
||||
PlaceCS: Matrix// 放置矩阵 Matrix4
|
||||
PlaceIndex: number// 放置的大板索引
|
||||
// #endregion
|
||||
|
||||
GroupMap: { [key: number]: Part[] } = {}
|
||||
get State(): PartState // 零件当前的状态
|
||||
{
|
||||
return this.RotatedStates[this.StateIndex]
|
||||
}
|
||||
|
||||
get Holes(): PartState[]
|
||||
{
|
||||
if (GNestConfig.RotateHole)
|
||||
return this._RotateHoles[this.StateIndex] || EmptyArray
|
||||
return this._Holes
|
||||
}
|
||||
|
||||
// 初始化零件的各个状态,按360度旋转个数
|
||||
Init(path: Path, bin: Path, rotateCount = 4): this
|
||||
{
|
||||
let rotations: number[] = []
|
||||
let a = 2 * Math.PI / rotateCount
|
||||
for (let i = 0; i < rotateCount; i++)
|
||||
{
|
||||
rotations.push(a * i)
|
||||
}
|
||||
this.Init2(path, bin, rotations)
|
||||
return this
|
||||
}
|
||||
|
||||
// 初始化零件的各个状态,按旋转角度表
|
||||
Init2(path: Path, bin: Path, rotations: number[] = []): this
|
||||
{
|
||||
let pathP = path.OrigionMinPoint
|
||||
let path_0 = PathGeneratorSingle.Allocate(path)
|
||||
let pathSet = new Set<Path>()
|
||||
|
||||
// 初始化零件的状态集合
|
||||
for (let pa of rotations)
|
||||
{
|
||||
let partState = new PartState()
|
||||
partState.Rotation = pa
|
||||
if (pa === 0)
|
||||
{
|
||||
partState.Contour = path_0
|
||||
partState.OrigionMinPoint = pathP
|
||||
partState.MinPoint = path.OrigionMinPoint
|
||||
}
|
||||
else
|
||||
{
|
||||
let path_r = new Path(path.Points, pa)
|
||||
partState.Contour = PathGeneratorSingle.Allocate(path_r)
|
||||
partState.Contour.Area = path_0.Area
|
||||
// 避免重复的Path进入State
|
||||
if (pathSet.has(partState.Contour))
|
||||
continue
|
||||
let p0 = path_r.OrigionMinPoint
|
||||
let c = Math.cos(-pa)
|
||||
let s = Math.sin(-pa)
|
||||
let x1 = p0.x * c - p0.y * s
|
||||
let y1 = p0.x * s + p0.y * c
|
||||
partState.OrigionMinPoint = new Vector2(pathP.x + x1, pathP.y + y1)
|
||||
|
||||
// 计算正确的最小点
|
||||
let tempPath = new Path(path.OrigionPoints, pa)
|
||||
partState.MinPoint = tempPath.OrigionMinPoint
|
||||
}
|
||||
// 记录已有Path
|
||||
pathSet.add(partState.Contour)
|
||||
// 必须能放置
|
||||
if (bin.GetInsideNFP(partState.Contour))
|
||||
{
|
||||
this.RotatedStates.push(partState)
|
||||
PathGeneratorSingle.RegisterId(partState.Contour)
|
||||
}
|
||||
}
|
||||
|
||||
// 为了复用NFP,不管第0个Path是否可用,都注册它.
|
||||
if (this.RotatedStates.length > 4)
|
||||
PathGeneratorSingle.RegisterId(path_0)
|
||||
return this
|
||||
}
|
||||
|
||||
ParseGroup(partOther: Part, bin: Path): Part[]
|
||||
{
|
||||
let arr = this.GroupMap[partOther.Id]
|
||||
if (arr)
|
||||
return arr
|
||||
|
||||
arr = []
|
||||
if (this.Holes.length || partOther.Holes.length)
|
||||
return arr
|
||||
this.GroupMap[partOther.Id] = arr
|
||||
if (this.State.Contour.IsRect || partOther.State.Contour.IsRect)
|
||||
return arr
|
||||
if (this.State.Contour.Area > this.State.Contour.BoundingBox.area * 0.9)
|
||||
return arr
|
||||
if (partOther.State.Contour.Area > partOther.State.Contour.BoundingBox.area * 0.9)
|
||||
return arr
|
||||
|
||||
for (let i = 0; i < this.RotatedStates.length; i++)
|
||||
{
|
||||
let s1 = this.RotatedStates[i]
|
||||
for (let j = 1; j < partOther.RotatedStates.length; j++)
|
||||
{
|
||||
let s2 = partOther.RotatedStates[j]
|
||||
let nfps = s1.Contour.GetOutsideNFP(s2.Contour)
|
||||
for (let nfp of nfps)
|
||||
{
|
||||
for (let k = 0; k < nfp.length * 2; k++)
|
||||
{
|
||||
let p: Point
|
||||
if (k % 2 === 0)
|
||||
{
|
||||
p = { ...nfp[k / 2] }
|
||||
}
|
||||
else
|
||||
{
|
||||
let p1 = nfp[FixIndex(k / 2 - 0.5, nfp)]
|
||||
let p2 = nfp[FixIndex(k / 2 + 0.5, nfp)]
|
||||
p = { x: p1.x + p2.x, y: p1.y + p2.y }
|
||||
p.x *= 0.5
|
||||
p.y *= 0.5
|
||||
}
|
||||
p.x *= 1e-4
|
||||
p.y *= 1e-4
|
||||
|
||||
let newBox = s2.Contour.BoundingBox.clone().translate(p)
|
||||
newBox.union(s1.Contour.BoundingBox)
|
||||
|
||||
if (newBox.area < (s1.Contour.Area + s2.Contour.Area) * 1.3)
|
||||
{
|
||||
let partGroup = new PartGroup(this, partOther, i, j, p, newBox, bin)
|
||||
if (partGroup.RotatedStates.length > 0
|
||||
&& !arr.some(p => p.State.Contour === partGroup.State.Contour)// 类似的
|
||||
)
|
||||
{
|
||||
arr.push(partGroup)
|
||||
return arr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 添加网洞
|
||||
AppendHole(path: Path)
|
||||
{
|
||||
let hole = new PartState()
|
||||
hole.Contour = PathGeneratorSingle.Allocate(path)
|
||||
PathGeneratorSingle.RegisterId(hole.Contour)
|
||||
hole.OrigionMinPoint = path.OrigionMinPoint
|
||||
hole.Rotation = 0
|
||||
this._Holes.push(hole)
|
||||
|
||||
if (GNestConfig.RotateHole)
|
||||
for (let i = 0; i < this.RotatedStates.length; i++)
|
||||
{
|
||||
let r = this.RotatedStates[i].Rotation
|
||||
let arr = this._RotateHoles[i]
|
||||
if (!arr)
|
||||
{
|
||||
arr = []
|
||||
this._RotateHoles[i] = arr
|
||||
}
|
||||
|
||||
if (r === 0)
|
||||
{
|
||||
hole.MinPoint = path.OrigionMinPoint
|
||||
arr.push(hole)
|
||||
}
|
||||
else
|
||||
{
|
||||
let newPath = new Path(path.Points, r)
|
||||
let newHole = new PartState()
|
||||
newHole.Rotation = r
|
||||
newHole.Contour = PathGeneratorSingle.Allocate(newPath)
|
||||
PathGeneratorSingle.RegisterId(newHole.Contour)
|
||||
newHole.OrigionMinPoint = newPath.OrigionMinPoint
|
||||
|
||||
// 计算正确的最小点
|
||||
let tempPath = new Path(path.OrigionPoints, r)
|
||||
newHole.MinPoint = tempPath.OrigionMinPoint
|
||||
|
||||
arr.push(newHole)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:因为现在实现的是左右翻转,所以会出现角度匹配不完全的问题(缺失上下翻转)
|
||||
Mirror(doubleFace: boolean)
|
||||
{
|
||||
let states = this.RotatedStates
|
||||
let holes = this._Holes
|
||||
let roholes = this._RotateHoles
|
||||
if (!doubleFace)
|
||||
{
|
||||
this.RotatedStates = []
|
||||
this._Holes = []
|
||||
this._RotateHoles = []
|
||||
}
|
||||
let count = states.length
|
||||
for (let i = 0; i < count; i++)
|
||||
{
|
||||
let s = states[i]
|
||||
let ns = s.Mirror()
|
||||
if (ns && !this.RotatedStates.some(state => state.Contour === s.Contour))
|
||||
{
|
||||
this.RotatedStates.push(ns)
|
||||
|
||||
if (this._Holes.length > 0)
|
||||
this._Holes.push(holes[i].Mirror())
|
||||
|
||||
if (this._RotateHoles.length > 0)
|
||||
this._RotateHoles.push(roholes[i].map(s => s.Mirror()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 浅克隆
|
||||
Clone()
|
||||
{
|
||||
let part = new Part()
|
||||
part.Id = this.Id
|
||||
part.UserData = this.UserData
|
||||
part.RotatedStates = this.RotatedStates
|
||||
part.StateIndex = this.StateIndex
|
||||
part._Holes = this._Holes
|
||||
part._RotateHoles = this._RotateHoles
|
||||
return part
|
||||
}
|
||||
|
||||
// 旋转起来,改变自身旋转状态(变异)
|
||||
Mutate(): this
|
||||
{
|
||||
this.StateIndex = RandomIndex(this.RotatedStates.length, this.StateIndex)
|
||||
return this
|
||||
}
|
||||
|
||||
// #region -------------------------File-------------------------
|
||||
ReadFile(file: NestFiler)
|
||||
{
|
||||
this.Id = file.Read()
|
||||
let count = file.Read() as number
|
||||
this.RotatedStates = []
|
||||
for (let i = 0; i < count; i++)
|
||||
{
|
||||
let state = new PartState()
|
||||
state.ReadFile(file)
|
||||
this.RotatedStates.push(state)
|
||||
}
|
||||
|
||||
// 无旋转网洞
|
||||
count = file.Read()
|
||||
this._Holes = []
|
||||
for (let i = 0; i < count; i++)
|
||||
{
|
||||
let state = new PartState()
|
||||
state.ReadFile(file)
|
||||
this._Holes.push(state)
|
||||
}
|
||||
|
||||
// 旋转网洞
|
||||
count = file.Read()
|
||||
this._RotateHoles = []
|
||||
for (let i = 0; i < count; i++)
|
||||
{
|
||||
let count2 = file.Read() as number
|
||||
|
||||
let holes: PartState[] = []
|
||||
for (let j = 0; j < count2; j++)
|
||||
{
|
||||
let state = new PartState()
|
||||
state.ReadFile(file)
|
||||
holes.push(state)
|
||||
}
|
||||
this._RotateHoles.push(holes)
|
||||
}
|
||||
}
|
||||
|
||||
WriteFile(file: NestFiler)
|
||||
{
|
||||
file.Write(this.Id)
|
||||
file.Write(this.RotatedStates.length)
|
||||
for (let state of this.RotatedStates)
|
||||
state.WriteFile(file)
|
||||
|
||||
// 非旋转网洞
|
||||
file.Write(this._Holes.length)
|
||||
for (let hole of this._Holes)
|
||||
hole.WriteFile(file)
|
||||
|
||||
// 写入旋转网洞
|
||||
file.Write(this._RotateHoles.length)
|
||||
for (let holes of this._RotateHoles)
|
||||
{
|
||||
file.Write(holes.length)
|
||||
for (let hole of holes)
|
||||
{
|
||||
hole.WriteFile(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// 零件组合
|
||||
export class PartGroup extends Part
|
||||
{
|
||||
constructor(public part1: Part,
|
||||
public part2: Part,
|
||||
public index1: number,
|
||||
public index2: number,
|
||||
public p: Point,
|
||||
public box: Box2,
|
||||
bin: Path,
|
||||
)
|
||||
{
|
||||
super()
|
||||
let size = box.getSize(new Vector2())
|
||||
this.Init2(NestCache.CreatePath(size.x, size.y, 0), bin, [0])
|
||||
}
|
||||
|
||||
Export(): Part[]
|
||||
{
|
||||
this.part1.StateIndex = this.index1
|
||||
this.part2.StateIndex = this.index2
|
||||
|
||||
this.part1.PlacePosition = {
|
||||
x: this.PlacePosition.x - this.box.min.x * 1e4,
|
||||
y: this.PlacePosition.y - this.box.min.y * 1e4,
|
||||
}
|
||||
|
||||
this.part2.PlacePosition = {
|
||||
x: this.PlacePosition.x - this.box.min.x * 1e4 + this.p.x * 1e4,
|
||||
y: this.PlacePosition.y - this.box.min.y * 1e4 + this.p.y * 1e4,
|
||||
}
|
||||
|
||||
return [this.part1, this.part2]
|
||||
}
|
||||
}
|
61
tests/dev1/dataHandle/common/core/PartState.ts
Normal file
61
tests/dev1/dataHandle/common/core/PartState.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Point } from '../Vector2'
|
||||
import type { NestFiler } from '../Filer'
|
||||
import { Path } from './Path'
|
||||
import { PathGeneratorSingle } from './PathGenerator'
|
||||
|
||||
/**
|
||||
* 用于存放零件旋转后的状态
|
||||
* 记录了用于放置时的轮廓。该轮廓总是首点等于0,便于放置时的计算。
|
||||
*/
|
||||
export class PartState
|
||||
{
|
||||
Rotation: number
|
||||
OrigionMinPoint: Point// 使用 Rotation(O - OrigionMinPoint) 将零件变换到和排料矩形状态相同的状态,然后使用PlacePoint(O)将零件设置到正确的状态
|
||||
|
||||
MinPoint: Point// 这个状态下的最小点
|
||||
Contour: Path// 轮廓
|
||||
|
||||
IsMirror: boolean = false
|
||||
MirrorOriginMinPoint: Point
|
||||
|
||||
Mirror(): PartState
|
||||
{
|
||||
if (this.Contour.IsRect)
|
||||
return
|
||||
|
||||
let mpts = this.Contour.Points.map((p) => { return { x: -p.x, y: p.y } }).reverse()
|
||||
let path = new Path(mpts, 0)
|
||||
let partState = new PartState()
|
||||
partState.Contour = PathGeneratorSingle.Allocate(path)
|
||||
PathGeneratorSingle.RegisterId(partState.Contour)
|
||||
partState.Rotation = this.Rotation
|
||||
partState.OrigionMinPoint = this.OrigionMinPoint
|
||||
partState.MinPoint = this.MinPoint
|
||||
partState.IsMirror = true
|
||||
partState.MirrorOriginMinPoint = path.OrigionMinPoint
|
||||
return partState
|
||||
}
|
||||
|
||||
// #region -------------------------File-------------------------
|
||||
ReadFile(file: NestFiler)
|
||||
{
|
||||
this.Rotation = file.Read()
|
||||
this.OrigionMinPoint = file.Read()
|
||||
this.MinPoint = file.Read()
|
||||
|
||||
let index = file.Read() as number
|
||||
this.Contour = PathGeneratorSingle.paths[index]
|
||||
|
||||
if (!this.Contour)
|
||||
console.error('无法得到PartState的轮廓!')
|
||||
}
|
||||
|
||||
WriteFile(file: NestFiler)
|
||||
{
|
||||
file.Write(this.Rotation)
|
||||
file.Write(this.OrigionMinPoint)
|
||||
file.Write(this.MinPoint)
|
||||
file.Write(this.Contour.Id)
|
||||
}
|
||||
// #endregion
|
||||
}
|
381
tests/dev1/dataHandle/common/core/Path.ts
Normal file
381
tests/dev1/dataHandle/common/core/Path.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import { Box2 } from '../Box2.js'
|
||||
import { clipperCpp } from '../ClipperCpp.js'
|
||||
import type { NestFiler } from '../Filer.js'
|
||||
import type { Point } from '../Vector2.js'
|
||||
import { equaln } from '../Util.js'
|
||||
import { Vector2 } from '../Vector2.js'
|
||||
|
||||
/**
|
||||
* 轮廓路径类
|
||||
* 可以求NFP,和保存NFPCahce
|
||||
* 因为NFP结果是按照最低点移动的,所以将点旋转后,按照盒子将点移动到0点.
|
||||
*/
|
||||
export class Path
|
||||
{
|
||||
Id: number
|
||||
Points: Point[]
|
||||
OutsideNFPCache: { [key: number]: Point[][] } = {}
|
||||
InsideNFPCache: { [key: number]: Point[][] } = {}
|
||||
|
||||
constructor(public OrigionPoints?: Point[], rotation: number = 0)
|
||||
{
|
||||
if (OrigionPoints)
|
||||
this.Init(OrigionPoints, rotation)
|
||||
}
|
||||
|
||||
Origion: Path
|
||||
// 点表在旋转后的原始最小点.使用这个点将轮廓移动到0点
|
||||
OrigionMinPoint: Vector2
|
||||
Rotation: number
|
||||
|
||||
Size: Vector2// 序列化
|
||||
private Init(origionPoints: Point[], rotation: number)
|
||||
{
|
||||
this.Rotation = rotation
|
||||
if (rotation === 0)
|
||||
this.Points = origionPoints.map((p) => { return { ...p } })
|
||||
else
|
||||
{
|
||||
let c = Math.cos(rotation)
|
||||
let s = Math.sin(rotation)
|
||||
|
||||
let npts: Point[] = []
|
||||
for (let p of origionPoints)
|
||||
{
|
||||
let x = p.x
|
||||
let y = p.y
|
||||
const x1 = x * c - y * s
|
||||
const y1 = x * s + y * c
|
||||
npts.push({ x: x1, y: y1 })
|
||||
}
|
||||
this.Points = npts
|
||||
}
|
||||
|
||||
let box = new Box2()
|
||||
let v2 = new Vector2()
|
||||
for (let p of this.Points)
|
||||
{
|
||||
v2.x = p.x
|
||||
v2.y = p.y
|
||||
box.expandByPoint(v2)
|
||||
}
|
||||
|
||||
this.OrigionMinPoint = box.min
|
||||
this.Size = box.max.sub(box.min)
|
||||
|
||||
for (let p of this.Points)
|
||||
{
|
||||
p.x -= box.min.x
|
||||
p.y -= box.min.y
|
||||
}
|
||||
}
|
||||
|
||||
GetNFPs(path: Path, outside: boolean): Point[][]
|
||||
{
|
||||
// 寻找内轮廓时,面积应该比本path小,这个判断移交给使用者自己判断
|
||||
// if (!outside && this.Area < path.Area) return [];
|
||||
let nfps = clipperCpp.lib.minkowskiSumPath(this.BigIntPoints, path.MirrorPoints, true)
|
||||
|
||||
// 必须删除自交,否则将会出错
|
||||
nfps = clipperCpp.lib.simplifyPolygons(nfps)
|
||||
nfps = nfps.filter((nfp) =>
|
||||
{
|
||||
let area = Area(nfp)
|
||||
// if (area > 1) return outside;//第一个不一定是外轮廓,但是面积为正时肯定为外轮廓 (因为使用了简化多段线,所以这个代码已经不能有了)
|
||||
if (Math.abs(area) < 10)
|
||||
return false// 应该不用在移除这个了
|
||||
|
||||
let { x, y } = nfp[0]
|
||||
if (outside)
|
||||
{
|
||||
if (this.Area > path.Area)
|
||||
{
|
||||
let p = { x: path.InPoint.x + x, y: path.InPoint.y + y }
|
||||
if (p.x < 0 || p.y < 0 || p.x > this.BigSize.x || p.y > this.BigSize.y)
|
||||
return true
|
||||
let dir = clipperCpp.lib.pointInPolygon(p, this.BigIntPoints)
|
||||
return dir === 0
|
||||
}
|
||||
else
|
||||
{
|
||||
let p = { x: this.InPoint.x - x, y: this.InPoint.y - y }
|
||||
if (p.x < 0 || p.y < 0 || p.x > path.BigSize.x || p.y > path.BigSize.y)
|
||||
return true
|
||||
let dir = clipperCpp.lib.pointInPolygon(p, path.BigIntPoints)
|
||||
return dir === 0
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let p = { x: path.InPoint.x + x, y: path.InPoint.y + y }
|
||||
if (p.x < 0 || p.y < 0 || p.x > this.BigSize.x || p.y > this.BigSize.y)
|
||||
return false
|
||||
let dir = clipperCpp.lib.pointInPolygon(p, this.BigIntPoints)
|
||||
return dir === 1
|
||||
}
|
||||
})
|
||||
return nfps
|
||||
}
|
||||
|
||||
GetOutsideNFP(path: Path): Point[][]
|
||||
{
|
||||
let nfps = this.OutsideNFPCache[path.Id]
|
||||
if (nfps)
|
||||
return nfps
|
||||
|
||||
if (this.IsRect && path.IsRect)
|
||||
{
|
||||
let [ax, ay] = [this.Size.x * 1e4, this.Size.y * 1e4]
|
||||
let [bx, by] = [path.Size.x * 1e4, path.Size.y * 1e4]
|
||||
nfps = [[
|
||||
{ x: -bx, y: -by },
|
||||
{ x: ax, y: -by },
|
||||
{ x: ax, y: ay },
|
||||
{ x: -bx, y: ay },
|
||||
]]
|
||||
}
|
||||
else
|
||||
nfps = this.GetNFPs(path, true)
|
||||
this.OutsideNFPCache[path.Id] = nfps
|
||||
// 虽然有这种神奇的特性,但是好像并不会提高性能。
|
||||
// path.OutsideNFPCache[this.id] = (this, nfps.map(nfp =>
|
||||
// {
|
||||
// return nfp.map(p =>
|
||||
// {
|
||||
// return { x: -p.x, y: -p.y };
|
||||
// });
|
||||
// }));
|
||||
return nfps
|
||||
}
|
||||
|
||||
GetInsideNFP(path: Path): Point[][]
|
||||
{
|
||||
if (path.Area > this.Area)
|
||||
return
|
||||
let nfp = this.InsideNFPCache[path.Id]
|
||||
if (nfp)
|
||||
return nfp
|
||||
|
||||
let nfps: Point[][]
|
||||
if (this.IsRect)
|
||||
{
|
||||
let [ax, ay] = [this.Size.x * 1e4, this.Size.y * 1e4]
|
||||
let [bx, by] = [path.Size.x * 1e4, path.Size.y * 1e4]
|
||||
|
||||
let l = ax - bx
|
||||
let h = ay - by
|
||||
|
||||
const MinNumber = 200// 清理的数值是100,所以200是可以接受的, 200=0.020问题不大(过盈配合)
|
||||
if (l < -MinNumber || h < -MinNumber)
|
||||
return
|
||||
|
||||
if (l < MinNumber)
|
||||
l = MinNumber
|
||||
else
|
||||
l += MinNumber
|
||||
|
||||
if (h < MinNumber)
|
||||
h = MinNumber
|
||||
else
|
||||
h += MinNumber
|
||||
|
||||
nfps = [[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: l, y: 0 },
|
||||
{ x: l, y: h },
|
||||
{ x: 0, y: h },
|
||||
]]
|
||||
}
|
||||
else
|
||||
nfps = this.GetNFPs(path, false)
|
||||
|
||||
if (path.Id !== undefined)
|
||||
this.InsideNFPCache[path.Id] = nfps
|
||||
return nfps
|
||||
}
|
||||
|
||||
private _InPoint: Point
|
||||
|
||||
/**
|
||||
* 用这个点来检测是否在Path内部
|
||||
*/
|
||||
private get InPoint()
|
||||
{
|
||||
if (this._InPoint)
|
||||
return this._InPoint
|
||||
let mp = { x: (this.Points[0].x + this.Points[1].x) / 2, y: (this.Points[0].y + this.Points[1].y) / 2 }
|
||||
let normal = new Vector2(this.Points[1].x - this.Points[0].x, this.Points[1].y - this.Points[0].y).normalize()
|
||||
// [normal.x, normal.y] = [normal.y, -normal.x];
|
||||
mp.x -= normal.y
|
||||
mp.y += normal.x
|
||||
|
||||
mp.x *= 1e4
|
||||
mp.y *= 1e4
|
||||
this._InPoint = mp
|
||||
return mp
|
||||
}
|
||||
|
||||
protected _BigIntPoints: Point[]
|
||||
get BigIntPoints()
|
||||
{
|
||||
if (this._BigIntPoints)
|
||||
return this._BigIntPoints
|
||||
this._BigIntPoints = this.Points.map((p) =>
|
||||
{
|
||||
return {
|
||||
x: Math.round(p.x * 1e4),
|
||||
y: Math.round(p.y * 1e4),
|
||||
}
|
||||
})
|
||||
return this._BigIntPoints
|
||||
}
|
||||
|
||||
private _BigSize: Vector2
|
||||
get BigSize()
|
||||
{
|
||||
if (this._BigSize)
|
||||
return this._BigSize
|
||||
this._BigSize = new Vector2(this.Size.x * 1e4, this.Size.y * 1e4)
|
||||
return this._BigSize
|
||||
}
|
||||
|
||||
protected _MirrorPoints: Point[]
|
||||
get MirrorPoints()
|
||||
{
|
||||
if (!this._MirrorPoints)
|
||||
this._MirrorPoints = this.BigIntPoints.map((p) =>
|
||||
{
|
||||
return { x: -p.x, y: -p.y }
|
||||
})
|
||||
|
||||
return this._MirrorPoints
|
||||
}
|
||||
|
||||
protected _BoundingBox: Box2
|
||||
get BoundingBox()
|
||||
{
|
||||
if (!this._BoundingBox)
|
||||
{
|
||||
this._BoundingBox = new Box2(new Vector2(), this.Size)
|
||||
}
|
||||
return this._BoundingBox
|
||||
}
|
||||
|
||||
protected _Area: number
|
||||
get Area()
|
||||
{
|
||||
if (this._Area === undefined)
|
||||
this._Area = Area(this.Points)
|
||||
return this._Area
|
||||
}
|
||||
|
||||
set Area(a: number)
|
||||
{
|
||||
this._Area = a
|
||||
}
|
||||
|
||||
private _IsRect: boolean
|
||||
get IsRect()
|
||||
{
|
||||
if (this._IsRect === undefined)
|
||||
{
|
||||
let s = this.BoundingBox.getSize(new Vector2())
|
||||
this._IsRect = equaln(this.Area, s.x * s.y, 1)
|
||||
}
|
||||
return this._IsRect
|
||||
}
|
||||
|
||||
ReadFile(file: NestFiler): void
|
||||
{
|
||||
let ver = file.Read()
|
||||
this.Id = file.Read()
|
||||
let arr = file.Read()
|
||||
this.Points = []
|
||||
for (let i = 0; i < arr.length; i += 2)
|
||||
{
|
||||
let p = { x: arr[i], y: arr[i + 1] }
|
||||
this.Points.push(p)
|
||||
}
|
||||
|
||||
this.Size = new Vector2(file.Read(), file.Read())
|
||||
this._Area = file.Read()
|
||||
let id = file.Read()
|
||||
if (id !== -1)
|
||||
{
|
||||
this.Origion = id
|
||||
this.Rotation = file.Read()
|
||||
this.OrigionMinPoint = new Vector2(file.Read(), file.Read())
|
||||
}
|
||||
}
|
||||
|
||||
WriteFile(file: NestFiler): void
|
||||
{
|
||||
file.Write(1)// ver
|
||||
file.Write(this.Id)
|
||||
let arr: number[] = []
|
||||
for (let p of this.Points)
|
||||
arr.push(p.x, p.y)
|
||||
file.Write(arr)
|
||||
|
||||
file.Write(this.Size.x)
|
||||
file.Write(this.Size.y)
|
||||
file.Write(this._Area)
|
||||
if (this.Origion && this.Origion.Id)
|
||||
{
|
||||
// 如果有原始的id,则传递它,以便后续进行NFP复用.
|
||||
file.Write(this.Origion.Id)
|
||||
file.Write(this.Rotation)
|
||||
file.Write(this.OrigionMinPoint.x)
|
||||
file.Write(this.OrigionMinPoint.y)
|
||||
}
|
||||
else
|
||||
file.Write(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// 点表面积
|
||||
export function Area(pts: Point[]): number
|
||||
{
|
||||
let cnt = pts.length
|
||||
if (cnt < 3)
|
||||
return 0
|
||||
let a = 0
|
||||
for (let i = 0, j = cnt - 1; i < cnt; ++i)
|
||||
{
|
||||
a += (pts[j].x + pts[i].x) * (pts[j].y - pts[i].y)
|
||||
j = i
|
||||
}
|
||||
return -a * 0.5
|
||||
}
|
||||
|
||||
/**
|
||||
* 平移点表,返回新点表
|
||||
*/
|
||||
export function TranslatePath(pts: Point[], p: Point): Point[]
|
||||
{
|
||||
return pts.map((px) =>
|
||||
{
|
||||
return { x: p.x + px.x, y: p.y + px.y }
|
||||
})
|
||||
}
|
||||
|
||||
export function TranslatePath_Self(pts: Point[], mx: number, my: number): Point[]
|
||||
{
|
||||
for (let pt of pts)
|
||||
{
|
||||
pt.x += mx
|
||||
pt.y += my
|
||||
}
|
||||
return pts
|
||||
}
|
||||
|
||||
// 缩放点表,返回原始点表
|
||||
export function PathScale(pts: Point[], scale: number): Point[]
|
||||
{
|
||||
for (let p of pts)
|
||||
{
|
||||
p.x *= scale
|
||||
p.y *= scale
|
||||
}
|
||||
return pts
|
||||
}
|
87
tests/dev1/dataHandle/common/core/PathGenerator.ts
Normal file
87
tests/dev1/dataHandle/common/core/PathGenerator.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { FixIndex, equaln } from '../Util'
|
||||
import type { Path } from './Path'
|
||||
|
||||
/**
|
||||
* 轮廓路径构造器
|
||||
* 传递一组简化后的点表过来,如果已经有同样的点表时,返回已经生产的Path,避免重复产生Path。
|
||||
* 使用相同的PATH有复用路径缓存。
|
||||
*
|
||||
* 每次进行优化时,必须清理构造器,保证Path生成是对本次优化唯一。
|
||||
*/
|
||||
class PathGenerator
|
||||
{
|
||||
paths: Path[] = []
|
||||
pathAreaMap: { [key: string]: Path[] } = {}
|
||||
|
||||
// 缓存命中次数
|
||||
cacheCount = 0
|
||||
|
||||
/**
|
||||
* 如果存在同样的轮廓,则返回已经构造的轮廓,
|
||||
* 如果没有,则返回自身,并且注册它。
|
||||
* 如果id没有被注册,那么证明它无法放置在bin中
|
||||
*/
|
||||
Allocate(path: Path): Path
|
||||
{
|
||||
let area = path.Area.toFixed(0)
|
||||
let paths = this.pathAreaMap[area]
|
||||
if (paths)
|
||||
{
|
||||
for (let ps of paths)
|
||||
{
|
||||
if (EqualPath(ps, path))
|
||||
{
|
||||
this.cacheCount++
|
||||
return ps
|
||||
}
|
||||
}
|
||||
paths.push(path)
|
||||
}
|
||||
else
|
||||
this.pathAreaMap[area] = [path]
|
||||
return path
|
||||
}
|
||||
|
||||
RegisterId(path: Path)
|
||||
{
|
||||
if (path.Id === undefined)
|
||||
{
|
||||
path.Id = this.paths.length
|
||||
this.paths.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
Clear()
|
||||
{
|
||||
this.paths = []
|
||||
this.pathAreaMap = {}
|
||||
this.cacheCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 两路径相等,点表个数相等且每个点都相似
|
||||
*/
|
||||
function EqualPath(path1: Path, path2: Path): boolean
|
||||
{
|
||||
if (path1.Points.length !== path2.Points.length)
|
||||
return false
|
||||
|
||||
let p0 = path1.Points[0]
|
||||
let p2Index = path2.Points.findIndex((p) =>
|
||||
{
|
||||
return equaln(p.x, p0.x, 1e-3) && equaln(p.y, p0.y, 1e-3)
|
||||
})
|
||||
|
||||
for (let i = 0; i < path1.Points.length; i++)
|
||||
{
|
||||
let p1 = path1.Points[i]
|
||||
let p2 = path2.Points[FixIndex(p2Index + i, path2.Points)]
|
||||
|
||||
if (!equaln(p1.x, p2.x, 1e-4) || !equaln(p1.y, p2.y, 1e-4))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const PathGeneratorSingle = new PathGenerator()
|
11
tests/dev1/dataHandle/common/core/PlaceType.ts
Normal file
11
tests/dev1/dataHandle/common/core/PlaceType.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
/**排牌类型:Hull=0凸包模式 (凸包面积) Box=1盒子模式 (长x宽) Gravity=2重力模式(重力) */
|
||||
export enum PlaceType
|
||||
{
|
||||
/**凸包模式 (凸包面积) */
|
||||
Hull = 0,
|
||||
/**盒子模式 (长乘以宽) */
|
||||
Box = 1,
|
||||
/**重力模式(重力) */
|
||||
Gravity = 2
|
||||
}
|
Reference in New Issue
Block a user