feat: screenshot API, camera control, pipe/wire crossing detection

New endpoints:
- POST /api/action/screenshot: capture screenshot to file
- GET /api/screenshot/latest: serve PNG binary
- POST /api/action/camera: move camera to (x,y) + set zoom
- GET /api/state/camera: get current camera position/zoom

Pipe/Wire crossing intelligence:
- build_pipe/build_wire now detect existing pipes/wires at each cell
- mode='line' (default): new pipe merges with existing
- mode='cross': auto-places bridges at intersections to avoid connection
- mode='single': one segment

CLI: snapshot [file.png], camera <x> <y> [zoom], camera_status
All high-level commands: auto pause, execute, resume
This commit is contained in:
root
2026-05-22 09:17:12 +08:00
parent 205ff7668a
commit f91ac2eb95
3 changed files with 402 additions and 44 deletions

View File

@ -702,24 +702,60 @@ def cmd_assign_job(args):
# ---------------------------------------------------------------------------
def cmd_build_pipe_line(args):
"""Build a pipe line. Usage: build_pipe_line <gas|liquid> <x1> <y1> <x2> <y2> [bridge]"""
"""Build a pipe line with crossing mode. Usage: build_pipe_line <gas|liquid> <x1> <y1> <x2> <y2> [mode]"""
if len(args) < 5:
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [bridge]")
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [mode]")
print(" mode: 'line' (default, auto-merge), 'cross' (use bridges at intersections), 'single' (one segment)")
return
payload = {"type": args[0], "x1": int(args[1]), "y1": int(args[2]),
"x2": int(args[3]), "y2": int(args[4]), "bridge": len(args) > 5 and args[5] == 'bridge'}
result = api_post('/api/action/build_pipe', payload)
_print_feedback(result)
ptype = args[0]
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
mode = args[5] if len(args) > 5 else 'line'
if mode not in ('line', 'cross', 'single'):
print("[!] mode must be 'line', 'cross', or 'single'")
return
result = api_post('/api/action/build_pipe', {
"type": ptype, "x1": x1, "y1": y1,
"x2": x2, "y2": y2, "mode": mode
})
if result.get('success'):
d = result.get('data', {})
segs = d.get('segmentCount', 0)
bridges = d.get('bridgesPlaced', 0)
xing = sum(1 for s in d.get('segments', []) if s.get('crossing'))
print(f"[OK] {ptype} pipe: {segs} segments, {bridges} bridges, {xing} crossings handled")
if xing > 0:
print(f" Mode '{mode}': {'bridges used at crossings' if mode == 'cross' else 'merged at crossings'}")
else:
_print_feedback(result)
def cmd_build_wire_line(args):
"""Build a wire line. Usage: build_wire_line <type> <x1> <y1> <x2> <y2> [bridge]"""
"""Build a wire line with crossing mode. Usage: build_wire_line <type> <x1> <y1> <x2> <y2> [mode]"""
if len(args) < 5:
print("Usage: build_wire_line regular|heavy|conductive <x1> <y1> <x2> <y2> [bridge]")
print("Usage: build_wire_line regular|heavy|conductive <x1> <y1> <x2> <y2> [mode]")
print(" mode: 'line' (default, auto-merge), 'cross' (use bridges at intersections), 'single' (one segment)")
return
payload = {"type": args[0], "x1": int(args[1]), "y1": int(args[2]),
"x2": int(args[3]), "y2": int(args[4]), "bridge": len(args) > 5 and args[5] == 'bridge'}
result = api_post('/api/action/build_wire', payload)
_print_feedback(result)
wtype = args[0]
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
mode = args[5] if len(args) > 5 else 'line'
if mode not in ('line', 'cross', 'single'):
print("[!] mode must be 'line', 'cross', or 'single'")
return
result = api_post('/api/action/build_wire', {
"type": wtype, "x1": x1, "y1": y1,
"x2": x2, "y2": y2, "mode": mode
})
if result.get('success'):
d = result.get('data', {})
segs = d.get('segmentCount', 0)
bridges = d.get('bridgesPlaced', 0)
xing = sum(1 for s in d.get('segments', []) if s.get('crossing'))
print(f"[OK] {wtype} wire: {segs} segments, {bridges} bridges, {xing} crossings handled")
else:
_print_feedback(result)
def _print_feedback(result):
"""Pretty-print action feedback."""
@ -734,6 +770,52 @@ def _print_feedback(result):
if sug:
print(f" -> {sug}")
# ---------------------------------------------------------------------------
# Screenshot / Camera
# ---------------------------------------------------------------------------
def cmd_snapshot(args):
"""Take a screenshot and save locally. Usage: snapshot [output.png]"""
output = args[0] if args else f"oni_snapshot_{int(__import__('time').time())}.png"
result = api_post('/api/action/screenshot', {})
if not result.get('success'):
print(f"[!] Screenshot failed: {result.get('errorMessage', '')}")
return
filename = result.get('data', {}).get('filename', '?')
print(f"[OK] Screenshot taken: {filename}")
# Download the image
try:
import urllib.request
from oni_api import api_url
url = api_url('/api/screenshot/latest')
urllib.request.urlretrieve(url, output)
print(f"[OK] Saved to {output} ({os.path.getsize(output)} bytes)")
except Exception as e:
print(f"[!] Download failed: {e}")
def cmd_camera(args):
"""Control camera. Usage: camera <x> <y> [zoom]"""
if len(args) < 2:
print("Usage: camera <x> <y> [zoom]")
print(" zoom: 5 (close) to 80 (far), default 30")
return
x, y = int(args[0]), int(args[1])
zoom = float(args[2]) if len(args) > 2 else None
payload = {"x": x, "y": y}
if zoom is not None:
payload["zoom"] = zoom
result = api_post('/api/action/camera', payload)
_print_feedback(result)
def cmd_camera_status(args):
"""Get current camera position. Usage: camera_status"""
data = api_get('/api/state/camera')
if 'error' in data:
print(f"Error: {data['error']}")
return
print(f"Camera at ({data.get('x', 0):.0f}, {data.get('y', 0):.0f}), zoom={data.get('zoom', 30):.0f}")
def cmd_explore(args):
"""AI-friendly exploration: reads a region and returns structured text summary."""
x = int(args[0]) if len(args) > 0 else 0
@ -849,6 +931,9 @@ COMMANDS = {
'assign_job': cmd_assign_job,
'build_pipe_line': cmd_build_pipe_line,
'build_wire_line': cmd_build_wire_line,
'snapshot': cmd_snapshot,
'camera': cmd_camera,
'camera_status': cmd_camera_status,
}
if __name__ == '__main__':
@ -920,6 +1005,11 @@ if __name__ == '__main__':
print(" assign_job <name> <job> Assign dupe to a chore group")
print(" morale Duplicant morale levels")
print("")
print("=== Screenshot / Camera ===")
print(" snapshot [file.png] Take screenshot + save locally")
print(" camera <x> <y> [zoom] Move camera view to coordinates")
print(" camera_status Get current camera position/zoom")
print("")
print("=== Batch / Priority (Advanced) ===")
print(" batch <json_file> Execute batch plan")
print(" priority_global <t> <p> Set global default priority")