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.
@@ -0,0 +1,558 @@
1
+ """独立任务引擎:管理输入、调用求解器并查询结果,不依赖 Web 框架。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from dataclasses import asdict, fields, replace
7
+ from pathlib import Path
8
+ from threading import Lock
9
+ from typing import Callable
10
+
11
+ from ._common import Runtime, integer, normalize_options, number, validate
12
+ from .models import (
13
+ Cut, Layout, LayoutGroup, Material, Options, Part, Placement, Progress,
14
+ Remnant, Result, Stock, StockComponent, ValidationError,
15
+ )
16
+
17
+
18
+ def _pascal(name: str) -> str:
19
+ return "".join(word.capitalize() for word in name.split("_"))
20
+
21
+
22
+ _OPTION_NAMES = {_pascal(f.name): f.name for f in fields(Options)}
23
+
24
+
25
+ def _layout_key(layout: Layout) -> tuple:
26
+ """不以零件编号分组;逐件溯源仍完整保存在每一张布局中。"""
27
+ return (layout.input_index, layout.width, layout.height,
28
+ tuple(sorted((p.width, p.height, p.x, p.y, p.rotated, p.flipped,
29
+ p.angle_start, p.angle_end, p.material) for p in layout.parts)),
30
+ tuple((c.x1, c.y1, c.x2, c.y2, c.level) for c in layout.cuts))
31
+
32
+
33
+ def group_layouts(result: Result, options: Options) -> Result:
34
+ groups: dict[tuple, list[Layout]] = defaultdict(list)
35
+ for layout in result.layouts:
36
+ groups[_layout_key(layout)].append(layout)
37
+ layouts: list[Layout] = []
38
+ summaries: list[LayoutGroup] = []
39
+ for group in groups.values():
40
+ limit = (options.max_layout_size if options.use_layout_minimization else 0) or len(group)
41
+ for start in range(0, len(group), limit):
42
+ chunk = group[start:start + limit]
43
+ summaries.append(LayoutGroup(len(layouts), len(chunk)))
44
+ layouts.extend(chunk)
45
+ return replace(result, layouts=tuple(layouts), layout_groups=tuple(summaries))
46
+
47
+
48
+ class CutEngine:
49
+ """一个实例表示一个切割任务;Web 的不同请求应分别创建实例。
50
+
51
+ Python 接口返回数据模型,非法参数抛 ValidationError。旧 PascalCase 名称
52
+ 作为迁移别名保留,但不仿造 .NET 的 out 参数和授权注册接口。
53
+ """
54
+
55
+ version = "0.2.3"
56
+
57
+ def __init__(self, options: Options | None = None, **settings):
58
+ self.options = normalize_options(options) if options is not None else Options()
59
+ self._parts: list[Part] = []
60
+ self._stocks: list[Stock] = []
61
+ self._materials: list[Material] = []
62
+ self._result: Result | None = None
63
+ self._result_options: Options | None = None
64
+ self._locations_cache: list[tuple[int, Placement] | None] | None = None
65
+ self._lock = Lock()
66
+ self._running = False
67
+ for name, value in settings.items():
68
+ field_name = _OPTION_NAMES.get(name, name)
69
+ if field_name not in Options.__dataclass_fields__:
70
+ raise TypeError(f"未知计算参数:{name}")
71
+ setattr(self.options, field_name, value)
72
+ self.options = normalize_options(self.options)
73
+
74
+ def __getattr__(self, name):
75
+ if name in _OPTION_NAMES:
76
+ return getattr(self.options, _OPTION_NAMES[name])
77
+ raise AttributeError(name)
78
+
79
+ def __setattr__(self, name, value):
80
+ if name in _OPTION_NAMES and "options" in self.__dict__:
81
+ setattr(self.options, _OPTION_NAMES[name], value)
82
+ else:
83
+ object.__setattr__(self, name, value)
84
+
85
+ @property
86
+ def parts(self) -> tuple[Part, ...]:
87
+ return tuple(self._parts)
88
+
89
+ @property
90
+ def stocks(self) -> tuple[Stock, ...]:
91
+ return tuple(self._stocks)
92
+
93
+ @property
94
+ def materials(self) -> tuple[Material, ...]:
95
+ return tuple(self._materials)
96
+
97
+ @property
98
+ def result(self) -> Result:
99
+ if self._result is None:
100
+ raise RuntimeError("请先执行 execute() 或 execute_linear()")
101
+ return self._result
102
+
103
+ def _modify(self, action):
104
+ with self._lock:
105
+ if self._running:
106
+ raise RuntimeError("同一引擎正在计算,不能同时修改输入")
107
+ value = action()
108
+ self._result = None
109
+ self._result_options = None
110
+ self._locations_cache = None
111
+ return value
112
+
113
+ def clear(self, *, reset_options: bool = False) -> None:
114
+ """清空输入和结果;原生接口默认保留选项,文档兼容入口另行重置。"""
115
+ def reset():
116
+ self._parts.clear()
117
+ self._stocks.clear()
118
+ self._materials.clear()
119
+ if reset_options:
120
+ self.options = Options()
121
+ self._modify(reset)
122
+
123
+ def add_material(self, id: str = "", description: str = "", thickness: float = 0.0) -> int:
124
+ if not isinstance(id, str) or not isinstance(description, str):
125
+ raise ValidationError("材料编号和描述必须是字符串")
126
+ thickness = number(thickness, "thickness")
127
+ def append():
128
+ self._materials.append(Material(id, description, thickness))
129
+ return len(self._materials) - 1
130
+ return self._modify(append)
131
+
132
+ def get_material(self, material: int) -> Material:
133
+ return self._at(self._materials, material)
134
+
135
+ def add_part(self, width: float, height: float, count: int = 1,
136
+ rotatable: bool = True, id: str = "", material: int = -1) -> bool:
137
+ part = Part(number(width, "width", positive=True), number(height, "height", positive=True),
138
+ count, rotatable, id, material)
139
+ validate((), (part,), Options(max_decimal_point=6), linear=False)
140
+ self._modify(lambda: self._parts.append(part))
141
+ return True
142
+
143
+ def add_stock(self, width: float, height: float, count: int = 1,
144
+ id: str = "", waste: bool = False, material: int = -1) -> bool:
145
+ stock = Stock(number(width, "width", positive=True), number(height, "height", positive=True),
146
+ count, id, waste, material)
147
+ validate((stock,), (), Options(max_decimal_point=6), linear=False)
148
+ self._modify(lambda: self._stocks.append(stock))
149
+ return True
150
+
151
+ def add_linear_part(self, length: float, count: int = 1, *, id: str = "",
152
+ material: int = -1, angle_start: float = 0.0,
153
+ angle_end: float = 0.0) -> bool:
154
+ part = Part(number(length, "length", positive=True), 1.0, count, False, id, material,
155
+ number(angle_start, "angle_start"), number(angle_end, "angle_end"))
156
+ # 输入阶段允许稍后设置截面和更细的计算精度,最终执行时统一核验。
157
+ validate((), (part,), Options(cross_section=1, max_decimal_point=6), linear=True)
158
+ self._modify(lambda: self._parts.append(part))
159
+ return True
160
+
161
+ def add_linear_stock(self, length: float, count: int = 1, *, id: str = "",
162
+ waste: bool = False, material: int = -1) -> bool:
163
+ stock = Stock(number(length, "length", positive=True), 1.0, count, id, waste, material)
164
+ validate((stock,), (), Options(max_decimal_point=6), linear=True)
165
+ self._modify(lambda: self._stocks.append(stock))
166
+ return True
167
+
168
+ def set_vertical_cut_direction(self, roll_mode: bool = False) -> None:
169
+ self._set_direction("vertical", roll_mode)
170
+
171
+ def set_horizontal_cut_direction(self, roll_mode: bool = False) -> None:
172
+ self._set_direction("horizontal", roll_mode)
173
+
174
+ def set_auto_cut_direction(self) -> None:
175
+ self._set_direction("auto", False)
176
+
177
+ def _set_direction(self, direction: str, roll_mode: bool):
178
+ if type(roll_mode) is not bool:
179
+ raise ValidationError("roll_mode 必须是布尔值")
180
+ def change():
181
+ self.options.cut_direction = direction
182
+ self.options.roll_mode = roll_mode
183
+ self._modify(change)
184
+
185
+ def execute(self, callback: Callable[[Progress], bool] | None = None) -> Result:
186
+ return self._execute(False, callback)
187
+
188
+ def execute_linear(self, callback: Callable[[Progress], bool] | None = None) -> Result:
189
+ return self._execute(True, callback)
190
+
191
+ def execute_linear_fast(self, callback: Callable[[Progress], bool] | None = None) -> Result:
192
+ """新增快速入口;复杂工艺自动使用完整求解器,原 execute_linear 行为不变。"""
193
+ from ._linear_fast import solve_linear_fast
194
+ return self._execute(True, callback, solver=solve_linear_fast)
195
+
196
+ def optimize_linear_plans(self, *, max_plans: int = 3, time_limit: float = 5.0,
197
+ max_searches: int = 12,
198
+ callback: Callable[[Progress], bool] | None = None):
199
+ """在共享预算内返回多种一维方案,不覆盖本引擎已有计算结果。"""
200
+ return self._optimize_plans(True, max_plans, time_limit, max_searches, callback)
201
+
202
+ def optimize_plans(self, *, max_plans: int = 3, time_limit: float = 5.0,
203
+ max_searches: int = 12,
204
+ callback: Callable[[Progress], bool] | None = None):
205
+ """在共享预算内返回多种二维方案;物理工艺约束在各方案中保持不变。"""
206
+ return self._optimize_plans(False, max_plans, time_limit, max_searches, callback)
207
+
208
+ def _optimize_plans(self, linear, max_plans, time_limit, max_searches, callback):
209
+ from .portfolio import search_portfolio
210
+ with self._lock:
211
+ if self._running:
212
+ raise RuntimeError("同一引擎不能同时执行两个计算,请为每个请求新建实例")
213
+ options, stocks, parts = normalize_options(self.options), self.stocks, self.parts
214
+ self._running = True
215
+ try:
216
+ return search_portfolio(stocks, parts, options, linear=linear, max_plans=max_plans,
217
+ time_limit=time_limit, max_searches=max_searches, callback=callback)
218
+ finally:
219
+ with self._lock:
220
+ self._running = False
221
+
222
+ def _execute(self, linear: bool, callback, *, solver=None) -> Result:
223
+ with self._lock:
224
+ if self._running:
225
+ raise RuntimeError("同一引擎不能同时执行两个计算,请为每个请求新建实例")
226
+ # 快照可以因公开配置被赋错类型而失败;在置忙之前完成,保证任务仍可清空重试。
227
+ options = normalize_options(self.options)
228
+ stocks, parts = self.stocks, self.parts
229
+ self._running = True
230
+ self._result = None
231
+ self._locations_cache = None
232
+ try:
233
+ validate(stocks, parts, options, linear=linear)
234
+ if callback is not None and not callable(callback):
235
+ raise ValidationError("callback 必须可调用")
236
+ runtime = Runtime(options, sum(p.count for p in parts), callback)
237
+ if solver is not None:
238
+ result = solver(stocks, parts, options, runtime)
239
+ elif linear:
240
+ from .linear import solve_linear
241
+ result = solve_linear(stocks, parts, options, runtime)
242
+ else:
243
+ from .rectangular import solve_rectangular
244
+ result = solve_rectangular(stocks, parts, options, runtime)
245
+ self._result = group_layouts(result, options)
246
+ self._result_options = options
247
+ return self._result
248
+ finally:
249
+ with self._lock:
250
+ self._running = False
251
+
252
+ @staticmethod
253
+ def _at(items, index):
254
+ if type(index) is not int or not 0 <= index < len(items):
255
+ raise IndexError(f"索引超出范围:{index}")
256
+ return items[index]
257
+
258
+ def _part_locations(self) -> list[tuple[int, Placement] | None]:
259
+ if self._locations_cache is not None:
260
+ return self._locations_cache
261
+ locations = {(p.input_index, p.instance_index): (s, p)
262
+ for s, layout in enumerate(self.result.layouts) for p in layout.parts}
263
+ self._locations_cache = [locations.get((i, n)) for i, part in enumerate(self._parts) for n in range(part.count)]
264
+ return self._locations_cache
265
+
266
+ def get_result_part(self, part: int) -> tuple[int, Placement] | None:
267
+ """按输入展开序号查询;未放置返回 None,避免重排后丢失订单映射。"""
268
+ return self._at(self._part_locations(), part)
269
+
270
+ get_result_linear_part = get_result_part
271
+
272
+ def get_stock_info(self, stock: int) -> Layout:
273
+ return self._at(self.result.layouts, stock)
274
+
275
+ get_linear_stock_info = get_stock_info
276
+
277
+ def get_part_count_on_stock(self, stock: int) -> int:
278
+ return len(self.get_stock_info(stock).parts)
279
+
280
+ def get_part_index_on_stock(self, stock: int, part: int) -> int:
281
+ placement = self._at(self.get_stock_info(stock).parts, part)
282
+ return sum(p.count for p in self._parts[:placement.input_index]) + placement.instance_index
283
+
284
+ def get_part_material(self, part: int) -> int:
285
+ self._at(self._part_locations(), part)
286
+ for p in self._parts:
287
+ if part < p.count:
288
+ return p.material
289
+ part -= p.count
290
+ raise IndexError(part)
291
+
292
+ def get_stock_material(self, stock: int) -> int:
293
+ return self.get_stock_info(stock).material
294
+
295
+ def _remnants(self) -> list[tuple[int, Remnant]]:
296
+ return [(s, r) for s, layout in enumerate(self.result.layouts) for r in layout.all_remnants]
297
+
298
+ def get_remaining_part(self, part: int) -> tuple[int, Remnant]:
299
+ return self._at(self._remnants(), part)
300
+
301
+ get_remaining_linear_part = get_remaining_part
302
+
303
+ def get_remaining_part_material(self, part: int) -> int:
304
+ return self.get_remaining_part(part)[1].material
305
+
306
+ def get_remaining_part_count_on_stock(self, stock: int) -> int:
307
+ return len(self.get_stock_info(stock).all_remnants)
308
+
309
+ def get_remaining_part_index_on_stock(self, stock: int, part: int) -> int:
310
+ self._at(self.get_stock_info(stock).all_remnants, part)
311
+ return sum(len(s.all_remnants) for s in self.result.layouts[:stock]) + part
312
+
313
+ def _cuts(self) -> list[tuple[int, Cut]]:
314
+ return [(s, c) for s, layout in enumerate(self.result.layouts) for c in layout.cuts]
315
+
316
+ def get_cuts_count(self) -> int:
317
+ return sum(len(s.cuts) for s in self.result.layouts)
318
+
319
+ get_linear_cuts_count = get_cuts_count
320
+
321
+ def get_cut(self, cut: int) -> tuple[int, Cut]:
322
+ return self._at(self._cuts(), cut)
323
+
324
+ get_linear_cut = get_cut
325
+
326
+ def get_stock_cut_count(self, stock: int) -> int:
327
+ return len(self.get_stock_info(stock).cuts)
328
+
329
+ def get_stock_cut(self, stock: int, cut: int) -> Cut:
330
+ return self._at(self.get_stock_info(stock).cuts, cut)
331
+
332
+ get_linear_stock_cut = get_stock_cut
333
+
334
+ def get_stock_trim_cut_count(self, stock: int) -> int:
335
+ return len(self.get_stock_info(stock).trim_cuts)
336
+
337
+ def get_stock_trim_cut(self, stock: int, cut: int) -> Cut:
338
+ return self._at(self.get_stock_info(stock).trim_cuts, cut)
339
+
340
+ def _part_cuts(self, part: int) -> tuple[int, list[Cut]]:
341
+ located = self.get_result_part(part)
342
+ if located is None:
343
+ return -1, []
344
+ stock, placement = located
345
+ layout = self.result.layouts[stock]
346
+ index = layout.parts.index(placement)
347
+ cuts = [c for c in layout.cuts if index in c.part_indices]
348
+ return stock, cuts
349
+
350
+ def get_part_cuts_count(self, part: int) -> int:
351
+ return len(self._part_cuts(part)[1])
352
+
353
+ def get_part_cut(self, part: int, cut: int) -> tuple[int, Cut]:
354
+ stock, cuts = self._part_cuts(part)
355
+ return stock, self._at(cuts, cut)
356
+
357
+ def get_linear_combine_stock_count(self, stock: int) -> int:
358
+ components = self.get_stock_info(stock).components
359
+ return len(components) if len(components) > 1 else 0
360
+
361
+ def get_linear_combine_stock_components(self, stock: int) -> tuple[StockComponent, ...]:
362
+ layout = self.get_stock_info(stock)
363
+ return layout.components or (StockComponent(layout.input_index, layout.instance_index,
364
+ layout.width, layout.id, layout.waste, layout.material),)
365
+
366
+ def get_linear_combine_stock_info(self, stock: int, index: int) -> StockComponent:
367
+ return self._at(self.get_linear_combine_stock_components(stock), index)
368
+
369
+ def get_layout_info(self, layout: int) -> LayoutGroup:
370
+ return self._at(self.result.layout_groups, layout)
371
+
372
+ def get_layout_fill_ratio(self, layout: int) -> float:
373
+ group = self.get_layout_info(layout)
374
+ stock = self.result.layouts[group.first_layout_index]
375
+ if self._result_options is not None and self._result_options.cross_section > 0:
376
+ return (stock.width - sum(r.width for r in stock.remnants)) / stock.width
377
+ return stock.fill_ratio
378
+
379
+ @property
380
+ def part_count(self) -> int:
381
+ return sum(p.count for p in self._parts)
382
+
383
+ @property
384
+ def remaining_part_count(self) -> int:
385
+ return len(self._remnants())
386
+
387
+ @property
388
+ def stock_count(self) -> int:
389
+ return len(self.result.layouts)
390
+
391
+ @property
392
+ def used_stock_count(self) -> int:
393
+ return self.result.used_stock_count
394
+
395
+ linear_stock_count = stock_count
396
+ used_linear_stock_count = used_stock_count
397
+
398
+ @property
399
+ def layout_count(self) -> int:
400
+ return len(self.result.layout_groups)
401
+
402
+ @property
403
+ def elapsed_time(self) -> float:
404
+ return self.result.elapsed
405
+
406
+ @property
407
+ def placed_part_count(self) -> int:
408
+ return self.result.placed_part_count
409
+
410
+ @property
411
+ def is_linear_run(self) -> bool:
412
+ return self._result is not None and self._result.linear
413
+
414
+ @property
415
+ def max_part_sizes(self) -> None:
416
+ """零件规格类数量不受旧产品授权上限限制;None 表示无限制。"""
417
+ return None
418
+
419
+ @property
420
+ def max_size_sizes(self) -> None:
421
+ """原料规格类数量不受旧产品授权上限限制;None 表示无限制。"""
422
+ return None
423
+
424
+ def export_csv(self, target) -> None:
425
+ from .exporters import export_csv
426
+ export_csv(self.result, target)
427
+
428
+ def export_dxf(self, target, include_part_ids: bool = True) -> None:
429
+ from .exporters import export_dxf
430
+ export_dxf(self.result, target, include_part_ids=include_part_ids)
431
+
432
+ def export_cuts_dxf(self, target, include_cut_number: bool = True,
433
+ include_stock_panel: bool = True) -> None:
434
+ from .exporters import export_dxf
435
+ export_dxf(self.result, target, cuts_only=True, include_cut_number=include_cut_number,
436
+ include_stock_panel=include_stock_panel)
437
+
438
+ def export_ptx(self, target, job_name="", customer="", material="", material_thickness=0,
439
+ units=0, unit_scale=1, require_all_trim_cuts=False) -> None:
440
+ from .exporters import export_ptx
441
+ export_ptx(self.result, target, job_name=job_name, customer=customer, material=material,
442
+ material_thickness=material_thickness, units=units, unit_scale=unit_scale,
443
+ require_all_trim_cuts=require_all_trim_cuts,
444
+ options=self._result_options, materials=self.materials)
445
+
446
+ def create_stock_image(self, stock: int, target, max_size: int = 1000) -> None:
447
+ from .exporters import render_png, render_svg
448
+ layout = self.get_stock_info(stock)
449
+ if isinstance(target, (str, Path)):
450
+ if Path(target).suffix.lower() == ".svg":
451
+ Path(target).write_text(render_svg(layout, linear=self.result.linear, max_size=max_size), encoding="utf-8")
452
+ else:
453
+ Path(target).write_bytes(render_png(layout, linear=self.result.linear, max_size=max_size))
454
+ else:
455
+ target.write(render_png(layout, linear=self.result.linear, max_size=max_size))
456
+
457
+ create_linear_stock_image = create_stock_image
458
+
459
+ def to_dict(self) -> dict:
460
+ """面向 Web 的任务结构,与版本化存档格式分别提供。"""
461
+ return {"stocks": [asdict(s) for s in self.stocks],
462
+ "parts": [asdict(p) for p in self.parts], "options": asdict(normalize_options(self.options)),
463
+ "materials": [asdict(m) for m in self.materials]}
464
+
465
+ @classmethod
466
+ def from_dict(cls, payload: dict) -> CutEngine:
467
+ if not isinstance(payload, dict) or set(payload) - {"stocks", "parts", "options", "materials"}:
468
+ raise ValidationError("任务必须包含 stocks、parts、options、materials 中的字段")
469
+ if not isinstance(payload.get("options", {}), dict):
470
+ raise ValidationError("options 必须是对象")
471
+ for name in ("stocks", "parts", "materials"):
472
+ if not isinstance(payload.get(name, []), list) or any(not isinstance(item, dict) for item in payload.get(name, [])):
473
+ raise ValidationError(f"{name} 必须是对象数组")
474
+ try:
475
+ engine = cls(**payload.get("options", {}))
476
+ for m in payload.get("materials", ()):
477
+ engine.add_material(**m)
478
+ engine._stocks = [Stock(**s) for s in payload.get("stocks", ())]
479
+ engine._parts = [Part(**p) for p in payload.get("parts", ())]
480
+ engine._stocks = [replace(s, width=number(s.width, "stock.width", positive=True),
481
+ height=number(s.height, "stock.height", positive=True)) for s in engine._stocks]
482
+ engine._parts = [replace(p, width=number(p.width, "part.width", positive=True),
483
+ height=number(p.height, "part.height", positive=True),
484
+ angle_start=number(p.angle_start, "angle_start"),
485
+ angle_end=number(p.angle_end, "angle_end")) for p in engine._parts]
486
+ return engine
487
+ except (TypeError, KeyError) as exc:
488
+ raise ValidationError(f"任务结构无效:{exc}") from exc
489
+
490
+ def to_json(self) -> str:
491
+ from .serialization import job_to_json
492
+ return job_to_json(self.stocks, self.parts, self.options, self.materials)
493
+
494
+ @classmethod
495
+ def from_json(cls, text: str) -> CutEngine:
496
+ from .serialization import job_from_json
497
+ return cls._from_job(job_from_json(text))
498
+
499
+ def to_xml(self) -> str:
500
+ from .serialization import job_to_xml
501
+ return job_to_xml(self.stocks, self.parts, self.options, self.materials)
502
+
503
+ @classmethod
504
+ def from_xml(cls, text: str) -> CutEngine:
505
+ from .serialization import job_from_xml
506
+ return cls._from_job(job_from_xml(text))
507
+
508
+ @classmethod
509
+ def _from_job(cls, job) -> CutEngine:
510
+ stocks, parts, options, materials = job
511
+ engine = cls(options)
512
+ engine._stocks, engine._parts, engine._materials = list(stocks), list(parts), list(materials)
513
+ return engine
514
+
515
+ def AddPart(self, width, height, count=1, rotatable=True, id="", material=-1):
516
+ if type(count) is bool:
517
+ count, rotatable = 1, count
518
+ return self.add_part(width, height, count, rotatable, id, material)
519
+
520
+ def AddLinearPart(self, length, count=1, *args):
521
+ if args and isinstance(args[0], str):
522
+ if len(args) > 2:
523
+ raise TypeError("AddLinearPart 参数过多")
524
+ return self.add_linear_part(length, count, id=args[0], material=args[1] if len(args) > 1 else -1)
525
+ if args:
526
+ if not 2 <= len(args) <= 4:
527
+ raise TypeError("斜角重载需要起始角和结束角")
528
+ return self.add_linear_part(length, count, angle_start=args[0], angle_end=args[1],
529
+ id=args[2] if len(args) > 2 else "", material=args[3] if len(args) > 3 else -1)
530
+ return self.add_linear_part(length, count)
531
+
532
+ def AddLinearStock(self, length, count=1, *args):
533
+ if len(args) == 1 and type(args[0]) is int:
534
+ return self.add_linear_stock(length, count, material=args[0])
535
+ if len(args) > 3:
536
+ raise TypeError("AddLinearStock 参数过多")
537
+ return self.add_linear_stock(length, count, id=args[0] if args else "",
538
+ waste=args[1] if len(args) > 1 else False, material=args[2] if len(args) > 2 else -1)
539
+
540
+
541
+ # 旧接口名称集中声明,避免维护两套计算和查询逻辑。
542
+ _METHOD_ALIASES = {
543
+ "Clear": "clear", "AddStock": "add_stock", "AddMaterial": "add_material",
544
+ "GetMaterial": "get_material", "Execute": "execute", "ExecuteLinear": "execute_linear",
545
+ "SetVerticalCutDirection": "set_vertical_cut_direction", "SetHorizontalCutDirection": "set_horizontal_cut_direction",
546
+ "SetAutoCutDirection": "set_auto_cut_direction", "ExportToCSV": "export_csv", "ExportToDXF": "export_dxf",
547
+ "ExportCutsToDXF": "export_cuts_dxf", "ExportToPTX": "export_ptx",
548
+ "CreateStockImage": "create_stock_image", "CreateLinearStockImage": "create_linear_stock_image",
549
+ "ExportToCSV_Stream": "export_csv", "ExportToDXF_Stream": "export_dxf",
550
+ "ExportCutsToDXF_Stream": "export_cuts_dxf", "ExportToPTX_Stream": "export_ptx",
551
+ "CreateStockImage_Stream": "create_stock_image", "CreateLinearStockImage_Stream": "create_linear_stock_image",
552
+ }
553
+ for _name, _value in tuple(vars(CutEngine).items()):
554
+ if _name.startswith("get_") or isinstance(_value, property):
555
+ _METHOD_ALIASES.setdefault(_pascal(_name), _name)
556
+ for _alias, _original in _METHOD_ALIASES.items():
557
+ setattr(CutEngine, _alias, vars(CutEngine)[_original])
558
+ CutEngine.Version = CutEngine.version