ecode-cutting 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecode_cutting/USAGE.md +673 -0
- ecode_cutting/__init__.py +18 -0
- ecode_cutting/_common.py +158 -0
- ecode_cutting/_linear_fast.py +231 -0
- ecode_cutting/compat.py +659 -0
- ecode_cutting/engine.py +558 -0
- ecode_cutting/exporters.py +362 -0
- ecode_cutting/linear.py +532 -0
- ecode_cutting/models.py +230 -0
- ecode_cutting/portfolio.py +234 -0
- ecode_cutting/py.typed +0 -0
- ecode_cutting/rectangular.py +357 -0
- ecode_cutting/serialization.py +234 -0
- ecode_cutting/validation.py +234 -0
- ecode_cutting-0.2.3.dist-info/METADATA +190 -0
- ecode_cutting-0.2.3.dist-info/RECORD +18 -0
- ecode_cutting-0.2.3.dist-info/WHEEL +5 -0
- ecode_cutting-0.2.3.dist-info/top_level.txt +1 -0
ecode_cutting/_common.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""输入校验、精度处理和合作式取消;不保存跨任务可变状态。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import fields, replace
|
|
6
|
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_EVEN
|
|
7
|
+
from math import isfinite
|
|
8
|
+
from sys import float_info
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Callable, Sequence
|
|
11
|
+
|
|
12
|
+
from .models import Options, Part, Progress, Stock, ValidationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def number(value: object, name: str, *, positive: bool = False) -> float:
|
|
16
|
+
if isinstance(value, (str, bytes, bool)):
|
|
17
|
+
raise ValidationError(f"{name} 必须是有限数值")
|
|
18
|
+
try:
|
|
19
|
+
result = float(value)
|
|
20
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
21
|
+
raise ValidationError(f"{name} 必须是有限数值") from exc
|
|
22
|
+
if not isfinite(result) or result < 0 or (positive and result == 0):
|
|
23
|
+
raise ValidationError(f"{name} 必须是{'正' if positive else '非负'}有限数值")
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def integer(value: object, name: str, minimum: int = 0) -> int:
|
|
28
|
+
if type(value) is not int or value < minimum:
|
|
29
|
+
raise ValidationError(f"{name} 必须是大于等于 {minimum} 的整数")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def quantize(value: float, precision: int) -> int:
|
|
34
|
+
"""与 .NET 默认舍入一致,避免二进制浮点数在锯缝边界积累误差。"""
|
|
35
|
+
try:
|
|
36
|
+
return int((Decimal(str(value)) * (10 ** precision)).to_integral_value(rounding=ROUND_HALF_EVEN))
|
|
37
|
+
except (InvalidOperation, ValueError, OverflowError) as exc:
|
|
38
|
+
raise ValidationError("尺寸超出可表示范围") from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def matches(stock_material: int, part_material: int) -> bool:
|
|
42
|
+
# -1 原料是通配材料;指定材料的原料不能接收未指定材料的零件。
|
|
43
|
+
return stock_material == -1 or stock_material == part_material
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_options(options: Options) -> Options:
|
|
47
|
+
"""计算边界统一为有限 float,避免 Decimal 与内部浮点计时混算。"""
|
|
48
|
+
if not isinstance(options, Options):
|
|
49
|
+
raise ValidationError("options 必须是 Options")
|
|
50
|
+
values = {}
|
|
51
|
+
for f in fields(options):
|
|
52
|
+
if isinstance(f.default, float):
|
|
53
|
+
value = getattr(options, f.name)
|
|
54
|
+
values[f.name] = None if f.name == "time_limit" and value is None else number(value, f.name)
|
|
55
|
+
return replace(options, **values)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate(stocks: Sequence[Stock], parts: Sequence[Part], options: Options, *, linear: bool) -> None:
|
|
59
|
+
if not isinstance(options, Options):
|
|
60
|
+
raise ValidationError("options 必须是 Options")
|
|
61
|
+
for name in ("saw_width", "trim_left", "trim_right", "trim_top", "trim_bottom", "waste_size_min", "cross_section"):
|
|
62
|
+
number(getattr(options, name), name)
|
|
63
|
+
for name in ("max_cut_level", "max_layout_size", "linear_max_size_per_stock"):
|
|
64
|
+
integer(getattr(options, name), name)
|
|
65
|
+
integer(options.max_size_per_stock, "max_size_per_stock", -1)
|
|
66
|
+
integer(options.max_decimal_point, "max_decimal_point")
|
|
67
|
+
if options.max_decimal_point > 6:
|
|
68
|
+
raise ValidationError("max_decimal_point 必须在 0 到 6 之间")
|
|
69
|
+
integer(options.max_attempts, "max_attempts", 1)
|
|
70
|
+
integer(options.seed, "seed")
|
|
71
|
+
threshold = number(options.linear_max_size_per_stock_threshold, "linear_max_size_per_stock_threshold")
|
|
72
|
+
if threshold > 1:
|
|
73
|
+
raise ValidationError("linear_max_size_per_stock_threshold 必须在 0 到 1 之间")
|
|
74
|
+
if options.cut_direction not in ("auto", "horizontal", "vertical"):
|
|
75
|
+
raise ValidationError("cut_direction 必须是 auto、horizontal 或 vertical")
|
|
76
|
+
if options.roll_mode and (linear or options.cut_direction == "auto"):
|
|
77
|
+
raise ValidationError("卷材模式必须显式选择二维横向或纵向切割")
|
|
78
|
+
if options.time_limit is not None:
|
|
79
|
+
number(options.time_limit, "time_limit")
|
|
80
|
+
for f in fields(options):
|
|
81
|
+
if isinstance(f.default, bool) and type(getattr(options, f.name)) is not bool:
|
|
82
|
+
raise ValidationError(f"{f.name} 必须是布尔值")
|
|
83
|
+
requested = sum(integer(p.count, "parts.count") for p in parts if isinstance(p, Part))
|
|
84
|
+
for kind, items in (("stocks", stocks), ("parts", parts)):
|
|
85
|
+
total_area = total_quantized_area = 0
|
|
86
|
+
for i, item in enumerate(items):
|
|
87
|
+
if not isinstance(item, Stock if kind == "stocks" else Part):
|
|
88
|
+
raise ValidationError(f"{kind}[{i}] 类型错误")
|
|
89
|
+
dimensions, quantized_dimensions = [], []
|
|
90
|
+
for dim in ("width", "height"):
|
|
91
|
+
value = number(getattr(item, dim), f"{kind}[{i}].{dim}", positive=True)
|
|
92
|
+
rounded = quantize(value, options.max_decimal_point)
|
|
93
|
+
if rounded <= 0:
|
|
94
|
+
raise ValidationError(f"{kind}[{i}].{dim} 在当前精度下为零")
|
|
95
|
+
dimensions.append(value)
|
|
96
|
+
quantized_dimensions.append(rounded)
|
|
97
|
+
integer(item.count, f"{kind}[{i}].count")
|
|
98
|
+
integer(item.material, f"{kind}[{i}].material", -1)
|
|
99
|
+
# 按实际浮点表示能力限制,而不规定业务尺寸上限;包含精度缩放后的中间量。
|
|
100
|
+
quantity = item.count or (max(1, requested) if kind == "stocks" else 0)
|
|
101
|
+
area = dimensions[0] * dimensions[1]
|
|
102
|
+
quantized_area = quantized_dimensions[0] * quantized_dimensions[1]
|
|
103
|
+
if linear and options.cross_section:
|
|
104
|
+
area = max(area, dimensions[0] * number(options.cross_section, "cross_section"))
|
|
105
|
+
quantized_area = max(quantized_area, quantized_dimensions[0] * quantize(options.cross_section, options.max_decimal_point))
|
|
106
|
+
try:
|
|
107
|
+
total_area += area * quantity
|
|
108
|
+
total_quantized_area += quantized_area * quantity
|
|
109
|
+
except OverflowError as exc:
|
|
110
|
+
raise ValidationError(f"{kind} 总材料尺寸超出浮点表示范围") from exc
|
|
111
|
+
if (not isfinite(area) or not isfinite(total_area)
|
|
112
|
+
or max(quantized_dimensions) > float_info.max
|
|
113
|
+
or total_quantized_area > float_info.max):
|
|
114
|
+
raise ValidationError(f"{kind} 面积或精度缩放后的总材料尺寸超出浮点表示范围")
|
|
115
|
+
if not isinstance(item.id, str):
|
|
116
|
+
raise ValidationError(f"{kind}[{i}].id 必须是字符串")
|
|
117
|
+
if isinstance(item, Part):
|
|
118
|
+
if type(item.rotatable) is not bool:
|
|
119
|
+
raise ValidationError("rotatable 必须是布尔值")
|
|
120
|
+
for angle in (item.angle_start, item.angle_end):
|
|
121
|
+
number(angle, "angle")
|
|
122
|
+
if angle != 0 and not 10 <= angle <= 170:
|
|
123
|
+
raise ValidationError("切角必须是 0 或 10 到 170 度")
|
|
124
|
+
if not linear and angle != 0:
|
|
125
|
+
raise ValidationError("斜角仅适用于一维型材")
|
|
126
|
+
elif type(item.waste) is not bool:
|
|
127
|
+
raise ValidationError("waste 必须是布尔值")
|
|
128
|
+
if linear and any(p.angle_start or p.angle_end for p in parts) and options.cross_section <= 0:
|
|
129
|
+
raise ValidationError("斜角切割必须设置正的 cross_section")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Runtime:
|
|
133
|
+
"""回调返回 True 时取消;超时或取消后仍返回已经得到的可行解。"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, options: Options, requested: int, callback: Callable[[Progress], bool] | None = None):
|
|
136
|
+
self.started = monotonic()
|
|
137
|
+
self.deadline = None if options.time_limit is None else self.started + number(options.time_limit, "time_limit")
|
|
138
|
+
self.callback = callback
|
|
139
|
+
self.requested = requested
|
|
140
|
+
self.cancelled = False
|
|
141
|
+
self.timed_out = False
|
|
142
|
+
self.last_callback = -1.0
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def elapsed(self) -> float:
|
|
146
|
+
return monotonic() - self.started
|
|
147
|
+
|
|
148
|
+
def stopped(self, placed: int = 0, attempt: int = 0) -> bool:
|
|
149
|
+
if self.cancelled or self.timed_out:
|
|
150
|
+
return True
|
|
151
|
+
now = monotonic()
|
|
152
|
+
if self.deadline is not None and now >= self.deadline:
|
|
153
|
+
self.timed_out = True
|
|
154
|
+
return True
|
|
155
|
+
if self.callback is not None and now - self.last_callback >= 0.02:
|
|
156
|
+
self.last_callback = now
|
|
157
|
+
self.cancelled = bool(self.callback(Progress(now - self.started, placed, self.requested, attempt)))
|
|
158
|
+
return self.cancelled
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""直端批量订单的容量索引;复杂约束交还完整的一维求解器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from bisect import bisect_left, insort
|
|
6
|
+
from collections import Counter, defaultdict
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from math import hypot
|
|
9
|
+
from random import Random
|
|
10
|
+
from typing import Sequence
|
|
11
|
+
|
|
12
|
+
from ._common import Runtime, matches, quantize
|
|
13
|
+
from .linear import _Bar, solve_linear
|
|
14
|
+
from .models import (Cut, Layout, Options, Part, Placement, Remnant, Result,
|
|
15
|
+
Stock, StockComponent, Unplaced, ValidationError)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def solve_linear_fast(stocks: Sequence[Stock], parts: Sequence[Part], options: Options,
|
|
19
|
+
runtime: Runtime) -> Result:
|
|
20
|
+
"""保持原启发式的候选顺序,仅替换逐棒扫描;输入校验由引擎负责。"""
|
|
21
|
+
requested = sum(p.count for p in parts)
|
|
22
|
+
if (requested <= 64 or options.waste_size_min or options.linear_max_size_per_stock
|
|
23
|
+
or options.max_size_per_stock
|
|
24
|
+
or any(p.angle_start not in (0, 90) or p.angle_end not in (0, 90) for p in parts)):
|
|
25
|
+
return solve_linear(stocks, parts, options, runtime)
|
|
26
|
+
|
|
27
|
+
scale = 10 ** options.max_decimal_point
|
|
28
|
+
q = lambda n: quantize(n, options.max_decimal_point)
|
|
29
|
+
widths, stock_widths = [q(p.width) for p in parts], [q(s.width) for s in stocks]
|
|
30
|
+
left, right, kerf = map(q, (options.trim_left, options.trim_right, options.saw_width))
|
|
31
|
+
keys = [(w, 0, 0) for w in widths]
|
|
32
|
+
instances = [(i, j) for i, p in enumerate(parts) for j in range(p.count)]
|
|
33
|
+
stock_order = sorted(range(len(stocks)), key=lambda j: (
|
|
34
|
+
not stocks[j].waste, -stock_widths[j] if options.use_large_stock_first else stock_widths[j], j))
|
|
35
|
+
best, best_score, attempts = [], None, 0
|
|
36
|
+
|
|
37
|
+
def score(bars):
|
|
38
|
+
cut_count = sum(len(b.items) + 1
|
|
39
|
+
- (b.items[0][2] in (0, left))
|
|
40
|
+
- (b.end in (b.width, b.width - right))
|
|
41
|
+
+ bool(left) + bool(right) for b in bars)
|
|
42
|
+
patterns = (len({(b.width, tuple(sorted(b.types.items()))) for b in bars})
|
|
43
|
+
if options.use_layout_minimization else 0)
|
|
44
|
+
return (-sum(len(b.items) for b in bars), len(bars), sum(not b.waste for b in bars),
|
|
45
|
+
patterns, sum(b.width for b in bars), cut_count)
|
|
46
|
+
|
|
47
|
+
def remember(bars):
|
|
48
|
+
nonlocal best, best_score
|
|
49
|
+
candidate = score(bars)
|
|
50
|
+
if best_score is None or candidate < best_score:
|
|
51
|
+
best, best_score = [b.copy() for b in bars], candidate
|
|
52
|
+
|
|
53
|
+
def append(bar, i, instance):
|
|
54
|
+
x = bar.end + kerf if bar.items else left
|
|
55
|
+
bar.items.append((i, instance, x, 0, 0, False, False))
|
|
56
|
+
bar.end = x + widths[i]
|
|
57
|
+
bar.types[keys[i]] += 1
|
|
58
|
+
|
|
59
|
+
if not runtime.stopped() and options.complete_mode:
|
|
60
|
+
for i, part in enumerate(parts):
|
|
61
|
+
if part.count and not any(matches(s.material, part.material)
|
|
62
|
+
and stock_widths[j] - left - right >= widths[i]
|
|
63
|
+
for j, s in enumerate(stocks)):
|
|
64
|
+
if runtime.stopped():
|
|
65
|
+
break
|
|
66
|
+
raise ValidationError(f"parts[{i}] 无法放入任何可用原料(含修边、余料和组合约束)")
|
|
67
|
+
|
|
68
|
+
rng = Random(options.seed)
|
|
69
|
+
for attempt in range(1 if options.linear_sort_ascending else options.max_attempts):
|
|
70
|
+
if runtime.stopped(-best_score[0] if best_score else 0, attempt):
|
|
71
|
+
break
|
|
72
|
+
attempts += 1
|
|
73
|
+
order = instances.copy()
|
|
74
|
+
if options.linear_sort_ascending or attempt == 1:
|
|
75
|
+
order.sort(key=lambda item: widths[item[0]])
|
|
76
|
+
elif attempt == 0:
|
|
77
|
+
order.sort(key=lambda item: widths[item[0]], reverse=True)
|
|
78
|
+
else:
|
|
79
|
+
rng.shuffle(order)
|
|
80
|
+
bars, used = [], Counter()
|
|
81
|
+
buckets = defaultdict(list)
|
|
82
|
+
for position, (i, instance) in enumerate(order):
|
|
83
|
+
if position % 32 == 0 and runtime.stopped(position, attempt + 1):
|
|
84
|
+
break
|
|
85
|
+
materials = (-1,) if parts[i].material == -1 else (-1, parts[i].material)
|
|
86
|
+
candidate = None
|
|
87
|
+
for waste in (True, False):
|
|
88
|
+
for material in materials:
|
|
89
|
+
bucket = buckets[material, waste]
|
|
90
|
+
index = bisect_left(bucket, (widths[i], -1))
|
|
91
|
+
if index < len(bucket):
|
|
92
|
+
capacity, j = bucket[index]
|
|
93
|
+
choice = (capacity, j, material, waste, index)
|
|
94
|
+
if candidate is None or choice[:2] < candidate[:2]:
|
|
95
|
+
candidate = choice
|
|
96
|
+
if candidate is not None:
|
|
97
|
+
break
|
|
98
|
+
if candidate is not None:
|
|
99
|
+
_, j, material, waste, index = candidate
|
|
100
|
+
buckets[material, waste].pop(index)
|
|
101
|
+
bar = bars[j]
|
|
102
|
+
else:
|
|
103
|
+
stock_i = next((j for j in stock_order
|
|
104
|
+
if (not stocks[j].count or used[j] < stocks[j].count)
|
|
105
|
+
and matches(stocks[j].material, parts[i].material)
|
|
106
|
+
and stock_widths[j] - left - right >= widths[i]), None)
|
|
107
|
+
if stock_i is None:
|
|
108
|
+
continue
|
|
109
|
+
stock = stocks[stock_i]
|
|
110
|
+
bar = _Bar(((stock_i, used[stock_i]),), stock_widths[stock_i],
|
|
111
|
+
stock.material, stock.waste, end=left)
|
|
112
|
+
used[stock_i] += 1
|
|
113
|
+
j = len(bars)
|
|
114
|
+
bars.append(bar)
|
|
115
|
+
append(bar, i, instance)
|
|
116
|
+
capacity = bar.width - right - bar.end - kerf
|
|
117
|
+
if capacity >= 0:
|
|
118
|
+
# ponytail: bisect 定位后列表移动仍为 O(B),避免逐棒执行 Python fit 循环。
|
|
119
|
+
insort(buckets[bar.material, bar.waste], (capacity, j))
|
|
120
|
+
remember(bars)
|
|
121
|
+
|
|
122
|
+
# 与原求解器同样的小组重排;多材料大订单也可能含一个小材料组。
|
|
123
|
+
capacity = max(stock_widths, default=0) - left - right + kerf
|
|
124
|
+
lower_bound = ((sum((widths[i] + kerf) * p.count for i, p in enumerate(parts))
|
|
125
|
+
+ capacity - 1) // capacity if capacity > 0 else 0)
|
|
126
|
+
nodes, changed = 0, True
|
|
127
|
+
while (changed and best and -best_score[0] == requested and best_score[1] != lower_bound
|
|
128
|
+
and not runtime.stopped(requested, attempts)):
|
|
129
|
+
changed = False
|
|
130
|
+
for removed in sorted(range(len(best)), key=lambda j: best[j].end):
|
|
131
|
+
group = [j for j, b in enumerate(best) if b.material == best[removed].material]
|
|
132
|
+
items = [(item[0], item[1]) for j in group for item in best[j].items]
|
|
133
|
+
if len(group) < 2 or len(group) > 8 or len(items) > 64:
|
|
134
|
+
continue
|
|
135
|
+
targets = [replace(best[j], items=[], types=Counter(), end=left)
|
|
136
|
+
for j in group if j != removed]
|
|
137
|
+
if sum(widths[i] + kerf for i, _ in items) > sum(b.width - left - right + kerf for b in targets):
|
|
138
|
+
continue
|
|
139
|
+
items.sort(key=lambda item: widths[item[0]], reverse=not options.linear_sort_ascending)
|
|
140
|
+
seen, local_nodes = set(), 0
|
|
141
|
+
|
|
142
|
+
def assign(position):
|
|
143
|
+
nonlocal nodes, local_nodes
|
|
144
|
+
nodes += 1
|
|
145
|
+
local_nodes += 1
|
|
146
|
+
if local_nodes > 20000 or nodes > 100000:
|
|
147
|
+
return False
|
|
148
|
+
if nodes % 64 == 0 and runtime.stopped(requested, attempts):
|
|
149
|
+
return False
|
|
150
|
+
if position == len(items):
|
|
151
|
+
return all(b.items for b in targets)
|
|
152
|
+
state = (position, tuple((b.end, tuple(sorted(b.types.items()))) for b in targets))
|
|
153
|
+
if state in seen:
|
|
154
|
+
return False
|
|
155
|
+
seen.add(state)
|
|
156
|
+
i, instance = items[position]
|
|
157
|
+
symmetric = set()
|
|
158
|
+
for j in sorted(range(len(targets)), key=lambda j: targets[j].width - targets[j].end):
|
|
159
|
+
bar = targets[j]
|
|
160
|
+
signature = (bar.width, bar.waste, bar.end, tuple(sorted(bar.types.items())))
|
|
161
|
+
if signature in symmetric:
|
|
162
|
+
continue
|
|
163
|
+
symmetric.add(signature)
|
|
164
|
+
x = bar.end + kerf if bar.items else left
|
|
165
|
+
if not matches(bar.material, parts[i].material) or x + widths[i] > bar.width - right:
|
|
166
|
+
continue
|
|
167
|
+
saved = bar.copy()
|
|
168
|
+
append(bar, i, instance)
|
|
169
|
+
if assign(position + 1):
|
|
170
|
+
return True
|
|
171
|
+
targets[j] = saved
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
if assign(0):
|
|
175
|
+
remember([b for j, b in enumerate(best) if j not in group] + targets)
|
|
176
|
+
changed = True
|
|
177
|
+
break
|
|
178
|
+
if nodes > 100000 or runtime.stopped(requested, attempts):
|
|
179
|
+
changed = False
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
layouts, placed_counts, stock_instances = [], Counter(), Counter()
|
|
183
|
+
for bar in best:
|
|
184
|
+
stock_i = bar.components[0][0]
|
|
185
|
+
stock, instance = stocks[stock_i], stock_instances[stock_i]
|
|
186
|
+
stock_instances[stock_i] += 1
|
|
187
|
+
height = options.cross_section or stock.height
|
|
188
|
+
placements, cuts = [], []
|
|
189
|
+
previous_end = None
|
|
190
|
+
for local_index, (i, _, integer_x, *_rest) in enumerate(bar.items):
|
|
191
|
+
part = parts[i]
|
|
192
|
+
x, width = integer_x / scale, widths[i] / scale
|
|
193
|
+
placements.append(Placement(i, placed_counts[i], width, height, x, id=part.id,
|
|
194
|
+
material=part.material, angle_start=part.angle_start,
|
|
195
|
+
angle_end=part.angle_end))
|
|
196
|
+
placed_counts[i] += 1
|
|
197
|
+
for is_start, top, angle in ((True, x, part.angle_start),
|
|
198
|
+
(False, x + width, part.angle_end)):
|
|
199
|
+
if abs(top) < 1e-9 or abs(top - bar.width / scale) < 1e-9:
|
|
200
|
+
continue
|
|
201
|
+
if is_start and local_index == 0 and abs(top - left / scale) < 1e-9 and left:
|
|
202
|
+
continue
|
|
203
|
+
if not is_start and abs(top - (bar.width - right) / scale) < 1e-9 and right:
|
|
204
|
+
continue
|
|
205
|
+
allowance = kerf / scale * hypot(height, 0) / (2 * height)
|
|
206
|
+
shift = -allowance if is_start else allowance
|
|
207
|
+
cut = Cut(top + shift, 0, top + shift, height, angle=angle or 90,
|
|
208
|
+
part_indices=(local_index,))
|
|
209
|
+
if is_start and previous_end is not None and abs(cuts[previous_end].x1 - cut.x1) <= 1e-9:
|
|
210
|
+
old = cuts[previous_end]
|
|
211
|
+
cuts[previous_end] = replace(old, x1=(old.x1 + cut.x1) / 2,
|
|
212
|
+
x2=(old.x2 + cut.x2) / 2,
|
|
213
|
+
part_indices=old.part_indices + (local_index,))
|
|
214
|
+
else:
|
|
215
|
+
cuts.append(cut)
|
|
216
|
+
if not is_start:
|
|
217
|
+
previous_end = len(cuts) - 1
|
|
218
|
+
remnant_x = min(bar.width - right, bar.end + kerf) if bar.end < bar.width - right else bar.end
|
|
219
|
+
remnant_width = max(0, bar.width - right - remnant_x)
|
|
220
|
+
remnants = ((Remnant(remnant_width / scale, height, remnant_x / scale, material=bar.material),)
|
|
221
|
+
if remnant_width else ())
|
|
222
|
+
trim_cuts = tuple(Cut(x / scale, 0, x / scale, height)
|
|
223
|
+
for x, trim in ((left - kerf / 2, left),
|
|
224
|
+
(bar.width - right + kerf / 2, right)) if trim)
|
|
225
|
+
component = StockComponent(stock_i, instance, stock_widths[stock_i] / scale,
|
|
226
|
+
stock.id, stock.waste, stock.material)
|
|
227
|
+
layouts.append(Layout(stock_i, instance, bar.width / scale, height, stock.id, bar.material,
|
|
228
|
+
bar.waste, tuple(placements), remnants, tuple(cuts), trim_cuts, (component,)))
|
|
229
|
+
return Result(tuple(layouts), tuple(Unplaced(i, p.count - placed_counts[i], p.id)
|
|
230
|
+
for i, p in enumerate(parts) if p.count > placed_counts[i]),
|
|
231
|
+
runtime.elapsed, True, runtime.cancelled, runtime.timed_out, attempts)
|