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[] = []// 已经无法放置的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 }