1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6use std::sync::{Arc, Mutex, OnceLock};
7use std::ffi::c_void;
8use std::time::Duration;
9
10use core_mumu::parser::interpreter::{Interpreter, DynamicFn, DynamicFnInfo};
11use core_mumu::parser::types::{FunctionValue, Value};
12
13use minifb::{Window, WindowOptions, Scale, ScaleMode};
14
15#[derive(Clone)]
18enum Shape {
19 Pixel {
20 x: i32,
21 y: i32,
22 },
23 Rectangle {
24 x1: i32,
25 y1: i32,
26 x2: i32,
27 y2: i32,
28 },
29 Triangle {
30 x1: i32,
31 y1: i32,
32 x2: i32,
33 y2: i32,
34 x3: i32,
35 y3: i32,
36 },
37 Line {
39 x1: i32,
40 y1: i32,
41 x2: i32,
42 y2: i32,
43 },
44}
45
46#[derive(Clone)]
48pub struct ShapeItem {
49 handle_id: i32,
50 shape: Shape,
51 color: u32,
52}
53
54pub struct WindowHandle {
57 pub window: Option<Window>,
58 pub width: usize,
59 pub height: usize,
60 pub buffer: Vec<u32>,
61 pub items: Vec<ShapeItem>,
62}
63
64thread_local! {
65 static REGISTRY: RefCell<HashMap<i32, Rc<RefCell<WindowHandle>>>> =
66 RefCell::new(HashMap::new());
67}
68
69static POLLER_INSTALLED: OnceLock<bool> = OnceLock::new();
70
71fn poll_kanvas(interp: &mut Interpreter) -> usize {
76 std::thread::sleep(Duration::from_millis(15));
78
79 let mut remove_list = Vec::new();
80 REGISTRY.with(|r| {
81 let map = r.borrow();
82 for (&win_id, rc_wh) in map.iter() {
83 let mut wh = rc_wh.borrow_mut();
84 if let Some(win) = wh.window.as_mut() {
85 win.update();
86 if !win.is_open() {
87 remove_list.push(win_id);
88 }
89 }
90 }
91 });
92
93 if !remove_list.is_empty() {
94 REGISTRY.with(|r| {
95 let mut map = r.borrow_mut();
96 for rid in remove_list {
97 if interp.is_verbose() {
98 eprintln!("[kanvas poll_kanvas] removing window {}", rid);
99 }
100 map.remove(&rid);
101 }
102 });
103 }
104
105 let remaining = REGISTRY.with(|r| r.borrow().len());
107 if remaining > 0 { 1 } else { 0 }
108}
109
110fn install_kanvas_poller_if_needed(interp: &mut Interpreter) {
111 if POLLER_INSTALLED.get().is_none() {
112 if interp.is_verbose() {
113 eprintln!("[kanvas] Installing poller => poll_kanvas");
114 }
115 interp.add_poller(Arc::new(Mutex::new(move |intrp: &mut Interpreter| {
116 poll_kanvas(intrp)
117 })));
118 POLLER_INSTALLED.set(true).ok();
119 }
120}
121
122fn parse_color_hex(s: &str) -> Result<u32, String> {
128 let trimmed = s.trim();
129 if !trimmed.starts_with('#') {
130 return Err(format!("Color must start with '#', got '{}'", s));
131 }
132 let hexpart = &trimmed[1..];
133 let val = u32::from_str_radix(hexpart, 16)
134 .map_err(|e| format!("Invalid color '{}': {}", s, e))?;
135 match hexpart.len() {
137 6 => Ok(0xFF000000 | val),
138 8 => Ok(val),
139 _ => Err(format!("Hex color must have 6 or 8 digits, got '{}'", s)),
140 }
141}
142
143fn alpha_blend(dest: u32, src: u32) -> u32 {
145 let sa = ((src >> 24) & 0xFF) as f32;
146 let sr = ((src >> 16) & 0xFF) as f32;
147 let sg = ((src >> 8) & 0xFF) as f32;
148 let sb = (src & 0xFF) as f32;
149
150 let da = ((dest >> 24) & 0xFF) as f32;
151 let dr = ((dest >> 16) & 0xFF) as f32;
152 let dg = ((dest >> 8) & 0xFF) as f32;
153 let db = (dest & 0xFF) as f32;
154
155 let sf = sa / 255.0; let df = da / 255.0; let out_a = sf + df * (1.0 - sf);
159 if out_a < 1e-6 {
160 return 0;
162 }
163 let out_r = (sr * sf + dr * df * (1.0 - sf)) / out_a;
164 let out_g = (sg * sf + dg * df * (1.0 - sf)) / out_a;
165 let out_b = (sb * sf + db * df * (1.0 - sf)) / out_a;
166
167 let final_a = (out_a * 255.0).round() as u32;
168 let final_r = out_r.round() as u32;
169 let final_g = out_g.round() as u32;
170 let final_b = out_b.round() as u32;
171
172 (final_a << 24) | (final_r << 16) | (final_g << 8) | final_b
173}
174
175fn point_in_triangle(px: i32, py: i32, x1: i32, y1: i32, x2: i32, y2: i32, x3: i32, y3: i32) -> bool {
181 let d1 = (px - x2) * (y1 - y2) - (py - y2) * (x1 - x2);
182 let d2 = (px - x3) * (y2 - y3) - (py - y3) * (x2 - x3);
183 let d3 = (px - x1) * (y3 - y1) - (py - y1) * (x3 - x1);
184
185 let has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
186 let has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
187 !(has_neg && has_pos)
188}
189
190fn clear_buffer_to_black(buf: &mut [u32]) {
196 for px in buf {
197 *px = 0xFF000000; }
199}
200
201fn draw_line_in_buffer(
207 buf: &mut [u32],
208 w: usize,
209 h: usize,
210 x1: i32, y1: i32,
211 x2: i32, y2: i32,
212 color: u32
213) {
214 let dx = (x2 - x1).abs();
215 let sx = if x1 < x2 { 1 } else { -1 };
216 let dy = -(y2 - y1).abs();
217 let sy = if y1 < y2 { 1 } else { -1 };
218 let mut err = dx + dy;
219
220 let (mut x, mut y) = (x1, y1);
221 loop {
222 if x >= 0 && x < w as i32 && y >= 0 && y < h as i32 {
223 let idx = (y as usize) * w + (x as usize);
224 buf[idx] = alpha_blend(buf[idx], color);
225 }
226 if x == x2 && y == y2 {
227 break;
228 }
229 let e2 = 2 * err;
230 if e2 >= dy {
231 err += dy;
232 x += sx;
233 }
234 if e2 <= dx {
235 err += dx;
236 y += sy;
237 }
238 }
239}
240
241fn redraw_all(wh: &mut WindowHandle) {
247 clear_buffer_to_black(&mut wh.buffer);
248
249 for item in wh.items.iter() {
250 match item.shape {
251 Shape::Pixel { x, y } => {
252 if x >= 0 && y >= 0 && (x as usize) < wh.width && (y as usize) < wh.height {
253 let idx = (y as usize) * wh.width + (x as usize);
254 let dest = wh.buffer[idx];
255 wh.buffer[idx] = alpha_blend(dest, item.color);
256 }
257 }
258 Shape::Rectangle { x1, y1, x2, y2 } => {
259 let xs = x1.min(x2).max(0);
260 let xe = x1.max(x2).max(0);
261 let ys = y1.min(y2).max(0);
262 let ye = y1.max(y2).max(0);
263
264 for yy in ys..=ye {
265 if yy < 0 || (yy as usize) >= wh.height { continue; }
266 for xx in xs..=xe {
267 if xx < 0 || (xx as usize) >= wh.width { continue; }
268 let idx = (yy as usize) * wh.width + (xx as usize);
269 let dest = wh.buffer[idx];
270 wh.buffer[idx] = alpha_blend(dest, item.color);
271 }
272 }
273 }
274 Shape::Triangle { x1, y1, x2, y2, x3, y3 } => {
275 let xx_min = x1.min(x2).min(x3).max(0);
276 let xx_max = x1.max(x2).max(x3).max(0);
277 let yy_min = y1.min(y2).min(y3).max(0);
278 let yy_max = y1.max(y2).max(y3).max(0);
279
280 for yy in yy_min..=yy_max {
281 if yy < 0 || (yy as usize) >= wh.height { continue; }
282 for xx in xx_min..=xx_max {
283 if xx < 0 || (xx as usize) >= wh.width { continue; }
284 if point_in_triangle(xx, yy, x1, y1, x2, y2, x3, y3) {
285 let idx = (yy as usize) * wh.width + (xx as usize);
286 let dest = wh.buffer[idx];
287 wh.buffer[idx] = alpha_blend(dest, item.color);
288 }
289 }
290 }
291 }
292 Shape::Line { x1, y1, x2, y2 } => {
294 draw_line_in_buffer(
295 &mut wh.buffer,
296 wh.width,
297 wh.height,
298 x1, y1,
299 x2, y2,
300 item.color
301 );
302 }
303 }
304 }
305
306 if let Some(win) = wh.window.as_mut() {
307 let _ = win.update_with_buffer(&wh.buffer, wh.width, wh.height);
308 }
309}
310
311static mut GLOBAL_ITEM_ID: i32 = 1;
316
317fn kanvas_create_bridge(interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
319 if args.len() != 1 {
320 return Err("kanvas:create => expected 1 argument => KeyedArray".to_string());
321 }
322 let keyed = match &args[0] {
323 Value::KeyedArray(map) => map,
324 _ => return Err("kanvas:create => argument must be a KeyedArray".to_string()),
325 };
326
327 let mut w: i32 = 640;
328 let mut h: i32 = 480;
329 let mut title = "Kanvas Window".to_string();
330
331 if let Some(Value::Int(x)) = keyed.get("width") {
332 w = *x;
333 }
334 if let Some(Value::Int(y)) = keyed.get("height") {
335 h = *y;
336 }
337 if let Some(Value::SingleString(tt)) = keyed.get("title") {
338 title = tt.clone();
339 }
340
341 if w <= 0 || h <= 0 {
342 return Err(format!("Invalid width/height => {}x{}", w, h));
343 }
344
345 if interp.is_verbose() {
346 eprintln!("[kanvas:create] size={}x{}, title='{}'", w, h, title);
347 }
348
349 let w_usize = w as usize;
350 let h_usize = h as usize;
351
352 let mut window = Window::new(
353 &title,
354 w_usize,
355 h_usize,
356 WindowOptions {
357 scale: Scale::X1,
358 scale_mode: ScaleMode::Stretch,
359 ..WindowOptions::default()
360 }
361 ).map_err(|e| format!("Error creating Window => {}", e))?;
362
363 let buffer = vec![0xFF000000; w_usize * h_usize];
364 let _ = window.update_with_buffer(&buffer, w_usize, h_usize);
365
366 static mut NEXT_WIN_ID: i32 = 1000;
367 let window_id: i32;
368 unsafe {
369 window_id = NEXT_WIN_ID;
370 NEXT_WIN_ID += 1;
371 }
372
373 let wh = WindowHandle {
374 window: Some(window),
375 width: w_usize,
376 height: h_usize,
377 buffer,
378 items: Vec::new(),
379 };
380
381 REGISTRY.with(|r| {
382 r.borrow_mut().insert(window_id, Rc::new(RefCell::new(wh)));
383 });
384
385 Ok(Value::Int(window_id))
386}
387
388fn kanvas_display_bridge(interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
390 if args.len() != 1 {
391 return Err("kanvas:display => expected 1 argument => handle".to_string());
392 }
393
394 let handle_id = match &args[0] {
395 Value::Int(i) => *i,
396 _ => return Err("kanvas:display => first arg must be int handle".to_string()),
397 };
398
399 install_kanvas_poller_if_needed(interp);
400
401 for _ in 0..3 {
402 poll_kanvas(interp);
403 let still_there = REGISTRY.with(|r| r.borrow().contains_key(&handle_id));
404 if !still_there {
405 return Ok(Value::Bool(false));
406 }
407 std::thread::sleep(std::time::Duration::from_millis(16));
408 }
409
410 Ok(Value::Bool(true))
411}
412
413fn kanvas_remove_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
415 if args.len() != 2 {
416 return Err("kanvas:remove => expected 2 arguments => (windowHandle, itemHandle)".to_string());
417 }
418 let win_id = match &args[0] {
419 Value::Int(i) => *i,
420 _ => return Err("kanvas:remove => first arg must be window handle int".to_string()),
421 };
422 let item_id = match &args[1] {
423 Value::Int(i) => *i,
424 _ => return Err("kanvas:remove => second arg must be item handle int".to_string()),
425 };
426
427 let mut found = false;
428 REGISTRY.with(|r| {
429 if let Some(rc_wh) = r.borrow().get(&win_id) {
430 let mut wh = rc_wh.borrow_mut();
431 let old_len = wh.items.len();
432 wh.items.retain(|it| it.handle_id != item_id);
433 if wh.items.len() < old_len {
434 found = true;
435 }
436 }
437 });
438
439 Ok(Value::Bool(found))
440}
441
442fn kanvas_refresh_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
444 if args.len() != 1 {
445 return Err("kanvas:refresh => expected 1 argument => windowHandle".to_string());
446 }
447 let win_id = match &args[0] {
448 Value::Int(i) => *i,
449 _ => return Err("kanvas:refresh => first arg must be window handle int".to_string()),
450 };
451
452 REGISTRY.with(|r| {
453 if let Some(rc_wh) = r.borrow().get(&win_id) {
454 let mut wh = rc_wh.borrow_mut();
455 redraw_all(&mut wh);
456 }
457 });
458 Ok(Value::Bool(true))
459}
460
461fn kanvas_pixel_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
463 if args.len() != 3 {
464 return Err("kanvas:pixel => (windowHandle, [x,y], colorString)".to_string());
465 }
466 let win_id = match &args[0] {
467 Value::Int(i) => *i,
468 _ => return Err("kanvas:pixel => first arg must be window handle int".to_string()),
469 };
470 let (px, py) = match &args[1] {
471 Value::IntArray(v) if v.len() == 2 => (v[0], v[1]),
472 _ => return Err("kanvas:pixel => second arg must be [x,y]".to_string()),
473 };
474 let color_str = match &args[2] {
475 Value::SingleString(s) => s.clone(),
476 _ => return Err("kanvas:pixel => third arg must be color string".to_string()),
477 };
478 let color_val = parse_color_hex(&color_str)?;
479
480 let item_id = unsafe {
481 GLOBAL_ITEM_ID += 1;
482 GLOBAL_ITEM_ID
483 };
484
485 REGISTRY.with(|r| {
486 if let Some(rc_wh) = r.borrow().get(&win_id) {
487 let mut wh = rc_wh.borrow_mut();
488 let shape = Shape::Pixel { x: px, y: py };
489 let new_item = ShapeItem {
490 handle_id: item_id,
491 shape,
492 color: color_val,
493 };
494 wh.items.push(new_item);
495 }
496 });
497
498 Ok(Value::Int(item_id))
499}
500
501fn kanvas_rectangle_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
503 if args.len() != 3 {
504 return Err("kanvas:rectangle => (windowHandle, [[x1,y1],[x2,y2]], colorString)".to_string());
505 }
506 let win_id = match &args[0] {
507 Value::Int(i) => *i,
508 _ => return Err("kanvas:rectangle => first arg must be window handle int".to_string()),
509 };
510 let coords = match &args[1] {
511 Value::Int2DArray(rows) if rows.len() == 2 && rows[0].len() == 2 && rows[1].len() == 2 => {
512 (rows[0][0], rows[0][1], rows[1][0], rows[1][1])
513 }
514 _ => {
515 return Err("kanvas:rectangle => second arg must be [[x1,y1],[x2,y2]]".to_string());
516 }
517 };
518 let color_str = match &args[2] {
519 Value::SingleString(s) => s.clone(),
520 _ => return Err("kanvas:rectangle => third arg must be color string".to_string()),
521 };
522 let color_val = parse_color_hex(&color_str)?;
523
524 let (x1, y1, x2, y2) = coords;
525 let item_id = unsafe {
526 GLOBAL_ITEM_ID += 1;
527 GLOBAL_ITEM_ID
528 };
529
530 REGISTRY.with(|r| {
531 if let Some(rc_wh) = r.borrow().get(&win_id) {
532 let mut wh = rc_wh.borrow_mut();
533 let shape = Shape::Rectangle { x1, y1, x2, y2 };
534 let new_item = ShapeItem {
535 handle_id: item_id,
536 shape,
537 color: color_val,
538 };
539 wh.items.push(new_item);
540 }
541 });
542
543 Ok(Value::Int(item_id))
544}
545
546fn kanvas_triangle_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
552 if args.len() != 3 {
553 return Err("kanvas:triangle => (windowHandle, [[x1,y1],[x2,y2],[x3,y3]], colorString)".to_string());
554 }
555 let win_id = match &args[0] {
556 Value::Int(i) => *i,
557 _ => return Err("kanvas:triangle => first arg must be window handle int".to_string()),
558 };
559
560 let (x1, y1, x2, y2, x3, y3) = match &args[1] {
562 Value::Int2DArray(rows)
564 if rows.len() == 3 &&
565 rows[0].len() == 2 &&
566 rows[1].len() == 2 &&
567 rows[2].len() == 2 => {
568 (rows[0][0], rows[0][1], rows[1][0], rows[1][1], rows[2][0], rows[2][1])
569 }
570 Value::Float2DArray(rows)
572 if rows.len() == 3 &&
573 rows[0].len() == 2 &&
574 rows[1].len() == 2 &&
575 rows[2].len() == 2 => {
576 (
577 rows[0][0].round() as i32, rows[0][1].round() as i32,
578 rows[1][0].round() as i32, rows[1][1].round() as i32,
579 rows[2][0].round() as i32, rows[2][1].round() as i32,
580 )
581 }
582 _ => {
583 return Err("kanvas:triangle => second arg must be [[x1,y1],[x2,y2],[x3,y3]] (ints or floats)".to_string());
584 }
585 };
586
587 let color_str = match &args[2] {
588 Value::SingleString(s) => s.clone(),
589 _ => return Err("kanvas:triangle => third arg must be color string".to_string()),
590 };
591 let color_val = parse_color_hex(&color_str)?;
592
593 let item_id = unsafe {
594 GLOBAL_ITEM_ID += 1;
595 GLOBAL_ITEM_ID
596 };
597
598 REGISTRY.with(|r| {
599 if let Some(rc_wh) = r.borrow().get(&win_id) {
600 let mut wh = rc_wh.borrow_mut();
601 let shape = Shape::Triangle { x1, y1, x2, y2, x3, y3 };
602 let new_item = ShapeItem {
603 handle_id: item_id,
604 shape,
605 color: color_val,
606 };
607 wh.items.push(new_item);
608 }
609 });
610
611 Ok(Value::Int(item_id))
612}
613
614fn kanvas_line_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
616 if args.len() != 3 {
617 return Err("kanvas:line => (windowHandle, [[x1,y1],[x2,y2]], colorString)".to_string());
618 }
619 let win_id = match &args[0] {
620 Value::Int(i) => *i,
621 _ => return Err("kanvas:line => first arg must be window handle int".to_string()),
622 };
623 let coords = match &args[1] {
624 Value::Int2DArray(rows) if rows.len() == 2 => {
625 let row0 = &rows[0];
626 let row1 = &rows[1];
627 if row0.len() != 2 || row1.len() != 2 {
628 return Err("kanvas:line => second arg must be [[x1,y1],[x2,y2]]".to_string());
629 }
630 (row0[0], row0[1], row1[0], row1[1])
631 }
632 _ => {
633 return Err("kanvas:line => second arg must be [[x1,y1],[x2,y2]]".to_string());
634 }
635 };
636 let color_str = match &args[2] {
637 Value::SingleString(s) => s.clone(),
638 _ => return Err("kanvas:line => third arg must be color string".to_string()),
639 };
640 let color_val = parse_color_hex(&color_str)?;
641
642 let (x1, y1, x2, y2) = coords;
643 let item_id = unsafe {
644 GLOBAL_ITEM_ID += 1;
645 GLOBAL_ITEM_ID
646 };
647
648 REGISTRY.with(|r| {
649 if let Some(rc_wh) = r.borrow().get(&win_id) {
650 let mut wh = rc_wh.borrow_mut();
651 let shape = Shape::Line { x1, y1, x2, y2 };
652 let new_item = ShapeItem {
653 handle_id: item_id,
654 shape,
655 color: color_val,
656 };
657 wh.items.push(new_item);
658 }
659 });
660
661 Ok(Value::Int(item_id))
662}
663
664#[export_name = "Cargo_lock"]
669pub unsafe extern "C" fn cargo_lock(interp_ptr: *mut c_void, _extra: *const c_void) -> i32 {
670 if interp_ptr.is_null() {
671 return 1;
672 }
673 let interp = &mut *(interp_ptr as *mut Interpreter);
674
675 if interp.is_verbose() {
676 eprintln!("[kanvas Cargo.lock] => registering + installing poller if needed");
677 }
678
679 {
681 let f: DynamicFn = Arc::new(Mutex::new(kanvas_create_bridge));
682 let info = DynamicFnInfo::new(f, false);
683 interp.register_dynamic_function_ex("kanvas:create", info);
684 interp.set_variable(
685 "kanvas:create",
686 Value::Function(Box::new(FunctionValue::Named("kanvas:create".to_string())))
687 );
688 }
689 {
691 let f: DynamicFn = Arc::new(Mutex::new(kanvas_display_bridge));
692 let info = DynamicFnInfo::new(f, false);
693 interp.register_dynamic_function_ex("kanvas:display", info);
694 interp.set_variable(
695 "kanvas:display",
696 Value::Function(Box::new(FunctionValue::Named("kanvas:display".to_string())))
697 );
698 }
699 {
701 let f: DynamicFn = Arc::new(Mutex::new(kanvas_remove_bridge));
702 let info = DynamicFnInfo::new(f, false);
703 interp.register_dynamic_function_ex("kanvas:remove", info);
704 interp.set_variable(
705 "kanvas:remove",
706 Value::Function(Box::new(FunctionValue::Named("kanvas:remove".to_string())))
707 );
708 }
709 {
711 let f: DynamicFn = Arc::new(Mutex::new(kanvas_refresh_bridge));
712 let info = DynamicFnInfo::new(f, false);
713 interp.register_dynamic_function_ex("kanvas:refresh", info);
714 interp.set_variable(
715 "kanvas:refresh",
716 Value::Function(Box::new(FunctionValue::Named("kanvas:refresh".to_string())))
717 );
718 }
719 {
721 let f: DynamicFn = Arc::new(Mutex::new(kanvas_pixel_bridge));
722 let info = DynamicFnInfo::new(f, false);
723 interp.register_dynamic_function_ex("kanvas:pixel", info);
724 interp.set_variable(
725 "kanvas:pixel",
726 Value::Function(Box::new(FunctionValue::Named("kanvas:pixel".to_string())))
727 );
728 }
729 {
731 let f: DynamicFn = Arc::new(Mutex::new(kanvas_rectangle_bridge));
732 let info = DynamicFnInfo::new(f, false);
733 interp.register_dynamic_function_ex("kanvas:rectangle", info);
734 interp.set_variable(
735 "kanvas:rectangle",
736 Value::Function(Box::new(FunctionValue::Named("kanvas:rectangle".to_string())))
737 );
738 }
739 {
741 let f: DynamicFn = Arc::new(Mutex::new(kanvas_triangle_bridge));
742 let info = DynamicFnInfo::new(f, false);
743 interp.register_dynamic_function_ex("kanvas:triangle", info);
744 interp.set_variable(
745 "kanvas:triangle",
746 Value::Function(Box::new(FunctionValue::Named("kanvas:triangle".to_string())))
747 );
748 }
749 {
751 let f: DynamicFn = Arc::new(Mutex::new(kanvas_line_bridge));
752 let info = DynamicFnInfo::new(f, false);
753 interp.register_dynamic_function_ex("kanvas:line", info);
754 interp.set_variable(
755 "kanvas:line",
756 Value::Function(Box::new(FunctionValue::Named("kanvas:line".to_string())))
757 );
758 }
759
760 install_kanvas_poller_if_needed(interp);
761 0
762}