395 lines
11 KiB
TypeScript
395 lines
11 KiB
TypeScript
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]
|
||
}
|
||
}
|