forked from 1b-agent/opencadstudio-upstream
feat(python): chamfer, arrays, explode, join, break, stretch and lengthen wrappers
Each is written against the prompt sequence observed in the exploration harness and checked for exact geometry in the real-IPC audit. HATCH is left to create_entity because its command needs an interactive boundary pick. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
1b1785eefb
commit
360dd853ed
5 files changed
+184
-46
No files matched your search
@@ -281,6 +281,13 @@ M.fillet(first, second, radius=2) # radius 0 squares the corner
|
||||
M.move([a, b], (0, 0, 0), (5, 5, 0)); M.copy([a], base, target)
|
||||
M.rotate([a], base, 90); M.scale([a], base, 2)
|
||||
M.mirror([a], p1, p2, erase_source=False); M.erase([a])
|
||||
M.chamfer(first, second, 2, 3) # distance 2 along first, 3 along second
|
||||
M.array_rect([a], rows=2, columns=3, row_spacing=10, column_spacing=20)
|
||||
M.array_polar([a], center, count=4, angle=360) # counts include the original
|
||||
M.explode([polyline]); M.join([line1, line2])
|
||||
M.break_entity(line, first_point, second_point)
|
||||
M.stretch(corner1, corner2, base, target) # crossing window, then the displacement
|
||||
M.lengthen(line, 5, at=near_the_end) # delta; negative shortens
|
||||
```
|
||||
|
||||
Each step returns an outcome dict: `status` (`completed` or `waiting_input`),
|
||||
@@ -292,7 +299,8 @@ Each step returns an outcome dict: `status` (`completed` or `waiting_input`),
|
||||
also closing any editor or dialog it opened. `doc.command` cancels and raises when
|
||||
the line leaves a prompt open.
|
||||
|
||||
Selection-based tools (`move`, `copy`, `rotate`, `scale`, `mirror`, `erase`) select
|
||||
Selection-based tools (`move`, `copy`, `rotate`, `scale`, `mirror`, `erase`, the arrays,
|
||||
`explode` and `join`) select
|
||||
the entities you pass first, so the command skips its own selection prompt. Pick
|
||||
points default to the middle of a line, arc or circle; give `at=` for anything else.
|
||||
`trim` and `extend` cut or extend against every entity in the drawing. Each command is
|
||||
@@ -304,7 +312,9 @@ line names a command that could end the session or re-enter Python: `QUIT`,
|
||||
`RECOVER`, `SCRIPT`, `RUNSCRIPT` and any `PY_*`. Commands run on the active
|
||||
document only. Commands that need an interactive surface a script cannot fill
|
||||
(the rich-text editor, a file dialog) report it in `blocked_by` and are cancelled.
|
||||
`ocs.command_step(kind, options)` is the raw single step behind all of this.
|
||||
Hatching has no wrapper: create it with `create_entity("Hatch", ...)`, because the
|
||||
HATCH command needs an interactive boundary pick. `ocs.command_step(kind, options)`
|
||||
is the raw single step behind all of this.
|
||||
|
||||
### Historical command-replay experiment (not in the default build)
|
||||
|
||||
|
||||
@@ -751,6 +751,76 @@ class _Modify:
|
||||
lambda c: c.token("Y" if erase_source else "N"),
|
||||
], entities)
|
||||
|
||||
def chamfer(self, first, second, distance1, distance2=None, at1=None, at2=None):
|
||||
"""Cut the corner between two lines; `distance1` is measured along `first`
|
||||
and `distance2` (default the same) along `second`. Returns the new line."""
|
||||
if distance2 is None:
|
||||
distance2 = distance1
|
||||
result = self._run("CHAMFER", [
|
||||
lambda c: c.token("D"),
|
||||
lambda c: c.text(distance1),
|
||||
lambda c: c.text(distance2),
|
||||
lambda c: c.entity(first, self._at(first, at1)),
|
||||
lambda c: c.entity(second, self._at(second, at2)),
|
||||
])
|
||||
if not result:
|
||||
raise RuntimeError("CHAMFER produced nothing")
|
||||
return result[0]
|
||||
|
||||
def array_rect(self, entities, rows, columns, row_spacing, column_spacing):
|
||||
"""Rectangular array of the entities: `rows` by `columns` copies counting
|
||||
the original, spaced by `row_spacing` and `column_spacing`. Returns the copies."""
|
||||
return self._run("ARRAYRECT", [
|
||||
lambda c: c.text(rows),
|
||||
lambda c: c.text(columns),
|
||||
lambda c: c.text(row_spacing),
|
||||
lambda c: c.text(column_spacing),
|
||||
], entities)
|
||||
|
||||
def array_polar(self, entities, center, count, angle=360):
|
||||
"""Polar array about `center`: `count` items counting the original, spread
|
||||
over `angle` degrees. Returns the copies."""
|
||||
return self._run("ARRAYPOLAR", [
|
||||
lambda c: c.point(center),
|
||||
lambda c: c.text(count),
|
||||
lambda c: c.text(angle),
|
||||
], entities)
|
||||
|
||||
def explode(self, entities):
|
||||
"""Break polylines, blocks and similar entities into their parts; returns the parts."""
|
||||
return self._run("EXPLODE", [], entities)
|
||||
|
||||
def join(self, entities):
|
||||
"""Join end-to-end lines, arcs and polylines into one entity; returns it."""
|
||||
return self._run("JOIN", [], entities)
|
||||
|
||||
def break_entity(self, entity, first, second):
|
||||
"""Remove the part of `entity` between two points on it; returns the new piece."""
|
||||
return self._run("BREAK", [
|
||||
lambda c: c.entity(entity, first),
|
||||
lambda c: c.point(second),
|
||||
])
|
||||
|
||||
def stretch(self, corner1, corner2, base, target):
|
||||
"""Stretch whatever the crossing window (two opposite corners) catches, moving
|
||||
`base` to `target`; entities fully inside move, ones crossing the edge stretch."""
|
||||
self._run("STRETCH", [
|
||||
lambda c: c.point(corner1),
|
||||
lambda c: c.point(corner2),
|
||||
lambda c: c.enter(),
|
||||
lambda c: c.point(base),
|
||||
lambda c: c.point(target),
|
||||
])
|
||||
|
||||
def lengthen(self, entity, delta, at=None):
|
||||
"""Lengthen (or, negative, shorten) `entity` by `delta` at the end nearest `at`."""
|
||||
self._run("LENGTHEN", [
|
||||
lambda c: c.token("DE"),
|
||||
lambda c: c.text(delta),
|
||||
lambda c: c.entity(entity, self._at(entity, at)),
|
||||
lambda c: c.enter(),
|
||||
])
|
||||
|
||||
def erase(self, entities):
|
||||
before = len(list(ocs.entity_handles()))
|
||||
self._select(entities)
|
||||
|
||||
@@ -58,7 +58,7 @@ class DocumentModelTests(unittest.TestCase):
|
||||
outcome = {"status": "completed", "blocked_by": None, "command": "", "prompt": "", "accepts": [],
|
||||
"options": [], "entities": 0, "added": 0, "unconsumed": [], "error": None}
|
||||
outcome.update(self.step_outcome.get(kind, {}))
|
||||
if kind == "enter":
|
||||
if kind in ("enter", "entity"):
|
||||
new = max(self.records) + 1
|
||||
self.records[new] = {"handle": new, "kind": "Line", "layer": "0", "_editable": True}
|
||||
return outcome
|
||||
@@ -98,6 +98,22 @@ class DocumentModelTests(unittest.TestCase):
|
||||
entity_step = [c for c in self.calls if c[0] == "step" and c[1] == "entity"][0]
|
||||
self.assertEqual(entity_step[2]["point"], [1, 0, 5.0])
|
||||
|
||||
def test_more_modify_wrappers(self):
|
||||
self.step_outcome = {}
|
||||
self.doc.modify.chamfer(1, 2, 2, 3, at1=(0, 0, 0), at2=(1, 1, 0))
|
||||
texts = [c[2]["text"] for c in self.calls if c[0] == "step" and c[1] == "text"]
|
||||
self.assertEqual(texts, ["2", "3"])
|
||||
self.calls.clear()
|
||||
self.doc.modify.chamfer(1, 2, 4, at1=(0, 0, 0), at2=(1, 1, 0))
|
||||
texts = [c[2]["text"] for c in self.calls if c[0] == "step" and c[1] == "text"]
|
||||
self.assertEqual(texts, ["4", "4"])
|
||||
self.calls.clear()
|
||||
self.doc.modify.array_rect([1], 2, 3, 10, 20)
|
||||
self.assertEqual([c[2]["text"] for c in self.calls if c[0] == "step" and c[1] == "text"], ["2", "3", "10", "20"])
|
||||
self.calls.clear()
|
||||
self.doc.modify.stretch((0, 0), (1, 1), (0, 0), (5, 0))
|
||||
self.assertEqual([c[1] for c in self.calls if c[0] == "step"], ["start", "point", "point", "enter", "point", "point"])
|
||||
|
||||
def test_batch_edit_and_rollback(self):
|
||||
line = self.doc.entities[1]
|
||||
self.assertEqual(line.start, (1, 2, 3))
|
||||
|
||||
Reference in new issue
Block a user