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/compat.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""手册兼容入口:保留 bool/out 和错误字符串,计算仍由纯 Python 内核完成。
|
|
2
|
+
|
|
3
|
+
原生 CutEngine 返回结构化模型;需要按 C# 手册迁移调用时使用 CutGLibEngine。
|
|
4
|
+
Out 是 Python 对 ref/out 的显式表示,不加载 CLR,也不伪造旧产品许可证。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from dataclasses import dataclass, replace
|
|
11
|
+
from math import isfinite, radians, tan
|
|
12
|
+
|
|
13
|
+
from .engine import CutEngine, _OPTION_NAMES, _layout_key
|
|
14
|
+
from .models import Layout, Material, Options, Part, Result, Stock, UnsupportedFeatureError, ValidationError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Out:
|
|
19
|
+
value: object = None
|
|
20
|
+
|
|
21
|
+
def __getitem__(self, index):
|
|
22
|
+
if index != 0:
|
|
23
|
+
raise IndexError(index)
|
|
24
|
+
return self.value
|
|
25
|
+
|
|
26
|
+
def __setitem__(self, index, value):
|
|
27
|
+
if index != 0:
|
|
28
|
+
raise IndexError(index)
|
|
29
|
+
self.value = value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QueryResult(tuple):
|
|
33
|
+
"""未提供 Out 时返回 (成功标志, 输出值...);布尔判断使用成功标志。"""
|
|
34
|
+
|
|
35
|
+
def __new__(cls, success, values):
|
|
36
|
+
return super().__new__(cls, (bool(success), *values))
|
|
37
|
+
|
|
38
|
+
def __bool__(self):
|
|
39
|
+
return self[0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _answer(outputs, success, values):
|
|
43
|
+
if outputs:
|
|
44
|
+
if len(outputs) != len(values) or any(not isinstance(out, Out) for out in outputs):
|
|
45
|
+
raise TypeError("out 参数必须逐一传入 Out(),数量需与所选重载相同")
|
|
46
|
+
for out, value in zip(outputs, values):
|
|
47
|
+
out.value = value
|
|
48
|
+
return bool(success)
|
|
49
|
+
return QueryResult(success, values)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# 输入参数个数、每种输出重载的默认值。输出顺序与手册/实测签名一致。
|
|
53
|
+
_QUERIES = {
|
|
54
|
+
"GetMaterial": (1, {3: ("", "", 0.0)}),
|
|
55
|
+
"GetLayoutInfo": (1, {2: (0, 0)}),
|
|
56
|
+
"GetStockInfo": (1, {3: (0.0, 0.0, False), 4: (0.0, 0.0, False, "")}),
|
|
57
|
+
"GetLinearStockInfo": (1, {2: (0.0, False), 3: (0.0, False, "")}),
|
|
58
|
+
"GetLinearCombineStockInfo": (2, {3: (0.0, False, "")}),
|
|
59
|
+
"GetResultPart": (1, {6: (-1, 0.0, 0.0, 0.0, 0.0, False), 7: (-1, 0.0, 0.0, 0.0, 0.0, False, "")}),
|
|
60
|
+
"GetResultLinearPart": (1, {3: (-1, 0.0, 0.0), 4: (-1, 0.0, 0.0, ""),
|
|
61
|
+
6: (-1, 0.0, 0.0, 0.0, 0.0, False), 7: (-1, 0.0, 0.0, 0.0, 0.0, False, False),
|
|
62
|
+
8: (-1, 0.0, 0.0, 0.0, 0.0, False, False, "")}),
|
|
63
|
+
"GetRemainingPart": (1, {5: (-1, 0.0, 0.0, 0.0, 0.0)}),
|
|
64
|
+
"GetRemainingLinearPart": (1, {3: (-1, 0.0, 0.0), 4: (-1, 0.0, 0.0, 0.0)}),
|
|
65
|
+
"GetCut": (1, {5: (0, 0.0, 0.0, 0.0, 0.0), 6: (0, 0.0, 0.0, 0.0, 0.0, 0)}),
|
|
66
|
+
"GetPartCut": (2, {5: (-1, 0.0, 0.0, 0.0, 0.0)}),
|
|
67
|
+
"GetStockCut": (2, {4: (0.0, 0.0, 0.0, 0.0), 5: (0.0, 0.0, 0.0, 0.0, 0)}),
|
|
68
|
+
"GetStockTrimCut": (2, {4: (0.0, 0.0, 0.0, 0.0), 5: (0.0, 0.0, 0.0, 0.0, 0)}),
|
|
69
|
+
"GetLinearCut": (1, {2: (0, 0.0), 3: (0, 0.0, 0.0)}),
|
|
70
|
+
"GetLinearStockCut": (2, {1: (0.0,), 2: (0.0, 0.0)}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CutGLibEngine(CutEngine):
|
|
75
|
+
"""手册中的大小写名称采用兼容契约;snake_case 方法保留 Python 原生契约。"""
|
|
76
|
+
|
|
77
|
+
reference_version = "5.3.8"
|
|
78
|
+
|
|
79
|
+
def __init__(self, options=None, **settings):
|
|
80
|
+
super().__init__(options)
|
|
81
|
+
self._inventory_cache = None
|
|
82
|
+
for name, value in settings.items():
|
|
83
|
+
if name in _OPTION_NAMES:
|
|
84
|
+
setattr(self, name, value)
|
|
85
|
+
elif name in Options.__dataclass_fields__:
|
|
86
|
+
setattr(self.options, name, value)
|
|
87
|
+
else:
|
|
88
|
+
raise TypeError(f"未知计算参数:{name}")
|
|
89
|
+
|
|
90
|
+
def __setattr__(self, name, value):
|
|
91
|
+
if name == "MaxDecimalPoint":
|
|
92
|
+
if type(value) is not int:
|
|
93
|
+
raise TypeError("MaxDecimalPoint 必须是整数")
|
|
94
|
+
value = min(6, max(0, value))
|
|
95
|
+
super().__setattr__(name, value)
|
|
96
|
+
|
|
97
|
+
def _modify(self, action):
|
|
98
|
+
value = super()._modify(action)
|
|
99
|
+
self._inventory_cache = None
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _number(value):
|
|
104
|
+
if isinstance(value, (str, bytes, bool)):
|
|
105
|
+
raise TypeError("尺寸必须是数值")
|
|
106
|
+
value = float(value)
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def _append(self, record):
|
|
110
|
+
# 原 DLL 的 Add 阶段接受负尺寸;执行阶段再报告尺寸错误,原生入口仍严格校验。
|
|
111
|
+
if not isfinite(record.width) or not isfinite(record.height):
|
|
112
|
+
return False
|
|
113
|
+
if abs(record.width) <= 1e-12 or abs(record.height) <= 1e-12:
|
|
114
|
+
return False
|
|
115
|
+
if type(record.count) is not int or type(record.material) is not int:
|
|
116
|
+
raise TypeError("数量与材料索引必须是整数")
|
|
117
|
+
target = self._parts if isinstance(record, Part) else self._stocks
|
|
118
|
+
self._modify(lambda: target.append(record))
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def AddPart(self, width, height, count=1, rotatable=True, id="", material=-1):
|
|
122
|
+
if type(count) is bool:
|
|
123
|
+
count, rotatable = 1, count
|
|
124
|
+
return self._append(Part(self._number(width), self._number(height), max(0, count), rotatable, id, material))
|
|
125
|
+
|
|
126
|
+
def AddStock(self, width, height, count=1, id="", waste=False, material=-1):
|
|
127
|
+
return self._append(Stock(self._number(width), self._number(height), max(0, count), id, waste, material))
|
|
128
|
+
|
|
129
|
+
def AddLinearPart(self, length, count=1, *args):
|
|
130
|
+
if args and isinstance(args[0], str):
|
|
131
|
+
if len(args) > 2:
|
|
132
|
+
raise TypeError("AddLinearPart 参数过多")
|
|
133
|
+
id, material = args[0], args[1] if len(args) == 2 else -1
|
|
134
|
+
start = end = 0.0
|
|
135
|
+
elif args:
|
|
136
|
+
if not 2 <= len(args) <= 4:
|
|
137
|
+
raise TypeError("斜角重载需要起始角和结束角")
|
|
138
|
+
start, end = map(self._number, args[:2])
|
|
139
|
+
id, material = args[2] if len(args) > 2 else "", args[3] if len(args) > 3 else -1
|
|
140
|
+
else:
|
|
141
|
+
id, material, start, end = "", -1, 0.0, 0.0
|
|
142
|
+
return self._append(Part(self._number(length), 1.0, max(0, count), False, id, material, start, end))
|
|
143
|
+
|
|
144
|
+
def AddLinearStock(self, length, count=1, *args):
|
|
145
|
+
if len(args) == 1 and type(args[0]) is int:
|
|
146
|
+
id, waste, material = "", False, args[0]
|
|
147
|
+
elif len(args) <= 3:
|
|
148
|
+
id, waste, material = (args[0] if args else "", args[1] if len(args) > 1 else False,
|
|
149
|
+
args[2] if len(args) > 2 else -1)
|
|
150
|
+
else:
|
|
151
|
+
raise TypeError("AddLinearStock 参数过多")
|
|
152
|
+
return self._append(Stock(self._number(length), 1.0, max(0, count), id, waste, material))
|
|
153
|
+
|
|
154
|
+
def AddMaterial(self, id, description, thickness):
|
|
155
|
+
thickness = self._number(thickness)
|
|
156
|
+
if not isfinite(thickness):
|
|
157
|
+
raise ValidationError("材质厚度必须有限")
|
|
158
|
+
def add():
|
|
159
|
+
self._materials.append(Material(id, description, thickness))
|
|
160
|
+
return len(self._materials) - 1
|
|
161
|
+
return self._modify(add)
|
|
162
|
+
|
|
163
|
+
def Clear(self):
|
|
164
|
+
# 原 5.3.8 Clear 实测保留三个搜索开关和尺寸精度,其余恢复默认。
|
|
165
|
+
retained = {name: getattr(self.options, name) for name in
|
|
166
|
+
("linear_allow_flipping", "linear_sort_ascending", "use_large_stock_first", "max_decimal_point")}
|
|
167
|
+
self.clear(reset_options=True)
|
|
168
|
+
for name, value in retained.items():
|
|
169
|
+
setattr(self.options, name, value)
|
|
170
|
+
|
|
171
|
+
def _run_documented(self, linear, callback):
|
|
172
|
+
self._inventory_cache = None
|
|
173
|
+
if not self.part_count:
|
|
174
|
+
return "Number of parts must be more than zero"
|
|
175
|
+
if not self.stocks:
|
|
176
|
+
return "Length of stock 0 is negative (0). Please check trims and saw kerf values to be less then the stock length."
|
|
177
|
+
def progress(_):
|
|
178
|
+
stop = Out(False)
|
|
179
|
+
returned = callback(stop)
|
|
180
|
+
return bool(stop.value) or returned is True
|
|
181
|
+
try:
|
|
182
|
+
result = self.execute_linear(progress if callback else None) if linear else self.execute(progress if callback else None)
|
|
183
|
+
except ValidationError as exc:
|
|
184
|
+
if "无法" in str(exc) or "cannot" in str(exc):
|
|
185
|
+
# 公开错误字符串与原版一致;异常输入仍可通过原生接口获得详细中文错误。
|
|
186
|
+
for part in self.parts:
|
|
187
|
+
return f"Part {part.id} cannot be placed because it exceeds the stock size"
|
|
188
|
+
return str(exc)
|
|
189
|
+
if result.cancelled:
|
|
190
|
+
return "Calculation stopped by the user"
|
|
191
|
+
if result.timed_out:
|
|
192
|
+
return "Calculation time limit reached"
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
def Execute(self, callback=None):
|
|
196
|
+
return self._run_documented(False, callback)
|
|
197
|
+
|
|
198
|
+
def ExecuteLinear(self, callback=None):
|
|
199
|
+
return self._run_documented(True, callback)
|
|
200
|
+
|
|
201
|
+
def _execute(self, linear, callback, *, solver=None):
|
|
202
|
+
self._inventory_cache = None
|
|
203
|
+
return super()._execute(linear, callback, solver=solver)
|
|
204
|
+
|
|
205
|
+
def _inventory(self):
|
|
206
|
+
"""按原版材料/尺寸排序,批次保存未用库存,避免展开 20000 根无限原料。"""
|
|
207
|
+
if self._inventory_cache is not None:
|
|
208
|
+
return self._inventory_cache
|
|
209
|
+
if self._result is None:
|
|
210
|
+
return (), {}, 0
|
|
211
|
+
consumed = Counter()
|
|
212
|
+
active = {}
|
|
213
|
+
combined = []
|
|
214
|
+
for index, layout in enumerate(self.result.layouts):
|
|
215
|
+
components = layout.components
|
|
216
|
+
if len(components) > 1:
|
|
217
|
+
consumed.update(c.input_index for c in components)
|
|
218
|
+
ordered = sorted(components, key=lambda c: (c.width, c.input_index))
|
|
219
|
+
id = ";".join(self._component_id(c) for c in ordered)
|
|
220
|
+
combined.append((layout.material, layout.width, 1.0 if self.result.linear else layout.height, layout.waste,
|
|
221
|
+
id, 1, [index], None))
|
|
222
|
+
else:
|
|
223
|
+
consumed[layout.input_index] += 1
|
|
224
|
+
active.setdefault(layout.input_index, []).append(index)
|
|
225
|
+
batches = list(combined)
|
|
226
|
+
for i, stock in enumerate(self.stocks):
|
|
227
|
+
layouts = active.get(i, [])
|
|
228
|
+
unused = (stock.count or 20000) - consumed[i]
|
|
229
|
+
count = max(0, unused) + len(layouts)
|
|
230
|
+
if count:
|
|
231
|
+
batches.append((stock.material, stock.width, stock.height, stock.waste, stock.id, count, layouts, i))
|
|
232
|
+
batches.sort(key=lambda b: (-b[0], -b[1] * b[2], b[1], -b[3]))
|
|
233
|
+
segments, mapping, start = [], {}, 0
|
|
234
|
+
for batch in batches:
|
|
235
|
+
segments.append((start, batch))
|
|
236
|
+
mapping.update((local, start + offset) for offset, local in enumerate(batch[6]))
|
|
237
|
+
start += batch[5]
|
|
238
|
+
self._inventory_cache = tuple(segments), mapping, start
|
|
239
|
+
return self._inventory_cache
|
|
240
|
+
|
|
241
|
+
def _component_id(self, component):
|
|
242
|
+
if component.id:
|
|
243
|
+
return component.id
|
|
244
|
+
return str(sum(stock.count or 20000 for stock in self.stocks[:component.input_index])
|
|
245
|
+
+ component.instance_index)
|
|
246
|
+
|
|
247
|
+
def _stock_id(self, stock, batch, local):
|
|
248
|
+
if batch[4] or batch[7] is None:
|
|
249
|
+
return batch[4]
|
|
250
|
+
input_index = batch[7]
|
|
251
|
+
base = sum(item.count or 20000 for item in self.stocks[:input_index])
|
|
252
|
+
if local is not None:
|
|
253
|
+
return str(base + self.result.layouts[local].instance_index)
|
|
254
|
+
start = next(start for start, candidate in self._inventory()[0] if candidate is batch)
|
|
255
|
+
offset = stock - start - len(batch[6])
|
|
256
|
+
used = {component.instance_index for layout in self.result.layouts for component in layout.components
|
|
257
|
+
if component.input_index == input_index}
|
|
258
|
+
used.update(layout.instance_index for layout in self.result.layouts
|
|
259
|
+
if layout.input_index == input_index and len(layout.components) <= 1)
|
|
260
|
+
instance = 0
|
|
261
|
+
while instance in used or offset:
|
|
262
|
+
if instance not in used:
|
|
263
|
+
offset -= 1
|
|
264
|
+
instance += 1
|
|
265
|
+
return str(base + instance)
|
|
266
|
+
|
|
267
|
+
def _stock(self, index):
|
|
268
|
+
segments, _, total = self._inventory()
|
|
269
|
+
if self._result is None:
|
|
270
|
+
return None
|
|
271
|
+
if index < 0:
|
|
272
|
+
raise IndexError(index)
|
|
273
|
+
if index >= total:
|
|
274
|
+
return None
|
|
275
|
+
for start, batch in reversed(segments):
|
|
276
|
+
if index >= start:
|
|
277
|
+
offset = index - start
|
|
278
|
+
local = batch[6][offset] if offset < len(batch[6]) else None
|
|
279
|
+
return batch, local
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _located(self, index):
|
|
283
|
+
if self._result is None or not 0 <= index < self.part_count:
|
|
284
|
+
return None
|
|
285
|
+
located = self.get_result_part(index)
|
|
286
|
+
return None if located is None else (self._inventory()[1][located[0]], located[1])
|
|
287
|
+
|
|
288
|
+
def _unplaced_values(self, index, name, n, flags):
|
|
289
|
+
# 原映射越界变为 -1,manager 再把 -1 映射至内部排序后的首件。
|
|
290
|
+
# 首件以已放置优先、原料序号、y、x 排序,不依赖特定订单或 ID。
|
|
291
|
+
locations = self._part_locations()
|
|
292
|
+
if 0 <= index < len(locations):
|
|
293
|
+
offset = index
|
|
294
|
+
for record in self.parts:
|
|
295
|
+
if offset < record.count:
|
|
296
|
+
part = record
|
|
297
|
+
width, height = record.width, record.height
|
|
298
|
+
break
|
|
299
|
+
offset -= record.count
|
|
300
|
+
else:
|
|
301
|
+
placed = [(self._inventory()[1][value[0]], value[1].y, value[1].x, value[1])
|
|
302
|
+
for value in locations if value is not None]
|
|
303
|
+
if placed:
|
|
304
|
+
placement = min(placed, key=lambda value: value[:3])[3]
|
|
305
|
+
part = self.parts[placement.input_index]
|
|
306
|
+
width, height = placement.width, placement.height
|
|
307
|
+
if self.result.linear:
|
|
308
|
+
height = part.height
|
|
309
|
+
else:
|
|
310
|
+
candidates = [(record.rotatable, -max(record.width, record.height)
|
|
311
|
+
if record.rotatable else -record.width,
|
|
312
|
+
-min(record.width, record.height) if record.rotatable else -record.height,
|
|
313
|
+
-i, record) for i, record in enumerate(self.parts) if record.count]
|
|
314
|
+
if not candidates:
|
|
315
|
+
return None
|
|
316
|
+
part = min(candidates, key=lambda value: value[:4])[4]
|
|
317
|
+
width, height = part.width, part.height
|
|
318
|
+
if part.rotatable and height > width:
|
|
319
|
+
width, height = height, width
|
|
320
|
+
if name == "GetResultPart":
|
|
321
|
+
return (-1, width, height, 0.0, 0.0, part.rotatable, part.id)[:n]
|
|
322
|
+
if n <= 4:
|
|
323
|
+
return (-1, width, 0.0, part.id)[:n]
|
|
324
|
+
values = (-1, width, part.angle_start, part.angle_end, 0.0, False)
|
|
325
|
+
return (*values, part.id) if n == 7 and flags.get("pdf_id") else (*values, False, part.id)[:n]
|
|
326
|
+
|
|
327
|
+
def _cut_values(self, cut, layout):
|
|
328
|
+
"""匹配原结果缓冲区:直切是线段,型材斜切存 position/angle。"""
|
|
329
|
+
if self.result.linear:
|
|
330
|
+
if self._result_options.cross_section > 0:
|
|
331
|
+
angle = cut.angle
|
|
332
|
+
if angle == 90 and all(part.angle_start != 90 and part.angle_end != 90
|
|
333
|
+
for part in layout.parts):
|
|
334
|
+
angle = 0.0
|
|
335
|
+
position = max(cut.x1, cut.x2) if angle > 90 else min(cut.x1, cut.x2)
|
|
336
|
+
return (position, angle, 0.0, 0.0, 0)
|
|
337
|
+
return (cut.x1, cut.y1, cut.x2, cut.y2, -1)
|
|
338
|
+
return (cut.x1, cut.y1, cut.x2, cut.y2, cut.level)
|
|
339
|
+
|
|
340
|
+
def _query(self, name, inputs, n, defaults, flags):
|
|
341
|
+
empty = (False, defaults)
|
|
342
|
+
if name == "GetMaterial":
|
|
343
|
+
index = inputs[0]
|
|
344
|
+
if index < 0:
|
|
345
|
+
raise IndexError(index)
|
|
346
|
+
if index >= len(self.materials):
|
|
347
|
+
return empty
|
|
348
|
+
m = self.materials[index]
|
|
349
|
+
return True, (m.id, m.description, m.thickness)
|
|
350
|
+
if self._result is None:
|
|
351
|
+
# 该返回值已经由原 DLL 所有执行前重载逐项测量。
|
|
352
|
+
return empty
|
|
353
|
+
mapping = self._inventory()[1]
|
|
354
|
+
if name in ("GetStockInfo", "GetLinearStockInfo"):
|
|
355
|
+
entry = self._stock(inputs[0])
|
|
356
|
+
if entry is None:
|
|
357
|
+
return empty
|
|
358
|
+
batch, local = entry
|
|
359
|
+
if local is not None:
|
|
360
|
+
layout = self.result.layouts[local]
|
|
361
|
+
width, height = layout.width, batch[2] if self.result.linear else layout.height
|
|
362
|
+
else:
|
|
363
|
+
width, height = batch[1:3]
|
|
364
|
+
id = self._stock_id(inputs[0], batch, local)
|
|
365
|
+
values = (width, height, local is not None, id) if name == "GetStockInfo" else (width, local is not None, id)
|
|
366
|
+
return True, values[:n]
|
|
367
|
+
if name == "GetLinearCombineStockInfo":
|
|
368
|
+
# 原方法先检查组件下标与全部库存数,再访问组合原料下标。
|
|
369
|
+
if inputs[1] >= self.StockCount:
|
|
370
|
+
return empty
|
|
371
|
+
if inputs[0] < 0 or inputs[0] >= self.StockCount:
|
|
372
|
+
raise IndexError(inputs[0])
|
|
373
|
+
entry = self._stock(inputs[0])
|
|
374
|
+
if entry is None or entry[1] is None:
|
|
375
|
+
return empty
|
|
376
|
+
components = sorted(self.result.layouts[entry[1]].components, key=lambda c: (c.width, c.input_index))
|
|
377
|
+
if len(components) > 1 and inputs[1] < 0:
|
|
378
|
+
raise IndexError(inputs[1])
|
|
379
|
+
if len(components) <= 1 or not 0 <= inputs[1] < len(components):
|
|
380
|
+
return empty
|
|
381
|
+
c = components[inputs[1]]
|
|
382
|
+
return True, (c.width, True, self._component_id(c))
|
|
383
|
+
if name == "GetLayoutInfo":
|
|
384
|
+
if not 0 <= inputs[0] < self.layout_count:
|
|
385
|
+
return empty
|
|
386
|
+
group = self.result.layout_groups[inputs[0]]
|
|
387
|
+
return True, (mapping[group.first_layout_index], group.layout_count)
|
|
388
|
+
if name in ("GetResultPart", "GetResultLinearPart"):
|
|
389
|
+
located = self._located(inputs[0])
|
|
390
|
+
if located is None:
|
|
391
|
+
values = self._unplaced_values(inputs[0], name, n, flags)
|
|
392
|
+
return (False, values) if values else empty
|
|
393
|
+
stock, p = located
|
|
394
|
+
if name == "GetResultPart":
|
|
395
|
+
# 原 GetResultPart 返回输入宽高,旋转由 aRotated 单独表示。
|
|
396
|
+
w, h = (p.height, p.width) if p.rotated and not self.result.linear else (p.width, p.height)
|
|
397
|
+
if self.result.linear:
|
|
398
|
+
h = self.parts[p.input_index].height
|
|
399
|
+
return True, (stock, w, h, p.x, p.y, p.rotated, p.id)[:n]
|
|
400
|
+
if n <= 4:
|
|
401
|
+
return True, (stock, p.width, p.x, p.id)[:n]
|
|
402
|
+
values = (stock, p.width, p.angle_start, p.angle_end, p.x, p.rotated)
|
|
403
|
+
if n == 7 and flags.get("pdf_id"):
|
|
404
|
+
return True, (*values, p.id)
|
|
405
|
+
return True, (*values, p.flipped, p.id)[:n]
|
|
406
|
+
if name in ("GetRemainingPart", "GetRemainingLinearPart"):
|
|
407
|
+
remnants = self._remnants()
|
|
408
|
+
if not 0 <= inputs[0] < len(remnants):
|
|
409
|
+
if inputs[0] < 0:
|
|
410
|
+
raise IndexError(inputs[0])
|
|
411
|
+
return empty
|
|
412
|
+
stock, r = remnants[inputs[0]]
|
|
413
|
+
width, x, angle = r.width, r.x, r.angle
|
|
414
|
+
if self.result.linear and self._result_options.cross_section > 0:
|
|
415
|
+
parts = self.result.layouts[stock].parts
|
|
416
|
+
if parts:
|
|
417
|
+
last = max(parts, key=lambda part: part.x + part.width)
|
|
418
|
+
if last.angle_end % 90:
|
|
419
|
+
overlap = abs(self._result_options.cross_section / tan(radians(last.angle_end)))
|
|
420
|
+
width, x = width + overlap, x - overlap
|
|
421
|
+
angle = last.angle_end
|
|
422
|
+
values = ((mapping[stock], width, 1.0 if self.result.linear else r.height, x, r.y) if name == "GetRemainingPart"
|
|
423
|
+
else (mapping[stock], width, x, angle))
|
|
424
|
+
return True, values[:n]
|
|
425
|
+
if name in ("GetCut", "GetLinearCut"):
|
|
426
|
+
cuts = self._cuts()
|
|
427
|
+
if not 0 <= inputs[0] < len(cuts):
|
|
428
|
+
return empty
|
|
429
|
+
stock, cut = cuts[inputs[0]]
|
|
430
|
+
segment = self._cut_values(cut, self.result.layouts[stock])
|
|
431
|
+
values = ((mapping[stock], *segment[:2]) if name == "GetLinearCut" else
|
|
432
|
+
(mapping[stock], *segment))
|
|
433
|
+
return True, values[:n]
|
|
434
|
+
if name == "GetPartCut":
|
|
435
|
+
if not 0 <= inputs[0] < self.part_count:
|
|
436
|
+
return empty
|
|
437
|
+
if self.result.linear:
|
|
438
|
+
# 原版此路径空引用异常;纯 Python 保留安全的 false,差异报告单独列出。
|
|
439
|
+
return empty
|
|
440
|
+
stock, cuts = self._part_cuts(inputs[0])
|
|
441
|
+
if inputs[1] < 0 and cuts:
|
|
442
|
+
raise IndexError(inputs[1])
|
|
443
|
+
if not 0 <= inputs[1] < len(cuts):
|
|
444
|
+
return empty
|
|
445
|
+
c = cuts[inputs[1]]
|
|
446
|
+
return True, (mapping[stock], c.x1, c.y1, c.x2, c.y2)
|
|
447
|
+
if inputs[0] < 0:
|
|
448
|
+
return empty
|
|
449
|
+
entry = self._stock(inputs[0])
|
|
450
|
+
if entry is None or entry[1] is None:
|
|
451
|
+
return empty
|
|
452
|
+
layout = self.result.layouts[entry[1]]
|
|
453
|
+
cuts = layout.trim_cuts if name == "GetStockTrimCut" else layout.cuts
|
|
454
|
+
if inputs[1] < 0 and cuts:
|
|
455
|
+
raise IndexError(inputs[1])
|
|
456
|
+
if not 0 <= inputs[1] < len(cuts):
|
|
457
|
+
return empty
|
|
458
|
+
c = cuts[inputs[1]]
|
|
459
|
+
segment = self._cut_values(c, layout)
|
|
460
|
+
values = segment[:2] if name == "GetLinearStockCut" else segment
|
|
461
|
+
return True, values[:n]
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def StockCount(self):
|
|
465
|
+
return self._inventory()[2]
|
|
466
|
+
|
|
467
|
+
LinearStockCount = StockCount
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def UsedStockCount(self):
|
|
471
|
+
return len(self._result.layouts) if self._result else 0
|
|
472
|
+
|
|
473
|
+
UsedLinearStockCount = UsedStockCount
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def RemainingPartCount(self):
|
|
477
|
+
return self.remaining_part_count if self._result is not None else 0
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def LayoutCount(self):
|
|
481
|
+
return self.layout_count if self._result is not None else 0
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def PlacedPartCount(self):
|
|
485
|
+
return self.placed_part_count if self._result is not None else 0
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def ElapsedTime(self):
|
|
489
|
+
return self.elapsed_time if self._result is not None else 0.0
|
|
490
|
+
|
|
491
|
+
def GetPartMaterial(self, part):
|
|
492
|
+
if self._result is None or not 0 <= part < self.part_count:
|
|
493
|
+
return -1
|
|
494
|
+
return self.get_part_material(part)
|
|
495
|
+
|
|
496
|
+
def GetStockMaterial(self, stock):
|
|
497
|
+
entry = self._stock(stock)
|
|
498
|
+
return entry[0][0] if entry else -1
|
|
499
|
+
|
|
500
|
+
def GetRemainingPartMaterial(self, part):
|
|
501
|
+
if self._result is None:
|
|
502
|
+
return -1
|
|
503
|
+
if not 0 <= part < self.remaining_part_count:
|
|
504
|
+
raise IndexError(part)
|
|
505
|
+
return self.get_remaining_part_material(part)
|
|
506
|
+
|
|
507
|
+
def GetLayoutFillRatio(self, layout):
|
|
508
|
+
return self.get_layout_fill_ratio(layout) if self._result and 0 <= layout < self.layout_count else 0.0
|
|
509
|
+
|
|
510
|
+
def GetPartCountOnStock(self, stock):
|
|
511
|
+
if stock < 0:
|
|
512
|
+
return 0
|
|
513
|
+
entry = self._stock(stock)
|
|
514
|
+
return len(self.result.layouts[entry[1]].parts) if entry and entry[1] is not None else 0
|
|
515
|
+
|
|
516
|
+
def GetPartIndexOnStock(self, stock, part):
|
|
517
|
+
if stock < 0:
|
|
518
|
+
return -1
|
|
519
|
+
entry = self._stock(stock)
|
|
520
|
+
if not entry or entry[1] is None or not 0 <= part < len(self.result.layouts[entry[1]].parts):
|
|
521
|
+
return -1
|
|
522
|
+
return self.get_part_index_on_stock(entry[1], part)
|
|
523
|
+
|
|
524
|
+
def GetRemainingPartCountOnStock(self, stock):
|
|
525
|
+
if stock < 0:
|
|
526
|
+
return 0
|
|
527
|
+
entry = self._stock(stock)
|
|
528
|
+
return len(self.result.layouts[entry[1]].all_remnants) if entry and entry[1] is not None else 0
|
|
529
|
+
|
|
530
|
+
def GetRemainingPartIndexOnStock(self, stock, part):
|
|
531
|
+
if stock < 0:
|
|
532
|
+
return -1
|
|
533
|
+
entry = self._stock(stock)
|
|
534
|
+
if not entry or entry[1] is None or not 0 <= part < len(self.result.layouts[entry[1]].all_remnants):
|
|
535
|
+
return -1
|
|
536
|
+
return self.get_remaining_part_index_on_stock(entry[1], part)
|
|
537
|
+
|
|
538
|
+
def GetLinearCombineStockCount(self, stock):
|
|
539
|
+
entry = self._stock(stock)
|
|
540
|
+
return self.get_linear_combine_stock_count(entry[1]) if entry and entry[1] is not None else 0
|
|
541
|
+
|
|
542
|
+
def GetPartCutsCount(self, part):
|
|
543
|
+
return self.get_part_cuts_count(part) if self._result and not self.result.linear and 0 <= part < self.part_count else 0
|
|
544
|
+
|
|
545
|
+
def GetCutsCount(self):
|
|
546
|
+
return self.get_cuts_count() if self._result else 0
|
|
547
|
+
|
|
548
|
+
GetLinearCutsCount = GetCutsCount
|
|
549
|
+
|
|
550
|
+
def GetStockCutCount(self, stock):
|
|
551
|
+
if stock < 0:
|
|
552
|
+
return 0
|
|
553
|
+
entry = self._stock(stock)
|
|
554
|
+
return self.get_stock_cut_count(entry[1]) if entry and entry[1] is not None else 0
|
|
555
|
+
|
|
556
|
+
def GetStockTrimCutCount(self, stock):
|
|
557
|
+
if stock < 0:
|
|
558
|
+
return 0
|
|
559
|
+
entry = self._stock(stock)
|
|
560
|
+
return self.get_stock_trim_cut_count(entry[1]) if entry and entry[1] is not None else 0
|
|
561
|
+
|
|
562
|
+
def _export(self, method, target, *args, **kwargs):
|
|
563
|
+
if method == "export_ptx" and self._result and self.result.linear:
|
|
564
|
+
return False
|
|
565
|
+
# 文件系统/不可写流异常按原版传播,不把 IO 错误伪装成 false。
|
|
566
|
+
if self._result is None:
|
|
567
|
+
return False
|
|
568
|
+
if target is None:
|
|
569
|
+
raise TypeError("导出目标不能为 None")
|
|
570
|
+
getattr(self, method)(target, *args, **kwargs)
|
|
571
|
+
return True
|
|
572
|
+
|
|
573
|
+
def ExportToCSV(self, target):
|
|
574
|
+
return self._export("export_csv", target)
|
|
575
|
+
|
|
576
|
+
ExportToCSV_Stream = ExportToCSV
|
|
577
|
+
|
|
578
|
+
def ExportToDXF(self, target, include_part_ids):
|
|
579
|
+
return self._export("export_dxf", target, include_part_ids)
|
|
580
|
+
|
|
581
|
+
ExportToDXF_Stream = ExportToDXF
|
|
582
|
+
|
|
583
|
+
def ExportCutsToDXF(self, target, include_cut_number, include_stock_panel):
|
|
584
|
+
return self._export("export_cuts_dxf", target, include_cut_number, include_stock_panel)
|
|
585
|
+
|
|
586
|
+
ExportCutsToDXF_Stream = ExportCutsToDXF
|
|
587
|
+
|
|
588
|
+
def ExportToPTX(self, target, job, customer, material, thickness, units, scale, require_all_trim_cuts=False):
|
|
589
|
+
return self._export("export_ptx", target, job, customer, material, thickness, units, scale, require_all_trim_cuts)
|
|
590
|
+
|
|
591
|
+
def ExportToPTX_Stream(self, target, *args):
|
|
592
|
+
if len(args) == 6:
|
|
593
|
+
# 原七参数流重载的顺序与文件重载不同,默认保留全部修边。
|
|
594
|
+
job, material, thickness, customer, units, scale = args
|
|
595
|
+
return self.ExportToPTX(target, job, customer, material, thickness, units, scale, True)
|
|
596
|
+
if len(args) == 7:
|
|
597
|
+
return self.ExportToPTX(target, *args)
|
|
598
|
+
raise TypeError("ExportToPTX_Stream 需要六个或七个任务参数")
|
|
599
|
+
|
|
600
|
+
def _create_image(self, stock, target, max_size, linear, stream=False):
|
|
601
|
+
if not stream and not hasattr(target, "write"):
|
|
602
|
+
# 原文件入口先创建文件,再查询原料;无结果时也会留下空文件。
|
|
603
|
+
with open(target, "wb") as destination:
|
|
604
|
+
return self._create_image(stock, destination, max_size, linear, stream=True)
|
|
605
|
+
entry = self._stock(stock)
|
|
606
|
+
if entry is None:
|
|
607
|
+
return False
|
|
608
|
+
if target is None:
|
|
609
|
+
raise TypeError("图片输出流不能为 None")
|
|
610
|
+
batch, local = entry
|
|
611
|
+
layout = self.result.layouts[local] if local is not None else Layout(
|
|
612
|
+
-1, stock, batch[1], batch[2], batch[4], batch[0], batch[3])
|
|
613
|
+
if self.result.linear:
|
|
614
|
+
layout = replace(layout, height=batch[2])
|
|
615
|
+
from .exporters import render_png
|
|
616
|
+
target.write(render_png(layout, linear=linear, max_size=min(4000, max(50, max_size))))
|
|
617
|
+
return True
|
|
618
|
+
|
|
619
|
+
def CreateStockImage(self, stock, target, max_size=1000):
|
|
620
|
+
return self._create_image(stock, target, max_size, False)
|
|
621
|
+
|
|
622
|
+
def CreateStockImage_Stream(self, stock, target, max_size=1000):
|
|
623
|
+
return self._create_image(stock, target, max_size, False, stream=True)
|
|
624
|
+
|
|
625
|
+
def CreateLinearStockImage(self, stock, target, max_size=1000):
|
|
626
|
+
return self._create_image(stock, target, max_size, True)
|
|
627
|
+
|
|
628
|
+
def CreateLinearStockImage_Stream(self, stock, target, max_size=1000):
|
|
629
|
+
return self._create_image(stock, target, max_size, True, stream=True)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _query_method(name, input_count, variants):
|
|
633
|
+
def method(self, *args, include_id=False, include_angles=False, include_angle=False,
|
|
634
|
+
with_level=False, include_flipped=True):
|
|
635
|
+
if len(args) < input_count:
|
|
636
|
+
raise TypeError(f"{name} 缺少输入参数")
|
|
637
|
+
inputs, outputs = args[:input_count], args[input_count:]
|
|
638
|
+
n = len(outputs) if outputs else min(variants)
|
|
639
|
+
if not outputs:
|
|
640
|
+
if name == "GetResultLinearPart" and include_angles:
|
|
641
|
+
n = (7 if include_flipped else 6) + bool(include_id)
|
|
642
|
+
elif include_id or include_angle or with_level:
|
|
643
|
+
n += 1
|
|
644
|
+
if n not in variants:
|
|
645
|
+
raise TypeError(f"{name} 不支持 {n} 个 out 参数")
|
|
646
|
+
flags = {"pdf_id": name == "GetResultLinearPart" and n == 7 and
|
|
647
|
+
((outputs and isinstance(outputs[-1].value, str)) or not include_flipped)}
|
|
648
|
+
defaults = variants[n]
|
|
649
|
+
if flags["pdf_id"]:
|
|
650
|
+
defaults = (*defaults[:-1], "")
|
|
651
|
+
success, values = self._query(name, inputs, n, defaults, flags)
|
|
652
|
+
return _answer(outputs, success, values)
|
|
653
|
+
method.__name__ = name
|
|
654
|
+
method.__doc__ = f"手册 {name};支持 Out 引用或返回 (成功标志, 输出值...)。"
|
|
655
|
+
return method
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
for _name, (_inputs, _variants) in _QUERIES.items():
|
|
659
|
+
setattr(CutGLibEngine, _name, _query_method(_name, _inputs, _variants))
|