scense_drag/drag/compiler.py
2026-08-29 21:10:23 +08:00

276 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""拖拽编程画布——图编译为 script_engine 可执行脚本Pythonscript_type=0
编译策略:
- 变量声明统一提升到脚本头部var_create保证使用前已定义
- 函数定义logic_function_def生成 def + 函数体链
- 每个事件块作为独立入口分支
- 逻辑块生成控制流for / while / if / else / 并行顺序展开)
- 动作块生成动作函数调用move/rotate/scale/set_property/play_animation/
play_sound/show_hide/camera/wait由宿主运行时注册到 script_engine 白名单
- 产物仅含白名单兼容语法:赋值 / def / for / while / if-else / 函数调用 / 注释,
无 import、无 class、无 lambda、无对象方法调用、无 async
- 表达式内 `@变量名` 引用统一替换为 Python 标识符(变量提升到头部后按名引用)
"""
import re
from .blocks import BLOCK_DEFS, get_dynamic_outputs
from .validator import validate_graph
_IDENT_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
_SAFE_EXPR = re.compile(r"^[A-Za-z0-9_+\-*/%()<>=!&| .'\"]+$")
_VAR_REF = re.compile(r'@([A-Za-z_][A-Za-z0-9_]*)')
def _ident(name):
s = re.sub(r'\W', '_', str(name or ''))
if not s:
return '_v'
if s[0].isdigit():
s = '_' + s
return s
def _num(s):
s = str(s or '').strip()
try:
float(s)
return s
except (TypeError, ValueError):
return '0'
def _str(s):
return "'%s'" % str(s or '').replace("'", "\\'")
def _expr(s):
"""表达式安全转义:表达式内 @var 引用 → 标识符;纯数字/安全表达式原样;其余当字符串。"""
s = str(s or '').strip()
s = _VAR_REF.sub(lambda m: _ident(m.group(1)), s)
try:
float(s)
return s
except (TypeError, ValueError):
pass
if (_SAFE_EXPR.match(s) and 'import' not in s and '__' not in s and ';' not in s
and not s.startswith('lambda') and not s.startswith('class')):
return s
return _str(s)
def _val(s):
"""值转义:@var → 标识符;数字 → 原样true/false → 布尔;其余 → 字符串字面量。"""
s = str(s or '').strip()
if s.startswith('@') and _IDENT_RE.match(s[1:]):
return _ident(s[1:])
try:
f = float(s)
return str(int(f)) if f == int(f) else s
except (TypeError, ValueError):
pass
if s in ('true', 'True', '1'):
return 'True'
if s in ('false', 'False', '0'):
return 'False'
return _str(s)
def _block_label(btype):
bdef = BLOCK_DEFS.get(btype) or {}
return bdef.get("label", btype)
def _block_code(b):
"""生成单个块的执行代码行;返回 None 表示该块不生成独立代码(结构块/已提升)。"""
btype = b.get("type")
p = b.get("params") or {}
if btype == "event_start":
return "# 事件: 开始"
if btype == "event_click":
return "# 事件: 点击 %s (%s)" % (p.get("target", ''), p.get("button", 'left'))
if btype == "event_hover":
return "# 事件: 悬停 %s %s" % (p.get("target", ''), p.get("state", 'enter'))
if btype == "event_timer":
return "# 事件: 定时 每 %s× %s" % (p.get("interval", ''), p.get("repeat", '-1'))
if btype == "event_collision":
return "# 事件: 碰撞 %s × %s" % (p.get("entity_a", ''), p.get("entity_b", ''))
if btype == "event_keyboard":
return "# 事件: 键盘 %s (%s)" % (p.get("key", ''), p.get("action", 'keydown'))
if btype == "logic_sequence":
return "# 顺序执行"
if btype == "logic_wait":
return "wait(seconds=%s)" % _num(p.get("seconds", '0'))
if btype == "logic_function_call":
return "%s()" % _ident(p.get("name", 'fn'))
if btype == "var_create":
return None # 已提升到头部变量声明区
if btype == "var_assign":
return "%s = %s" % (_ident(p.get("name", '_v')), _val(p.get("value", '0')))
if btype == "var_read":
return "%s = %s" % (_ident(p.get("result", '_r')), _ident(p.get("name", '_v')))
if btype == "expr_arith":
return "%s = %s %s %s" % (_ident(p.get("result", '_r')), _expr(p.get("operand_a", '0')),
p.get("operator", '+'), _expr(p.get("operand_b", '0')))
if btype == "expr_compare":
return "%s = %s %s %s" % (_ident(p.get("result", '_r')), _expr(p.get("operand_a", '0')),
p.get("operator", '=='), _expr(p.get("operand_b", '0')))
if btype == "expr_logic":
op = p.get("operator", 'and')
a, bv = _expr(p.get("operand_a", 'True')), _expr(p.get("operand_b", 'True'))
body = "not %s" % a if op == 'not' else "%s %s %s" % (a, op, bv)
return "%s = %s" % (_ident(p.get("result", '_r')), body)
# 动作
if btype == "action_move":
return "move(entity=%s, x=%s, y=%s, duration=%s)" % (
_str(p.get("entity", '')), _num(p.get("x", '0')), _num(p.get("y", '0')), _num(p.get("duration", '0')))
if btype == "action_rotate":
return "rotate(entity=%s, angle=%s, duration=%s)" % (
_str(p.get("entity", '')), _num(p.get("angle", '0')), _num(p.get("duration", '0')))
if btype == "action_scale":
return "scale(entity=%s, scale_x=%s, scale_y=%s, duration=%s)" % (
_str(p.get("entity", '')), _num(p.get("scale_x", '1')), _num(p.get("scale_y", '1')),
_num(p.get("duration", '0')))
if btype == "action_set_property":
return "set_property(entity=%s, property=%s, value=%s)" % (
_str(p.get("entity", '')), _str(p.get("property", '')), _str(p.get("value", '')))
if btype == "action_play_animation":
return "play_animation(entity=%s, animation=%s)" % (
_str(p.get("entity", '')), _str(p.get("animation", '')))
if btype == "action_play_sound":
return "play_sound(entity=%s, sound=%s)" % (_str(p.get("entity", '')), _str(p.get("sound", '')))
if btype == "action_show_hide":
return "show_hide(entity=%s, visible=%s)" % (_str(p.get("entity", '')), _val(p.get("visible", 'true')))
if btype == "action_camera":
return "camera(camera_id=%s, mode=%s, x=%s, y=%s, zoom=%s)" % (
_str(p.get("camera_id", 'main')), _str(p.get("mode", 'follow')),
_num(p.get("x", '0')), _num(p.get("y", '0')), _num(p.get("zoom", '1')))
return None
def _emit_chain(bid, bmap, adj, indent, visited=None):
"""沿输出端口生成执行代码(每块仅访问一次防环)。返回代码行列表。"""
if visited is None:
visited = set()
if bid in visited or bid not in bmap:
return []
visited.add(bid)
b = bmap[bid]
btype = b.get("type")
p = b.get("params") or {}
ind = ' ' * indent
lines = []
outs = adj.get(bid, [])
if btype == "logic_condition":
lines.append(ind + "if %s:" % _expr(p.get("condition", 'True')))
t = [c for c in outs if c.get("fromPort") == 'true']
if t:
lines.extend(_emit_chain(t[0].get("to"), bmap, adj, indent + 1, visited))
else:
lines.append(ind + " pass")
lines.append(ind + "else:")
f = [c for c in outs if c.get("fromPort") == 'false']
if f:
lines.extend(_emit_chain(f[0].get("to"), bmap, adj, indent + 1, visited))
else:
lines.append(ind + " pass")
return lines
if btype == "logic_loop":
mode = p.get("mode", 'count')
if mode == 'while':
lines.append(ind + "while %s:" % _expr(p.get("condition", 'True')))
else:
lines.append(ind + "for _i in range(%s):" % _num(p.get("times", '3')))
nxt = [c for c in outs if c.get("fromPort") == 'out']
if nxt:
lines.extend(_emit_chain(nxt[0].get("to"), bmap, adj, indent + 1, visited))
else:
lines.append(ind + " pass")
return lines
if btype in ("logic_branch", "logic_parallel"):
prefix = 'branch' if btype == "logic_branch" else 'p'
names = get_dynamic_outputs(btype, p) or []
for i, port in enumerate(names, 1):
lines.append(ind + "# %s分支%d" % ("并行" if btype == "logic_parallel" else "", i))
br = [c for c in outs if c.get("fromPort") == port]
if br:
lines.extend(_emit_chain(br[0].get("to"), bmap, adj, indent, visited))
else:
lines.append(ind + "pass")
return lines
# 普通块
code = _block_code(b)
if code is not None:
lines.append(ind + code)
# 单输出继续
nxt = [c for c in outs if c.get("fromPort") == 'out']
if nxt:
lines.extend(_emit_chain(nxt[0].get("to"), bmap, adj, indent, visited))
return lines
def compile_graph(graph, meta=None):
"""编译画布 → {success, content, warnings, entry_points} 或 {success:False, errors:[...]}"""
v = validate_graph(graph)
if v["errors"]:
return {"success": False, "valid": False, "errors": v["errors"]}
blocks = graph.get("blocks") or []
conns = graph.get("connections") or []
bmap = {b.get("id"): b for b in blocks if b.get("id")}
adj = {}
for c in conns:
adj.setdefault(c.get("from"), []).append(c)
events = [b for b in blocks if (BLOCK_DEFS.get(b.get("type")) or {}).get("category") == "event"]
lines = []
lines.append("# -*- coding: utf-8 -*-")
lines.append("# 拖拽编程画布编译产物 (drag compiler v1)")
if meta:
lines.append("# 画布: %s (id=%s)" % (meta.get("name", ''), meta.get("id", '')))
lines.append("# 入口事件: %s" % ", ".join(
"%s[%s]" % (e.get("id"), _block_label(e.get("type"))) for e in events))
lines.append("")
# 变量声明区(提升)
var_decls = []
for b in blocks:
if b.get("type") == "var_create":
p = b.get("params") or {}
var_decls.append("%s = %s" % (_ident(p.get("name", '_v')), _val(p.get("initial_value", '0'))))
if var_decls:
lines.append("# ===== 变量声明 =====")
lines.extend(var_decls)
lines.append("")
# 函数定义区
funcs = [b for b in blocks if b.get("type") == "logic_function_def"]
if funcs:
lines.append("# ===== 函数定义 =====")
for fb in funcs:
p = fb.get("params") or {}
lines.append("def %s():" % _ident(p.get("name", 'fn')))
body = _emit_chain(fb.get("id"), bmap, adj, 1, set())
lines.extend(body or [" pass"])
lines.append("")
# 入口执行区(每个事件块一个分支)
if not events:
lines.append("# (无入口事件)")
for ev in events:
lines.append("# ===== 入口: %s [%s] =====" % (ev.get("id"), _block_label(ev.get("type"))))
body = _emit_chain(ev.get("id"), bmap, adj, 0, set())
lines.extend(body or ["pass"])
lines.append("")
content = "\n".join(lines).rstrip() + "\n"
return {"success": True, "valid": True, "content": content,
"warnings": v["warnings"],
"entry_points": [e.get("id") for e in events]}