# -*- coding: utf-8 -*- """拖拽编程画布——图编译为 script_engine 可执行脚本(Python,script_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]}