///|
/// MoonBit 画布交互命令层(全目标共享;输入循环由各 target 的 main 提供)。
///
/// 命令:help / render / list / decl / svg / pick / drag / pan / zoom /
/// agent width|height / agent text <文本> / exit
struct ReplState {
camera : Camera
mut decl : String
mut highlight : String?
}
///|
fn repl_state_new(initial : String) -> ReplState {
{ camera: Camera::new(), decl: initial, highlight: None, }
}
///|
const CANVAS_COLS : Int = 64
///|
const CANVAS_ROWS : Int = 26
///|
fn repaint(st : ReplState) -> String {
match @decl.parse_decl(st.decl) {
Err(e) => "!! 声明解析错误:" + e.describe() + "\n"
Ok(page) => {
let buf = screen_new(CANVAS_COLS, CANVAS_ROWS)
draw_document(buf, st.camera, page.build(), st.highlight)
"MoonViz 画布 pan=(\{st.camera.pan_x}, \{st.camera.pan_y}) zoom=\{st.camera.zoom}\n\n" +
frame_of(buf) +
"\n帮助见 help\n"
}
}
}
///|
fn tokenize(line : String) -> Array[String] {
let out : Array[String] = Array::new()
let mut buf = StringBuilder::new()
for c in line.iter() {
if c == ' ' || c == '\t' {
if buf.to_string().length() > 0 {
out.push(buf.to_string())
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
let rest = buf.to_string()
if rest.length() > 0 {
out.push(rest)
}
out
}
///|
let help_text : String = "命令:\n render 重绘画布\n list / decl / svg 节点表 / 当前声明 / SVG\n" +
" pick 屏幕命中\n drag 拖拽命中节点(人类策略)\n" +
" pan 平移视口(世界像素)\n zoom in | zoom out | zoom reset\n" +
" agent width|height / agent text <文本>(Agent 硬门槛)\n exit\n"
///|
pub fn apply_command(st : ReplState, line : String) -> String {
let toks = tokenize(line)
if toks.length() == 0 {
return ""
}
match toks[0] {
"help" => help_text
"render" | "r" => repaint(st)
"list" =>
match @decl.parse_decl(st.decl) {
Err(e) => "!! " + e.describe()
Ok(page) => {
let doc = page.build()
let layout = @core.solve(doc)
let sb = StringBuilder::new()
for id in doc.nodes.keys() {
match layout.rect_of(id) {
Some(r) =>
sb.write_string(
"\{id} (\{r.x.to_int()}, \{r.y.to_int()}, \{r.width.to_int()}x\{r.height.to_int()})\n",
)
None => ()
}
}
sb.to_string()
}
}
"decl" => st.decl
"pick" | "p" => cmd_pick(st, toks)
"drag" | "d" => cmd_drag(st, toks)
"pan" =>
if toks.length() < 3 {
"!! 用法:pan (世界像素)"
} else {
camera_pan(
st.camera,
parse_num_or(toks[1], 0.0),
parse_num_or(toks[2], 0.0),
)
repaint(st)
}
"zoom" => cmd_zoom(st, toks)
"agent" => apply_agent_edit(st, toks)
"png" =>
match @decl.parse_decl(st.decl) {
Err(e) => "!! " + e.describe()
Ok(page) => {
let bytes = render_png_hires(page.build(), 195, 422)
"PNG(base64, 解码后可打开):\n" + base64_encode(bytes)
}
}
"svg" =>
match @decl.parse_decl(st.decl) {
Err(e) => "!! " + e.describe()
Ok(page) => page.render()
}
"exit" | "quit" | "q" => ":exit"
other => "!! 未知命令 \{other}(help 查看命令)"
}
}
///|
fn parse_num_or(t : String, fallback : Double) -> Double {
match parse_num(t) {
Some(v) => v
None => fallback
}
}
///|
fn cmd_pick(st : ReplState, toks : Array[String]) -> String {
if toks.length() < 3 {
return "!! 用法:pick "
}
match @decl.parse_decl(st.decl) {
Err(e) => "!! " + e.describe()
Ok(page) => {
let doc = page.build()
match
pick_at(
doc,
st.camera,
parse_num_or(toks[1], -1.0),
parse_num_or(toks[2], -1.0),
) {
Some(id) => "命中:" + id
None => "空白区域"
}
}
}
}
///|
fn cmd_drag(st : ReplState, toks : Array[String]) -> String {
if toks.length() < 3 {
return "!! 用法:drag "
}
match @decl.parse_decl(st.decl) {
Err(e) => "!! " + e.describe()
Ok(page) => {
let doc = page.build()
let col = parse_num_or(toks[1], -1.0)
let row = parse_num_or(toks[2], -1.0)
match pick_at(doc, st.camera, col, row) {
None => "!! 点中空白,无法拖拽"
Some(id) => {
st.highlight = Some(id)
let xy = to_world_pt(st.camera, col, row)
match page.move_node(id~, x=xy.0, y=xy.1) {
Err(msg) => "!! " + msg
Ok(_) => {
st.decl = page.to_decl_text()
repaint(st)
}
}
}
}
}
}
}
///|
/// 屏幕 → 世界(拖拽用)
fn to_world_pt(c : Camera, col : Double, row : Double) -> (Double, Double) {
(col / c.zoom + c.pan_x, row * 2.0 / c.zoom + c.pan_y)
}
///|
fn cmd_zoom(st : ReplState, toks : Array[String]) -> String {
if toks.length() < 2 {
return "!! zoom in | zoom out | zoom reset"
}
if toks[1] == "in" {
camera_zoom_in(st.camera)
} else if toks[1] == "out" {
camera_zoom_out(st.camera)
} else {
st.camera.pan_x = 0.0
st.camera.pan_y = 0.0
st.camera.zoom = 1.0
}
repaint(st)
}
///|
/// agent width|height / agent text <文本>
fn apply_agent_edit(st : ReplState, toks : Array[String]) -> String {
match @decl.parse_decl(st.decl) {
Err(e) => "!! " + e.describe()
Ok(page) => {
let doc = page.build()
let id = agent_target(toks)
match id {
None =>
"!! 用法:agent width|height / agent text <文本>"
Some(id) => {
let patch : @core.NodePatch? = agent_patch(toks)
match patch {
None =>
"!! 用法:agent width|height / agent text <文本>"
Some(p) => {
let result = @core.apply_agent_patch(doc, {
base_revision: doc.revision,
message: "agent 命令行编辑",
ops: [@core.UpdateNode(id, p)],
})
match result {
Err(err) =>
"Agent 提交被拒(硬门槛,未落盘):\n" +
err_summary(err)
Ok(_) => {
st.decl = @decl.from_document(doc).to_decl_text()
st.highlight = Some(id)
repaint(st)
}
}
}
}
}
}
}
}
}
///|
fn agent_target(toks : Array[String]) -> String? {
if toks[1] == "text" {
if toks.length() >= 4 {
Some(toks[2])
} else {
None
}
} else if toks.length() >= 2 {
Some(toks[1])
} else {
None
}
}
///|
fn join_from_idx(toks : Array[String], from : Int) -> String {
let sb = StringBuilder::new()
for i in from.. from {
sb.write_char(' ')
}
sb.write_string(toks[i])
}
sb.to_string()
}
///|
///agent 子命令的补丁:text / width / height
fn agent_patch(toks : Array[String]) -> @core.NodePatch? {
if toks[1] == "text" {
Some({ ..@core.NodePatch::empty(), text: Some(join_from_idx(toks, 3)), })
} else if toks[2] == "width" {
Some({
..@core.NodePatch::empty(),
width: Some(fixed_spec(parse_num(toks[3]))),
})
} else if toks[2] == "height" {
Some({
..@core.NodePatch::empty(),
height: Some(fixed_spec(parse_num(toks[3]))),
})
} else {
None
}
}
///|
fn fixed_spec(v : Double?) -> @core.SizeSpec {
match v {
Some(d) => @core.Fixed(d)
None => @core.Fixed(0.0)
}
}
///|
/// 补丁错误摘要(一行)
fn err_summary(err : @core.PatchError) -> String {
match err {
@core.StaleBase(cur) =>
"patch 过时(文档当前 \{cur}),请重新读取后再提交"
@core.HardBlock(vs) =>
"hard_block:阻断违规 \{vs.length()} 条:" + viol_brief(vs)
@core.StructuralBlock(vs) => "structural_block:违规 \{vs.length()} 条"
@core.UnknownNode(id) => "unknown_node " + id
@core.UnknownParent(id) => "unknown_parent " + id
@core.DuplicateId(id) => "duplicate_id " + id
@core.HierarchyError(msg) => msg
}
}
///|
fn viol_brief(vs : Array[@core.Violation]) -> String {
vs[0].to_json()
}