feat:提交

This commit is contained in:
2025-07-22 18:22:31 +08:00
parent 160bb294ca
commit 2ebb3e1abe
85 changed files with 36380 additions and 0 deletions

View 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
}

View File

@@ -0,0 +1,5 @@
export const GNestConfig = {
RotateHole: true, // 在旋转零件的时候旋转网洞
UsePartGroup: false, // 如果开启这个特性,将在第一次放置零件时,尝试计算完全对插的板件,并且使用它.(基因注册,模范夫妻)
UseOffsetSimplify: true,
}

View 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
}

View 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 = {}
}
}

View 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
}
}

View 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() {
}
}

View 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
}
}
}

View 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]
}
}

View 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
}

View 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
}

View 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()

View File

@@ -0,0 +1,11 @@
/**排牌类型Hull=0凸包模式 (凸包面积) Box=1盒子模式 (长x宽) Gravity=2重力模式(重力) */
export enum PlaceType
{
/**凸包模式 (凸包面积) */
Hull = 0,
/**盒子模式 (长乘以宽) */
Box = 1,
/**重力模式(重力) */
Gravity = 2
}