1use crate::console::{Console, PixelsXY};
20use endbasic_core::{
21 ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
22 CallableMetadataBuilder, ExprType, LineCol, RepeatedSyntax, RepeatedTypeSyntax,
23 RequiredRefSyntax, RequiredValueSyntax, Scope, SingularArgSyntax,
24};
25use std::borrow::Cow;
26use std::cell::RefCell;
27use std::convert::TryFrom;
28use std::rc::Rc;
29
30use crate::MachineBuilder;
31
32pub mod lcd;
33
34const CATEGORY: &str = "Graphics
36The EndBASIC console overlays text and graphics in the same canvas. The consequence of this \
37design choice is that the console has two coordinate systems: the character-based system, used by
38the commands described in HELP \"CONSOLE\", and the pixel-based system, used by the commands \
39described in this section.";
40
41fn parse_coordinate(scope: &Scope<'_>, narg: u8) -> CallResult<i16> {
43 parse_coordinate_value(scope.get_pos(narg), scope.get_integer(narg))
44}
45
46fn parse_coordinate_value(pos: LineCol, i: i32) -> CallResult<i16> {
48 match i16::try_from(i) {
49 Ok(i) => Ok(i),
50 Err(_) => Err(CallError::Syntax(pos, format!("Coordinate {} out of range", i))),
51 }
52}
53
54fn parse_coordinates(scope: &Scope<'_>, xarg: u8, yarg: u8) -> CallResult<PixelsXY> {
56 Ok(PixelsXY { x: parse_coordinate(scope, xarg)?, y: parse_coordinate(scope, yarg)? })
57}
58
59fn parse_triangle_coordinates(
61 scope: &Scope<'_>,
62 x1arg: u8,
63 y1arg: u8,
64 x2arg: u8,
65 y2arg: u8,
66 x3arg: u8,
67 y3arg: u8,
68) -> CallResult<(PixelsXY, PixelsXY, PixelsXY)> {
69 Ok((
70 parse_coordinates(scope, x1arg, y1arg)?,
71 parse_coordinates(scope, x2arg, y2arg)?,
72 parse_coordinates(scope, x3arg, y3arg)?,
73 ))
74}
75
76fn parse_polygon_coordinates(scope: &Scope<'_>) -> CallResult<Vec<PixelsXY>> {
78 match scope.nargs() {
79 0 => return Ok(vec![]),
80 1 => return parse_polygon_coordinates_from_array(scope),
81 _ => (),
82 }
83
84 if !scope.nargs().is_multiple_of(2) {
85 let narg = u8::try_from(scope.nargs() - 1).expect("Argument index must fit in u8");
86 return Err(CallError::Syntax(
87 scope.get_pos(narg),
88 "Expected an even number of coordinates".to_owned(),
89 ));
90 }
91
92 let mut points = Vec::with_capacity(scope.nargs() / 2);
93 for i in (0..scope.nargs()).step_by(2) {
94 let xarg = u8::try_from(i).expect("Argument index must fit in u8");
95 let yarg = u8::try_from(i + 1).expect("Argument index must fit in u8");
96 points.push(parse_coordinates(scope, xarg, yarg)?);
97 }
98 Ok(points)
99}
100
101fn parse_polygon_coordinates_from_array(scope: &Scope<'_>) -> CallResult<Vec<PixelsXY>> {
103 debug_assert_eq!(1, scope.nargs());
104
105 let array = scope.get_ref(0);
106 let pos = scope.get_pos(0);
107 if array.vtype != ExprType::Integer {
108 return Err(CallError::Syntax(pos, "Expected an INTEGER array".to_owned()));
109 }
110
111 let dimensions = array.array_dimensions();
112 match dimensions {
113 [ncoords] => {
114 if !ncoords.is_multiple_of(2) {
115 return Err(CallError::Syntax(
116 pos,
117 "Expected an even number of coordinates".to_owned(),
118 ));
119 }
120
121 let mut points = Vec::with_capacity(*ncoords / 2);
122 for i in (0..*ncoords).step_by(2) {
123 let x = array
124 .deref_array_integer(&[i as i32])
125 .map_err(|e| CallError::Syntax(pos, e))?;
126 let y = array
127 .deref_array_integer(&[(i + 1) as i32])
128 .map_err(|e| CallError::Syntax(pos, e))?;
129 points.push(PixelsXY {
130 x: parse_coordinate_value(pos, x)?,
131 y: parse_coordinate_value(pos, y)?,
132 });
133 }
134 Ok(points)
135 }
136
137 [npoints, 2] => {
138 let mut points = Vec::with_capacity(*npoints);
139 for i in 0..*npoints {
140 let x = array
141 .deref_array_integer(&[i as i32, 0])
142 .map_err(|e| CallError::Syntax(pos, e))?;
143 let y = array
144 .deref_array_integer(&[i as i32, 1])
145 .map_err(|e| CallError::Syntax(pos, e))?;
146 points.push(PixelsXY {
147 x: parse_coordinate_value(pos, x)?,
148 y: parse_coordinate_value(pos, y)?,
149 });
150 }
151 Ok(points)
152 }
153
154 _ => Err(CallError::Syntax(
155 pos,
156 "Expected a flat array of coordinates or an Nx2 array of points".to_owned(),
157 )),
158 }
159}
160
161fn parse_radius(scope: &Scope<'_>, narg: u8) -> CallResult<u16> {
163 let i = scope.get_integer(narg);
164 match u16::try_from(i) {
165 Ok(i) => Ok(i),
166 Err(_) if i < 0 => {
167 Err(CallError::Syntax(scope.get_pos(narg), format!("Radius {} must be positive", i)))
168 }
169 Err(_) => Err(CallError::Syntax(scope.get_pos(narg), format!("Radius {} out of range", i))),
170 }
171}
172
173pub struct GfxCircleCommand {
175 metadata: Rc<CallableMetadata>,
176 console: Rc<RefCell<dyn Console>>,
177}
178
179impl GfxCircleCommand {
180 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
182 Rc::from(Self {
183 metadata: CallableMetadataBuilder::new("GFX_CIRCLE")
184 .with_syntax(&[(
185 &[
186 SingularArgSyntax::RequiredValue(
187 RequiredValueSyntax {
188 name: Cow::Borrowed("x"),
189 vtype: ExprType::Integer,
190 },
191 ArgSepSyntax::Exactly(ArgSep::Long),
192 ),
193 SingularArgSyntax::RequiredValue(
194 RequiredValueSyntax {
195 name: Cow::Borrowed("y"),
196 vtype: ExprType::Integer,
197 },
198 ArgSepSyntax::Exactly(ArgSep::Long),
199 ),
200 SingularArgSyntax::RequiredValue(
201 RequiredValueSyntax {
202 name: Cow::Borrowed("r"),
203 vtype: ExprType::Integer,
204 },
205 ArgSepSyntax::End,
206 ),
207 ],
208 None,
209 )])
210 .with_category(CATEGORY)
211 .with_description(
212 "Draws a circle of radius r centered at (x,y).
213The outline of the circle is drawn using the foreground color as selected by COLOR and the \
214area of the circle is left untouched.",
215 )
216 .build(),
217 console,
218 })
219 }
220}
221
222impl Callable for GfxCircleCommand {
223 fn metadata(&self) -> Rc<CallableMetadata> {
224 self.metadata.clone()
225 }
226
227 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
228 debug_assert_eq!(3, scope.nargs());
229 let xy = parse_coordinates(&scope, 0, 1)?;
230 let r = parse_radius(&scope, 2)?;
231
232 self.console.borrow_mut().draw_circle(xy, r)?;
233 Ok(())
234 }
235}
236
237pub struct GfxCirclefCommand {
239 metadata: Rc<CallableMetadata>,
240 console: Rc<RefCell<dyn Console>>,
241}
242
243impl GfxCirclefCommand {
244 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
246 Rc::from(Self {
247 metadata: CallableMetadataBuilder::new("GFX_CIRCLEF")
248 .with_syntax(&[(
249 &[
250 SingularArgSyntax::RequiredValue(
251 RequiredValueSyntax {
252 name: Cow::Borrowed("x"),
253 vtype: ExprType::Integer,
254 },
255 ArgSepSyntax::Exactly(ArgSep::Long),
256 ),
257 SingularArgSyntax::RequiredValue(
258 RequiredValueSyntax {
259 name: Cow::Borrowed("y"),
260 vtype: ExprType::Integer,
261 },
262 ArgSepSyntax::Exactly(ArgSep::Long),
263 ),
264 SingularArgSyntax::RequiredValue(
265 RequiredValueSyntax {
266 name: Cow::Borrowed("r"),
267 vtype: ExprType::Integer,
268 },
269 ArgSepSyntax::End,
270 ),
271 ],
272 None,
273 )])
274 .with_category(CATEGORY)
275 .with_description(
276 "Draws a filled circle of radius r centered at (x,y).
277The outline and area of the circle are drawn using the foreground color as selected by COLOR.",
278 )
279 .build(),
280 console,
281 })
282 }
283}
284
285impl Callable for GfxCirclefCommand {
286 fn metadata(&self) -> Rc<CallableMetadata> {
287 self.metadata.clone()
288 }
289
290 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
291 debug_assert_eq!(3, scope.nargs());
292 let xy = parse_coordinates(&scope, 0, 1)?;
293 let r = parse_radius(&scope, 2)?;
294
295 self.console.borrow_mut().draw_circle_filled(xy, r)?;
296 Ok(())
297 }
298}
299
300pub struct GfxFillCommand {
302 metadata: Rc<CallableMetadata>,
303 console: Rc<RefCell<dyn Console>>,
304}
305
306impl GfxFillCommand {
307 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
309 Rc::from(Self {
310 metadata: CallableMetadataBuilder::new("GFX_FILL")
311 .with_syntax(&[(
312 &[
313 SingularArgSyntax::RequiredValue(
314 RequiredValueSyntax {
315 name: Cow::Borrowed("x"),
316 vtype: ExprType::Integer,
317 },
318 ArgSepSyntax::Exactly(ArgSep::Long),
319 ),
320 SingularArgSyntax::RequiredValue(
321 RequiredValueSyntax {
322 name: Cow::Borrowed("y"),
323 vtype: ExprType::Integer,
324 },
325 ArgSepSyntax::End,
326 ),
327 ],
328 None,
329 )])
330 .with_category(CATEGORY)
331 .with_description(
332 "Fills the region around (x,y).
333Fills the 4-connected region containing (x,y) using the foreground color as selected by COLOR. \
334If the seed pixel lies outside of the graphical console or if its color cannot be mapped back to a \
335COLOR value, this command does nothing.",
336 )
337 .build(),
338 console,
339 })
340 }
341}
342
343impl Callable for GfxFillCommand {
344 fn metadata(&self) -> Rc<CallableMetadata> {
345 self.metadata.clone()
346 }
347
348 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
349 debug_assert_eq!(2, scope.nargs());
350 let xy = parse_coordinates(&scope, 0, 1)?;
351
352 self.console.borrow_mut().bucket_fill(xy)?;
353 Ok(())
354 }
355}
356
357pub struct GfxHeightFunction {
359 metadata: Rc<CallableMetadata>,
360 console: Rc<RefCell<dyn Console>>,
361}
362
363impl GfxHeightFunction {
364 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
366 Rc::from(Self {
367 metadata: CallableMetadataBuilder::new("GFX_HEIGHT")
368 .with_return_type(ExprType::Integer)
369 .with_syntax(&[(&[], None)])
370 .with_category(CATEGORY)
371 .with_description(
372 "Returns the height in pixels of the graphical console.
373See GFX_WIDTH to query the other dimension.",
374 )
375 .build(),
376 console,
377 })
378 }
379}
380
381impl Callable for GfxHeightFunction {
382 fn metadata(&self) -> Rc<CallableMetadata> {
383 self.metadata.clone()
384 }
385
386 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
387 debug_assert_eq!(0, scope.nargs());
388 let size = self.console.borrow().size_pixels()?;
389 scope.return_integer(i32::from(size.height))
390 }
391}
392
393pub struct GfxLineCommand {
395 metadata: Rc<CallableMetadata>,
396 console: Rc<RefCell<dyn Console>>,
397}
398
399impl GfxLineCommand {
400 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
402 Rc::from(Self {
403 metadata: CallableMetadataBuilder::new("GFX_LINE")
404 .with_syntax(&[(
405 &[
406 SingularArgSyntax::RequiredValue(
407 RequiredValueSyntax {
408 name: Cow::Borrowed("x1"),
409 vtype: ExprType::Integer,
410 },
411 ArgSepSyntax::Exactly(ArgSep::Long),
412 ),
413 SingularArgSyntax::RequiredValue(
414 RequiredValueSyntax {
415 name: Cow::Borrowed("y1"),
416 vtype: ExprType::Integer,
417 },
418 ArgSepSyntax::Exactly(ArgSep::Long),
419 ),
420 SingularArgSyntax::RequiredValue(
421 RequiredValueSyntax {
422 name: Cow::Borrowed("x2"),
423 vtype: ExprType::Integer,
424 },
425 ArgSepSyntax::Exactly(ArgSep::Long),
426 ),
427 SingularArgSyntax::RequiredValue(
428 RequiredValueSyntax {
429 name: Cow::Borrowed("y2"),
430 vtype: ExprType::Integer,
431 },
432 ArgSepSyntax::End,
433 ),
434 ],
435 None,
436 )])
437 .with_category(CATEGORY)
438 .with_description(
439 "Draws a line from (x1,y1) to (x2,y2).
440The line is drawn using the foreground color as selected by COLOR.",
441 )
442 .build(),
443 console,
444 })
445 }
446}
447
448impl Callable for GfxLineCommand {
449 fn metadata(&self) -> Rc<CallableMetadata> {
450 self.metadata.clone()
451 }
452
453 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
454 debug_assert_eq!(4, scope.nargs());
455 let x1y1 = parse_coordinates(&scope, 0, 1)?;
456 let x2y2 = parse_coordinates(&scope, 2, 3)?;
457
458 self.console.borrow_mut().draw_line(x1y1, x2y2)?;
459 Ok(())
460 }
461}
462
463pub struct GfxPeekFunction {
465 metadata: Rc<CallableMetadata>,
466 console: Rc<RefCell<dyn Console>>,
467}
468
469impl GfxPeekFunction {
470 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
472 Rc::from(Self {
473 metadata: CallableMetadataBuilder::new("GFX_PEEK")
474 .with_return_type(ExprType::Integer)
475 .with_syntax(&[(
476 &[
477 SingularArgSyntax::RequiredValue(
478 RequiredValueSyntax {
479 name: Cow::Borrowed("x"),
480 vtype: ExprType::Integer,
481 },
482 ArgSepSyntax::Exactly(ArgSep::Long),
483 ),
484 SingularArgSyntax::RequiredValue(
485 RequiredValueSyntax {
486 name: Cow::Borrowed("y"),
487 vtype: ExprType::Integer,
488 },
489 ArgSepSyntax::End,
490 ),
491 ],
492 None,
493 )])
494 .with_category(CATEGORY)
495 .with_description(
496 "Returns the color number of the pixel at (x,y).
497Returns -1 if the pixel lies outside of the graphical console or if its color cannot be mapped \
498back to a COLOR value.",
499 )
500 .build(),
501 console,
502 })
503 }
504}
505
506impl Callable for GfxPeekFunction {
507 fn metadata(&self) -> Rc<CallableMetadata> {
508 self.metadata.clone()
509 }
510
511 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
512 debug_assert_eq!(2, scope.nargs());
513 let xy = parse_coordinates(&scope, 0, 1)?;
514 let color = self.console.borrow().peek_pixel(xy)?.map(i32::from).unwrap_or(-1);
515 scope.return_integer(color)
516 }
517}
518
519pub struct GfxPixelCommand {
521 metadata: Rc<CallableMetadata>,
522 console: Rc<RefCell<dyn Console>>,
523}
524
525impl GfxPixelCommand {
526 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
528 Rc::from(Self {
529 metadata: CallableMetadataBuilder::new("GFX_PIXEL")
530 .with_syntax(&[(
531 &[
532 SingularArgSyntax::RequiredValue(
533 RequiredValueSyntax {
534 name: Cow::Borrowed("x"),
535 vtype: ExprType::Integer,
536 },
537 ArgSepSyntax::Exactly(ArgSep::Long),
538 ),
539 SingularArgSyntax::RequiredValue(
540 RequiredValueSyntax {
541 name: Cow::Borrowed("y"),
542 vtype: ExprType::Integer,
543 },
544 ArgSepSyntax::End,
545 ),
546 ],
547 None,
548 )])
549 .with_category(CATEGORY)
550 .with_description(
551 "Draws a pixel at (x,y).
552The pixel is drawn using the foreground color as selected by COLOR.",
553 )
554 .build(),
555 console,
556 })
557 }
558}
559
560impl Callable for GfxPixelCommand {
561 fn metadata(&self) -> Rc<CallableMetadata> {
562 self.metadata.clone()
563 }
564
565 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
566 debug_assert_eq!(2, scope.nargs());
567 let xy = parse_coordinates(&scope, 0, 1)?;
568
569 self.console.borrow_mut().draw_pixel(xy)?;
570 Ok(())
571 }
572}
573
574pub struct GfxPolyCommand {
576 metadata: Rc<CallableMetadata>,
577 console: Rc<RefCell<dyn Console>>,
578}
579
580impl GfxPolyCommand {
581 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
583 Rc::from(Self {
584 metadata: CallableMetadataBuilder::new("GFX_POLY")
585 .with_syntax(&[
586 (&[], None),
587 (
588 &[SingularArgSyntax::RequiredRef(
589 RequiredRefSyntax {
590 name: Cow::Borrowed("points"),
591 require_array: true,
592 define_undefined: false,
593 },
594 ArgSepSyntax::End,
595 )],
596 None,
597 ),
598 (
599 &[
600 SingularArgSyntax::RequiredValue(
601 RequiredValueSyntax {
602 name: Cow::Borrowed("x1"),
603 vtype: ExprType::Integer,
604 },
605 ArgSepSyntax::Exactly(ArgSep::Long),
606 ),
607 SingularArgSyntax::RequiredValue(
608 RequiredValueSyntax {
609 name: Cow::Borrowed("y1"),
610 vtype: ExprType::Integer,
611 },
612 ArgSepSyntax::Exactly(ArgSep::Long),
613 ),
614 ],
615 Some(&RepeatedSyntax {
616 name: Cow::Borrowed("coord"),
617 type_syn: RepeatedTypeSyntax::TypedValue(ExprType::Integer),
618 sep: ArgSepSyntax::Exactly(ArgSep::Long),
619 require_one: false,
620 allow_missing: false,
621 }),
622 ),
623 ])
624 .with_category(CATEGORY)
625 .with_description(
626 "Draws a polygon with vertices at (x1,y1), (x2,y2), ..., and (xN,yN).
627The points can be specified either as individual coordinates or via an INTEGER array containing a \
628flat list of coordinates or an Nx2 matrix of points.
629The outline of the polygon is drawn using the foreground color as selected by COLOR and the area \
630of the polygon is left untouched.",
631 )
632 .build(),
633 console,
634 })
635 }
636}
637
638impl Callable for GfxPolyCommand {
639 fn metadata(&self) -> Rc<CallableMetadata> {
640 self.metadata.clone()
641 }
642
643 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
644 let points = parse_polygon_coordinates(&scope)?;
645
646 self.console.borrow_mut().draw_poly(&points)?;
647 Ok(())
648 }
649}
650
651pub struct GfxPolyfCommand {
653 metadata: Rc<CallableMetadata>,
654 console: Rc<RefCell<dyn Console>>,
655}
656
657impl GfxPolyfCommand {
658 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
660 Rc::from(Self {
661 metadata: CallableMetadataBuilder::new("GFX_POLYF")
662 .with_syntax(&[
663 (&[], None),
664 (
665 &[SingularArgSyntax::RequiredRef(
666 RequiredRefSyntax {
667 name: Cow::Borrowed("points"),
668 require_array: true,
669 define_undefined: false,
670 },
671 ArgSepSyntax::End,
672 )],
673 None,
674 ),
675 (
676 &[
677 SingularArgSyntax::RequiredValue(
678 RequiredValueSyntax {
679 name: Cow::Borrowed("x1"),
680 vtype: ExprType::Integer,
681 },
682 ArgSepSyntax::Exactly(ArgSep::Long),
683 ),
684 SingularArgSyntax::RequiredValue(
685 RequiredValueSyntax {
686 name: Cow::Borrowed("y1"),
687 vtype: ExprType::Integer,
688 },
689 ArgSepSyntax::Exactly(ArgSep::Long),
690 ),
691 ],
692 Some(&RepeatedSyntax {
693 name: Cow::Borrowed("coord"),
694 type_syn: RepeatedTypeSyntax::TypedValue(ExprType::Integer),
695 sep: ArgSepSyntax::Exactly(ArgSep::Long),
696 require_one: false,
697 allow_missing: false,
698 }),
699 ),
700 ])
701 .with_category(CATEGORY)
702 .with_description(
703 "Draws a filled polygon with vertices at (x1,y1), (x2,y2), ..., and (xN,yN).
704The points can be specified either as individual coordinates or via an INTEGER array containing a \
705flat list of coordinates or an Nx2 matrix of points.
706The outline and area of the polygon are drawn using the foreground color as selected by COLOR.",
707 )
708 .build(),
709 console,
710 })
711 }
712}
713
714impl Callable for GfxPolyfCommand {
715 fn metadata(&self) -> Rc<CallableMetadata> {
716 self.metadata.clone()
717 }
718
719 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
720 let points = parse_polygon_coordinates(&scope)?;
721
722 self.console.borrow_mut().draw_poly_filled(&points)?;
723 Ok(())
724 }
725}
726
727pub struct GfxRectCommand {
729 metadata: Rc<CallableMetadata>,
730 console: Rc<RefCell<dyn Console>>,
731}
732
733impl GfxRectCommand {
734 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
736 Rc::from(Self {
737 metadata: CallableMetadataBuilder::new("GFX_RECT")
738 .with_syntax(&[(
739 &[
740 SingularArgSyntax::RequiredValue(
741 RequiredValueSyntax {
742 name: Cow::Borrowed("x1"),
743 vtype: ExprType::Integer,
744 },
745 ArgSepSyntax::Exactly(ArgSep::Long),
746 ),
747 SingularArgSyntax::RequiredValue(
748 RequiredValueSyntax {
749 name: Cow::Borrowed("y1"),
750 vtype: ExprType::Integer,
751 },
752 ArgSepSyntax::Exactly(ArgSep::Long),
753 ),
754 SingularArgSyntax::RequiredValue(
755 RequiredValueSyntax {
756 name: Cow::Borrowed("x2"),
757 vtype: ExprType::Integer,
758 },
759 ArgSepSyntax::Exactly(ArgSep::Long),
760 ),
761 SingularArgSyntax::RequiredValue(
762 RequiredValueSyntax {
763 name: Cow::Borrowed("y2"),
764 vtype: ExprType::Integer,
765 },
766 ArgSepSyntax::End,
767 ),
768 ],
769 None,
770 )])
771 .with_category(CATEGORY)
772 .with_description(
773 "Draws a rectangle from (x1,y1) to (x2,y2).
774The outline of the rectangle is drawn using the foreground color as selected by COLOR and the \
775area of the rectangle is left untouched.",
776 )
777 .build(),
778 console,
779 })
780 }
781}
782
783impl Callable for GfxRectCommand {
784 fn metadata(&self) -> Rc<CallableMetadata> {
785 self.metadata.clone()
786 }
787
788 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
789 debug_assert_eq!(4, scope.nargs());
790 let x1y1 = parse_coordinates(&scope, 0, 1)?;
791 let x2y2 = parse_coordinates(&scope, 2, 3)?;
792
793 self.console.borrow_mut().draw_rect(x1y1, x2y2)?;
794 Ok(())
795 }
796}
797
798pub struct GfxRectfCommand {
800 metadata: Rc<CallableMetadata>,
801 console: Rc<RefCell<dyn Console>>,
802}
803
804impl GfxRectfCommand {
805 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
807 Rc::from(Self {
808 metadata: CallableMetadataBuilder::new("GFX_RECTF")
809 .with_syntax(&[(
810 &[
811 SingularArgSyntax::RequiredValue(
812 RequiredValueSyntax {
813 name: Cow::Borrowed("x1"),
814 vtype: ExprType::Integer,
815 },
816 ArgSepSyntax::Exactly(ArgSep::Long),
817 ),
818 SingularArgSyntax::RequiredValue(
819 RequiredValueSyntax {
820 name: Cow::Borrowed("y1"),
821 vtype: ExprType::Integer,
822 },
823 ArgSepSyntax::Exactly(ArgSep::Long),
824 ),
825 SingularArgSyntax::RequiredValue(
826 RequiredValueSyntax {
827 name: Cow::Borrowed("x2"),
828 vtype: ExprType::Integer,
829 },
830 ArgSepSyntax::Exactly(ArgSep::Long),
831 ),
832 SingularArgSyntax::RequiredValue(
833 RequiredValueSyntax {
834 name: Cow::Borrowed("y2"),
835 vtype: ExprType::Integer,
836 },
837 ArgSepSyntax::End,
838 ),
839 ],
840 None,
841 )])
842 .with_category(CATEGORY)
843 .with_description(
844 "Draws a filled rectangle from (x1,y1) to (x2,y2).
845The outline and area of the rectangle are drawn using the foreground color as selected by COLOR.",
846 )
847 .build(),
848 console,
849 })
850 }
851}
852
853impl Callable for GfxRectfCommand {
854 fn metadata(&self) -> Rc<CallableMetadata> {
855 self.metadata.clone()
856 }
857
858 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
859 debug_assert_eq!(4, scope.nargs());
860 let x1y1 = parse_coordinates(&scope, 0, 1)?;
861 let x2y2 = parse_coordinates(&scope, 2, 3)?;
862
863 self.console.borrow_mut().draw_rect_filled(x1y1, x2y2)?;
864 Ok(())
865 }
866}
867
868pub struct GfxSyncCommand {
870 metadata: Rc<CallableMetadata>,
871 console: Rc<RefCell<dyn Console>>,
872}
873
874pub struct GfxTriCommand {
876 metadata: Rc<CallableMetadata>,
877 console: Rc<RefCell<dyn Console>>,
878}
879
880impl GfxTriCommand {
881 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
883 Rc::from(Self {
884 metadata: CallableMetadataBuilder::new("GFX_TRI")
885 .with_syntax(&[(
886 &[
887 SingularArgSyntax::RequiredValue(
888 RequiredValueSyntax {
889 name: Cow::Borrowed("x1"),
890 vtype: ExprType::Integer,
891 },
892 ArgSepSyntax::Exactly(ArgSep::Long),
893 ),
894 SingularArgSyntax::RequiredValue(
895 RequiredValueSyntax {
896 name: Cow::Borrowed("y1"),
897 vtype: ExprType::Integer,
898 },
899 ArgSepSyntax::Exactly(ArgSep::Long),
900 ),
901 SingularArgSyntax::RequiredValue(
902 RequiredValueSyntax {
903 name: Cow::Borrowed("x2"),
904 vtype: ExprType::Integer,
905 },
906 ArgSepSyntax::Exactly(ArgSep::Long),
907 ),
908 SingularArgSyntax::RequiredValue(
909 RequiredValueSyntax {
910 name: Cow::Borrowed("y2"),
911 vtype: ExprType::Integer,
912 },
913 ArgSepSyntax::Exactly(ArgSep::Long),
914 ),
915 SingularArgSyntax::RequiredValue(
916 RequiredValueSyntax {
917 name: Cow::Borrowed("x3"),
918 vtype: ExprType::Integer,
919 },
920 ArgSepSyntax::Exactly(ArgSep::Long),
921 ),
922 SingularArgSyntax::RequiredValue(
923 RequiredValueSyntax {
924 name: Cow::Borrowed("y3"),
925 vtype: ExprType::Integer,
926 },
927 ArgSepSyntax::End,
928 ),
929 ],
930 None,
931 )])
932 .with_category(CATEGORY)
933 .with_description(
934 "Draws a triangle with vertices at (x1,y1), (x2,y2), and (x3,y3).
935The outline of the triangle is drawn using the foreground color as selected by COLOR and the \
936area of the triangle is left untouched.",
937 )
938 .build(),
939 console,
940 })
941 }
942}
943
944impl Callable for GfxTriCommand {
945 fn metadata(&self) -> Rc<CallableMetadata> {
946 self.metadata.clone()
947 }
948
949 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
950 debug_assert_eq!(6, scope.nargs());
951 let (x1y1, x2y2, x3y3) = parse_triangle_coordinates(&scope, 0, 1, 2, 3, 4, 5)?;
952
953 self.console.borrow_mut().draw_tri(x1y1, x2y2, x3y3)?;
954 Ok(())
955 }
956}
957
958pub struct GfxTrifCommand {
960 metadata: Rc<CallableMetadata>,
961 console: Rc<RefCell<dyn Console>>,
962}
963
964impl GfxTrifCommand {
965 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
967 Rc::from(Self {
968 metadata: CallableMetadataBuilder::new("GFX_TRIF")
969 .with_syntax(&[(
970 &[
971 SingularArgSyntax::RequiredValue(
972 RequiredValueSyntax {
973 name: Cow::Borrowed("x1"),
974 vtype: ExprType::Integer,
975 },
976 ArgSepSyntax::Exactly(ArgSep::Long),
977 ),
978 SingularArgSyntax::RequiredValue(
979 RequiredValueSyntax {
980 name: Cow::Borrowed("y1"),
981 vtype: ExprType::Integer,
982 },
983 ArgSepSyntax::Exactly(ArgSep::Long),
984 ),
985 SingularArgSyntax::RequiredValue(
986 RequiredValueSyntax {
987 name: Cow::Borrowed("x2"),
988 vtype: ExprType::Integer,
989 },
990 ArgSepSyntax::Exactly(ArgSep::Long),
991 ),
992 SingularArgSyntax::RequiredValue(
993 RequiredValueSyntax {
994 name: Cow::Borrowed("y2"),
995 vtype: ExprType::Integer,
996 },
997 ArgSepSyntax::Exactly(ArgSep::Long),
998 ),
999 SingularArgSyntax::RequiredValue(
1000 RequiredValueSyntax {
1001 name: Cow::Borrowed("x3"),
1002 vtype: ExprType::Integer,
1003 },
1004 ArgSepSyntax::Exactly(ArgSep::Long),
1005 ),
1006 SingularArgSyntax::RequiredValue(
1007 RequiredValueSyntax {
1008 name: Cow::Borrowed("y3"),
1009 vtype: ExprType::Integer,
1010 },
1011 ArgSepSyntax::End,
1012 ),
1013 ],
1014 None,
1015 )])
1016 .with_category(CATEGORY)
1017 .with_description(
1018 "Draws a filled triangle with vertices at (x1,y1), (x2,y2), and (x3,y3).
1019The outline and area of the triangle are drawn using the foreground color as selected by COLOR.",
1020 )
1021 .build(),
1022 console,
1023 })
1024 }
1025}
1026
1027impl Callable for GfxTrifCommand {
1028 fn metadata(&self) -> Rc<CallableMetadata> {
1029 self.metadata.clone()
1030 }
1031
1032 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
1033 debug_assert_eq!(6, scope.nargs());
1034 let (x1y1, x2y2, x3y3) = parse_triangle_coordinates(&scope, 0, 1, 2, 3, 4, 5)?;
1035
1036 self.console.borrow_mut().draw_tri_filled(x1y1, x2y2, x3y3)?;
1037 Ok(())
1038 }
1039}
1040
1041impl GfxSyncCommand {
1042 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
1044 Rc::from(Self {
1045 metadata: CallableMetadataBuilder::new("GFX_SYNC")
1046 .with_syntax(&[
1047 (&[], None),
1048 (
1049 &[SingularArgSyntax::RequiredValue(
1050 RequiredValueSyntax {
1051 name: Cow::Borrowed("enabled"),
1052 vtype: ExprType::Boolean,
1053 },
1054 ArgSepSyntax::End,
1055 )],
1056 None,
1057 ),
1058 ])
1059 .with_category(CATEGORY)
1060 .with_description(
1061 "Controls the video syncing flag and/or forces a sync.
1062With no arguments, this command triggers a video sync without updating the video syncing flag. \
1063When enabled? is specified, this updates the video syncing flag accordingly and triggers a video \
1064sync if enabled? is TRUE.
1065When video syncing is enabled, all console commands immediately refresh the console. This is \
1066useful to see the effects of the commands right away, which is why this is the default mode in the \
1067interpreter. However, this is a *very* inefficient way of drawing.
1068When video syncing is disabled, all console updates are buffered until video syncing is enabled \
1069again. This is perfect to draw complex graphics efficiently. If this is what you want to do, \
1070you should disable syncing first, render a frame, call GFX_SYNC to flush the frame, repeat until \
1071you are done, and then enable video syncing again. To keep a stable frame rate, use MONOTONIC \
1072to measure each frame and SLEEP only for the time left until the next one. Note that the textual \
1073cursor is not visible when video syncing is disabled.
1074WARNING: Be aware that if you disable video syncing in the interactive interpreter, you will not \
1075be able to see what you are typing any longer until you reenable video syncing.",
1076 )
1077 .build(),
1078 console,
1079 })
1080 }
1081}
1082
1083impl Callable for GfxSyncCommand {
1084 fn metadata(&self) -> Rc<CallableMetadata> {
1085 self.metadata.clone()
1086 }
1087
1088 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
1089 if scope.nargs() == 0 {
1090 self.console.borrow_mut().sync_now()?;
1091 Ok(())
1092 } else {
1093 debug_assert_eq!(1, scope.nargs());
1094 let enabled = scope.get_boolean(0);
1095
1096 let mut console = self.console.borrow_mut();
1097 if enabled {
1098 console.show_cursor()?;
1099 } else {
1100 console.hide_cursor()?;
1101 }
1102 console.set_sync(enabled)?;
1103 Ok(())
1104 }
1105 }
1106}
1107
1108pub struct GfxWidthFunction {
1110 metadata: Rc<CallableMetadata>,
1111 console: Rc<RefCell<dyn Console>>,
1112}
1113
1114impl GfxWidthFunction {
1115 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
1117 Rc::from(Self {
1118 metadata: CallableMetadataBuilder::new("GFX_WIDTH")
1119 .with_return_type(ExprType::Integer)
1120 .with_syntax(&[(&[], None)])
1121 .with_category(CATEGORY)
1122 .with_description(
1123 "Returns the width in pixels of the graphical console.
1124See GFX_HEIGHT to query the other dimension.",
1125 )
1126 .build(),
1127 console,
1128 })
1129 }
1130}
1131
1132impl Callable for GfxWidthFunction {
1133 fn metadata(&self) -> Rc<CallableMetadata> {
1134 self.metadata.clone()
1135 }
1136
1137 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
1138 debug_assert_eq!(0, scope.nargs());
1139 let size = self.console.borrow().size_pixels()?;
1140 scope.return_integer(i32::from(size.width))
1141 }
1142}
1143
1144pub fn add_all(machine: &mut MachineBuilder, console: Rc<RefCell<dyn Console>>) {
1146 machine.add_callable(GfxCircleCommand::new(console.clone()));
1147 machine.add_callable(GfxCirclefCommand::new(console.clone()));
1148 machine.add_callable(GfxFillCommand::new(console.clone()));
1149 machine.add_callable(GfxHeightFunction::new(console.clone()));
1150 machine.add_callable(GfxLineCommand::new(console.clone()));
1151 machine.add_callable(GfxPeekFunction::new(console.clone()));
1152 machine.add_callable(GfxPixelCommand::new(console.clone()));
1153 machine.add_callable(GfxPolyCommand::new(console.clone()));
1154 machine.add_callable(GfxPolyfCommand::new(console.clone()));
1155 machine.add_callable(GfxRectCommand::new(console.clone()));
1156 machine.add_callable(GfxRectfCommand::new(console.clone()));
1157 machine.add_callable(GfxSyncCommand::new(console.clone()));
1158 machine.add_callable(GfxTriCommand::new(console.clone()));
1159 machine.add_callable(GfxTrifCommand::new(console.clone()));
1160 machine.add_callable(GfxWidthFunction::new(console));
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use crate::console::SizeInPixels;
1167 use crate::testutils::*;
1168
1169 fn check_errors_two_xy(name: &'static str) {
1171 for args in &["1, 2, , 4", "1, 2, 3", "1, 2, 3, 4, 5", "2; 3, 4"] {
1172 check_stmt_compilation_err(
1173 format!("1:1: {} expected x1%, y1%, x2%, y2%", name),
1174 &format!("{} {}", name, args),
1175 );
1176 }
1177
1178 for args in &["-40000, 1, 1, 1", "1, -40000, 1, 1", "1, 1, -40000, 1", "1, 1, 1, -40000"] {
1179 let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1180 check_stmt_err(
1181 format!("1:{}: Coordinate -40000 out of range", pos),
1182 &format!("{} {}", name, args),
1183 );
1184 }
1185
1186 for args in &["40000, 1, 1, 1", "1, 40000, 1, 1", "1, 1, 40000, 1", "1, 1, 1, 40000"] {
1187 let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1188 check_stmt_err(
1189 format!("1:{}: Coordinate 40000 out of range", pos),
1190 &format!("{} {}", name, args),
1191 );
1192 }
1193
1194 for args in &["\"a\", 1, 1, 1", "1, \"a\", 1, 1", "1, 1, \"a\", 1", "1, 1, 1, \"a\""] {
1195 let stmt = &format!("{} {}", name, args);
1196 let pos = stmt.find('"').unwrap() + 1;
1197 check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1198 }
1199 }
1200
1201 fn check_errors_three_xy(name: &'static str) {
1203 for args in &["1, 2, 3, 4, 5", "1, 2, 3, 4, 5, 6, 7", "1, 2, 3, 4, , 6"] {
1204 check_stmt_compilation_err(
1205 format!("1:1: {} expected x1%, y1%, x2%, y2%, x3%, y3%", name),
1206 &format!("{} {}", name, args),
1207 );
1208 }
1209
1210 check_stmt_compilation_err(
1211 format!("1:{}: {} expected x1%, y1%, x2%, y2%, x3%, y3%", name.len() + 3, name),
1212 &format!("{} 2; 3, 4, 5, 6, 7", name),
1213 );
1214
1215 for args in &[
1216 "-40000, 1, 1, 1, 1, 1",
1217 "1, -40000, 1, 1, 1, 1",
1218 "1, 1, -40000, 1, 1, 1",
1219 "1, 1, 1, -40000, 1, 1",
1220 "1, 1, 1, 1, -40000, 1",
1221 "1, 1, 1, 1, 1, -40000",
1222 ] {
1223 let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1224 check_stmt_err(
1225 format!("1:{}: Coordinate -40000 out of range", pos),
1226 &format!("{} {}", name, args),
1227 );
1228 }
1229
1230 for args in &[
1231 "40000, 1, 1, 1, 1, 1",
1232 "1, 40000, 1, 1, 1, 1",
1233 "1, 1, 40000, 1, 1, 1",
1234 "1, 1, 1, 40000, 1, 1",
1235 "1, 1, 1, 1, 40000, 1",
1236 "1, 1, 1, 1, 1, 40000",
1237 ] {
1238 let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1239 check_stmt_err(
1240 format!("1:{}: Coordinate 40000 out of range", pos),
1241 &format!("{} {}", name, args),
1242 );
1243 }
1244
1245 for args in &[
1246 "\"a\", 1, 1, 1, 1, 1",
1247 "1, \"a\", 1, 1, 1, 1",
1248 "1, 1, \"a\", 1, 1, 1",
1249 "1, 1, 1, \"a\", 1, 1",
1250 "1, 1, 1, 1, \"a\", 1",
1251 "1, 1, 1, 1, 1, \"a\"",
1252 ] {
1253 let stmt = &format!("{} {}", name, args);
1254 let pos = stmt.find('"').unwrap() + 1;
1255 check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1256 }
1257 }
1258
1259 fn check_errors_poly(name: &'static str) {
1261 let syntax = format!(
1262 "1:{}: {} expected <> | <points> | <x1%, y1%[, coord1%, .., coordN%]>",
1263 name.len() + 2,
1264 name,
1265 );
1266 check_stmt_compilation_err(syntax, &format!("{} 1", name));
1267 check_stmt_err(
1268 format!("1:{}: Expected an even number of coordinates", name.len() + 14),
1269 &format!("{} 1, 2, 3, 4, 5", name),
1270 );
1271
1272 check_stmt_compilation_err(
1273 format!(
1274 "1:{}: {} expected <> | <points> | <x1%, y1%[, coord1%, .., coordN%]>",
1275 name.len() + 3,
1276 name,
1277 ),
1278 &format!("{} 2; 3, 4", name),
1279 );
1280 check_stmt_compilation_err(
1281 format!(
1282 "1:{}: {} expected <> | <points> | <x1%, y1%[, coord1%, .., coordN%]>",
1283 name.len() + 8,
1284 name,
1285 ),
1286 &format!("{} 1, 2, , 4", name),
1287 );
1288
1289 for args in &["-40000, 1", "1, -40000", "1, 1, -40000, 2", "1, 1, 2, -40000"] {
1290 let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1291 check_stmt_err(
1292 format!("1:{}: Coordinate -40000 out of range", pos),
1293 &format!("{} {}", name, args),
1294 );
1295 }
1296
1297 for args in &["40000, 1", "1, 40000", "1, 1, 40000, 2", "1, 1, 2, 40000"] {
1298 let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1299 check_stmt_err(
1300 format!("1:{}: Coordinate 40000 out of range", pos),
1301 &format!("{} {}", name, args),
1302 );
1303 }
1304
1305 for args in &["\"a\", 1", "1, \"a\"", "1, 1, \"a\", 2", "1, 1, 2, \"a\""] {
1306 let stmt = &format!("{} {}", name, args);
1307 let pos = stmt.find('"').unwrap() + 1;
1308 check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1309 }
1310
1311 check_stmt_err(
1312 format!("1:{}: Expected an even number of coordinates", name.len() + 28),
1313 &format!("DIM points(5) AS INTEGER: {} points", name),
1314 );
1315 check_stmt_err(
1316 format!(
1317 "1:{}: Expected a flat array of coordinates or an Nx2 array of points",
1318 name.len() + 31
1319 ),
1320 &format!("DIM points(3, 3) AS INTEGER: {} points", name),
1321 );
1322 check_stmt_err(
1323 format!("1:{}: Expected an INTEGER array", name.len() + 28),
1324 &format!("DIM points(6) AS BOOLEAN: {} points", name),
1325 );
1326 check_stmt_err(
1327 format!("1:{}: Coordinate 40000 out of range", name.len() + 47),
1328 &format!("DIM points(2) AS INTEGER: points(0) = 40000: {} points", name),
1329 );
1330 }
1331
1332 fn check_errors_xy_radius(name: &'static str) {
1334 for args in &["1, , 3", "1, 2", "1, 2, 3, 4"] {
1335 check_stmt_compilation_err(
1336 format!("1:1: {} expected x%, y%, r%", name),
1337 &format!("{} {}", name, args),
1338 );
1339 }
1340 check_stmt_compilation_err(
1341 format!("1:{}: {} expected x%, y%, r%", name.len() + 3, name),
1342 &format!("{} 2; 3, 4", name),
1343 );
1344
1345 for args in &["-40000, 1, 1", "1, -40000, 1"] {
1346 let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1347 check_stmt_err(
1348 format!("1:{}: Coordinate -40000 out of range", pos),
1349 &format!("{} {}", name, args),
1350 );
1351 }
1352 check_stmt_err(
1353 format!("1:{}: Radius -40000 must be positive", name.len() + 8),
1354 &format!("{} 1, 1, -40000", name),
1355 );
1356
1357 for args in &["40000, 1, 1", "1, 40000, 1"] {
1358 let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1359 check_stmt_err(
1360 format!("1:{}: Coordinate 40000 out of range", pos),
1361 &format!("{} {}", name, args),
1362 );
1363 }
1364 check_stmt_err(
1365 format!("1:{}: Radius 80000 out of range", name.len() + 8),
1366 &format!("{} 1, 1, 80000", name),
1367 );
1368
1369 for args in &["\"a\", 1, 1", "1, \"a\", 1", "1, 1, \"a\""] {
1370 let stmt = &format!("{} {}", name, args);
1371 let pos = stmt.find('"').unwrap() + 1;
1372 check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1373 }
1374
1375 check_stmt_err(
1376 format!("1:{}: Radius -1 must be positive", name.len() + 8),
1377 &format!("{} 1, 1, -1", name),
1378 );
1379 }
1380
1381 #[test]
1382 fn test_gfx_circle_ok() {
1383 Tester::default()
1384 .run("GFX_CIRCLE 0, 0, 0")
1385 .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 0, y: 0 }, 0)])
1386 .check();
1387
1388 Tester::default()
1389 .run("GFX_CIRCLE 1.1, 2.3, 2.5")
1390 .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 1, y: 2 }, 3)])
1391 .check();
1392
1393 Tester::default()
1394 .run("GFX_CIRCLE -31000, -32000, 31000")
1395 .expect_output([CapturedOut::DrawCircle(PixelsXY { x: -31000, y: -32000 }, 31000)])
1396 .check();
1397 }
1398
1399 #[test]
1400 fn test_gfx_circle_errors() {
1401 check_errors_xy_radius("GFX_CIRCLE");
1402 }
1403
1404 #[test]
1405 fn test_gfx_circlef_ok() {
1406 Tester::default()
1407 .run("GFX_CIRCLEF 0, 0, 0")
1408 .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 0, y: 0 }, 0)])
1409 .check();
1410
1411 Tester::default()
1412 .run("GFX_CIRCLEF 1.1, 2.3, 2.5")
1413 .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 1, y: 2 }, 3)])
1414 .check();
1415
1416 Tester::default()
1417 .run("GFX_CIRCLEF -31000, -32000, 31000")
1418 .expect_output([CapturedOut::DrawCircleFilled(
1419 PixelsXY { x: -31000, y: -32000 },
1420 31000,
1421 )])
1422 .check();
1423 }
1424
1425 #[test]
1426 fn test_gfx_circlef_errors() {
1427 check_errors_xy_radius("GFX_CIRCLEF");
1428 }
1429
1430 #[test]
1431 fn test_gfx_fill_ok() {
1432 Tester::default()
1433 .run("GFX_FILL 1, 2")
1434 .expect_output([CapturedOut::BucketFill(PixelsXY { x: 1, y: 2 })])
1435 .check();
1436
1437 Tester::default()
1438 .run("GFX_FILL -31000, -32000")
1439 .expect_output([CapturedOut::BucketFill(PixelsXY { x: -31000, y: -32000 })])
1440 .check();
1441
1442 Tester::default()
1443 .run("GFX_FILL 30999.5, 31999.7")
1444 .expect_output([CapturedOut::BucketFill(PixelsXY { x: 31000, y: 32000 })])
1445 .check();
1446 }
1447
1448 #[test]
1449 fn test_gfx_fill_errors() {
1450 for cmd in &["GFX_FILL , 2", "GFX_FILL 1, 2, 3", "GFX_FILL 1"] {
1451 check_stmt_compilation_err("1:1: GFX_FILL expected x%, y%", cmd);
1452 }
1453 check_stmt_compilation_err("1:11: GFX_FILL expected x%, y%", "GFX_FILL 1; 2");
1454
1455 for cmd in &["GFX_FILL -40000, 1", "GFX_FILL 1, -40000"] {
1456 check_stmt_err(
1457 format!("1:{}: Coordinate -40000 out of range", cmd.find('-').unwrap() + 1),
1458 cmd,
1459 );
1460 }
1461
1462 for cmd in &["GFX_FILL \"a\", 1", "GFX_FILL 1, \"a\""] {
1463 let pos = cmd.find('"').unwrap() + 1;
1464 check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), cmd);
1465 }
1466 }
1467
1468 #[test]
1469 fn test_gfx_height() {
1470 let mut t = Tester::default();
1471 t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(1, 768));
1472 t.run("result = GFX_HEIGHT").expect_var("result", 768i32).check();
1473
1474 check_expr_error("1:10: Graphical console size not yet set", "GFX_HEIGHT");
1475
1476 check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT()");
1477 check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT(1)");
1478 }
1479
1480 #[test]
1481 fn test_gfx_line_ok() {
1482 Tester::default()
1483 .run("GFX_LINE 1, 2, 3, 4")
1484 .expect_output([CapturedOut::DrawLine(
1485 PixelsXY { x: 1, y: 2 },
1486 PixelsXY { x: 3, y: 4 },
1487 )])
1488 .check();
1489
1490 Tester::default()
1491 .run("GFX_LINE -31000.3, -32000.2, 31000.4, 31999.8")
1492 .expect_output([CapturedOut::DrawLine(
1493 PixelsXY { x: -31000, y: -32000 },
1494 PixelsXY { x: 31000, y: 32000 },
1495 )])
1496 .check();
1497 }
1498
1499 #[test]
1500 fn test_gfx_line_errors() {
1501 check_errors_two_xy("GFX_LINE");
1502 }
1503
1504 #[test]
1505 fn test_gfx_pixel_ok() {
1506 Tester::default()
1507 .run("GFX_PIXEL 1, 2")
1508 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
1509 .check();
1510
1511 Tester::default()
1512 .run("GFX_PIXEL -31000, -32000")
1513 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: -31000, y: -32000 })])
1514 .check();
1515
1516 Tester::default()
1517 .run("GFX_PIXEL 30999.5, 31999.7")
1518 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 31000, y: 32000 })])
1519 .check();
1520 }
1521
1522 #[test]
1523 fn test_gfx_pixel_errors() {
1524 for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1"] {
1525 check_stmt_compilation_err("1:1: GFX_PIXEL expected x%, y%", cmd);
1526 }
1527 check_stmt_compilation_err("1:12: GFX_PIXEL expected x%, y%", "GFX_PIXEL 1; 2");
1528
1529 for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] {
1530 check_stmt_err(
1531 format!("1:{}: Coordinate -40000 out of range", cmd.find('-').unwrap() + 1),
1532 cmd,
1533 );
1534 }
1535
1536 for cmd in &["GFX_PIXEL \"a\", 1", "GFX_PIXEL 1, \"a\""] {
1537 let pos = cmd.find('"').unwrap() + 1;
1538 check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), cmd);
1539 }
1540 }
1541
1542 #[test]
1543 fn test_gfx_peek_ok() {
1544 let mut t = Tester::default();
1545 t.get_console().borrow_mut().set_peek_pixel(PixelsXY::new(1, 2), Some(7));
1546 t.run("result = GFX_PEEK(1, 2)").expect_var("result", 7i32).check();
1547
1548 let mut t = Tester::default();
1549 t.get_console().borrow_mut().set_peek_pixel(PixelsXY::new(1, 2), None);
1550 t.run("result = GFX_PEEK(1, 2)").expect_var("result", -1i32).check();
1551
1552 Tester::default()
1553 .run("result = GFX_PEEK(-31000.2, 31999.7)")
1554 .expect_var("result", -1i32)
1555 .check();
1556 }
1557
1558 #[test]
1559 fn test_gfx_peek_errors() {
1560 for expr in &["GFX_PEEK()", "GFX_PEEK(1)", "GFX_PEEK(1, 2, 3)"] {
1561 check_expr_compilation_error("1:10: GFX_PEEK expected x%, y%", expr);
1562 }
1563 check_expr_compilation_error("1:20: Unexpected ;", "GFX_PEEK(1; 2)");
1564
1565 for expr in &["GFX_PEEK(-40000, 1)", "GFX_PEEK(1, -40000)"] {
1566 let pos = expr.find('-').unwrap() + 10;
1567 check_expr_error(format!("1:{}: Coordinate -40000 out of range", pos), expr);
1568 }
1569
1570 for expr in &["GFX_PEEK(40000, 1)", "GFX_PEEK(1, 40000)"] {
1571 let pos = expr.find('4').unwrap() + 10;
1572 check_expr_error(format!("1:{}: Coordinate 40000 out of range", pos), expr);
1573 }
1574
1575 for expr in &[r#"GFX_PEEK("a", 1)"#, r#"GFX_PEEK(1, "a")"#] {
1576 let pos = expr.find('"').unwrap() + 10;
1577 check_expr_compilation_error(format!("1:{}: STRING is not a number", pos), expr);
1578 }
1579 }
1580
1581 #[test]
1582 fn test_gfx_poly_ok() {
1583 Tester::default().run("GFX_POLY").expect_output([]).check();
1584
1585 Tester::default()
1586 .run("GFX_POLY 1.1, 2.3")
1587 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
1588 .check();
1589
1590 Tester::default()
1591 .run("GFX_POLY 1.1, 2.3, 2.5, 3.9")
1592 .expect_output([CapturedOut::DrawLine(
1593 PixelsXY { x: 1, y: 2 },
1594 PixelsXY { x: 3, y: 4 },
1595 )])
1596 .check();
1597
1598 Tester::default()
1599 .run("GFX_POLY 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1600 .expect_output([CapturedOut::DrawTri(
1601 PixelsXY { x: 1, y: 2 },
1602 PixelsXY { x: 3, y: 4 },
1603 PixelsXY { x: 4, y: 6 },
1604 )])
1605 .check();
1606
1607 Tester::default()
1608 .run("GFX_POLY -31000, -32000, 31000, -32000, 0, 32000, -1, 0")
1609 .expect_output([CapturedOut::DrawPoly(vec![
1610 PixelsXY { x: -31000, y: -32000 },
1611 PixelsXY { x: 31000, y: -32000 },
1612 PixelsXY { x: 0, y: 32000 },
1613 PixelsXY { x: -1, y: 0 },
1614 ])])
1615 .check();
1616
1617 Tester::default()
1618 .run(
1619 "DIM points(6) AS INTEGER\
1620 : points(0) = 1\
1621 : points(1) = 2\
1622 : points(2) = 3\
1623 : points(3) = 4\
1624 : points(4) = 5\
1625 : points(5) = 6\
1626 : GFX_POLY points",
1627 )
1628 .expect_output([CapturedOut::DrawTri(
1629 PixelsXY { x: 1, y: 2 },
1630 PixelsXY { x: 3, y: 4 },
1631 PixelsXY { x: 5, y: 6 },
1632 )])
1633 .check();
1634
1635 Tester::default()
1636 .run(
1637 "DIM points(3, 2) AS INTEGER\
1638 : points(0, 0) = 1\
1639 : points(0, 1) = 2\
1640 : points(1, 0) = 3\
1641 : points(1, 1) = 4\
1642 : points(2, 0) = 5\
1643 : points(2, 1) = 6\
1644 : GFX_POLY points",
1645 )
1646 .expect_output([CapturedOut::DrawTri(
1647 PixelsXY { x: 1, y: 2 },
1648 PixelsXY { x: 3, y: 4 },
1649 PixelsXY { x: 5, y: 6 },
1650 )])
1651 .check();
1652 }
1653
1654 #[test]
1655 fn test_gfx_poly_errors() {
1656 check_errors_poly("GFX_POLY");
1657 }
1658
1659 #[test]
1660 fn test_gfx_polyf_ok() {
1661 Tester::default().run("GFX_POLYF").expect_output([]).check();
1662
1663 Tester::default()
1664 .run("GFX_POLYF 1.1, 2.3")
1665 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
1666 .check();
1667
1668 Tester::default()
1669 .run("GFX_POLYF 1.1, 2.3, 2.5, 3.9")
1670 .expect_output([CapturedOut::DrawLine(
1671 PixelsXY { x: 1, y: 2 },
1672 PixelsXY { x: 3, y: 4 },
1673 )])
1674 .check();
1675
1676 Tester::default()
1677 .run("GFX_POLYF 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1678 .expect_output([CapturedOut::DrawTriFilled(
1679 PixelsXY { x: 1, y: 2 },
1680 PixelsXY { x: 3, y: 4 },
1681 PixelsXY { x: 4, y: 6 },
1682 )])
1683 .check();
1684
1685 Tester::default()
1686 .run("GFX_POLYF -31000, -32000, 31000, -32000, 0, 32000, -1, 0")
1687 .expect_output([CapturedOut::DrawPolyFilled(vec![
1688 PixelsXY { x: -31000, y: -32000 },
1689 PixelsXY { x: 31000, y: -32000 },
1690 PixelsXY { x: 0, y: 32000 },
1691 PixelsXY { x: -1, y: 0 },
1692 ])])
1693 .check();
1694
1695 Tester::default()
1696 .run(
1697 "DIM points(6) AS INTEGER\
1698 : points(0) = 1\
1699 : points(1) = 2\
1700 : points(2) = 3\
1701 : points(3) = 4\
1702 : points(4) = 5\
1703 : points(5) = 6\
1704 : GFX_POLYF points",
1705 )
1706 .expect_output([CapturedOut::DrawTriFilled(
1707 PixelsXY { x: 1, y: 2 },
1708 PixelsXY { x: 3, y: 4 },
1709 PixelsXY { x: 5, y: 6 },
1710 )])
1711 .check();
1712
1713 Tester::default()
1714 .run(
1715 "DIM points(3, 2) AS INTEGER\
1716 : points(0, 0) = 1\
1717 : points(0, 1) = 2\
1718 : points(1, 0) = 3\
1719 : points(1, 1) = 4\
1720 : points(2, 0) = 5\
1721 : points(2, 1) = 6\
1722 : GFX_POLYF points",
1723 )
1724 .expect_output([CapturedOut::DrawTriFilled(
1725 PixelsXY { x: 1, y: 2 },
1726 PixelsXY { x: 3, y: 4 },
1727 PixelsXY { x: 5, y: 6 },
1728 )])
1729 .check();
1730 }
1731
1732 #[test]
1733 fn test_gfx_polyf_errors() {
1734 check_errors_poly("GFX_POLYF");
1735 }
1736
1737 #[test]
1738 fn test_gfx_rect_ok() {
1739 Tester::default()
1740 .run("GFX_RECT 1.1, 2.3, 2.5, 3.9")
1741 .expect_output([CapturedOut::DrawRect(
1742 PixelsXY { x: 1, y: 2 },
1743 PixelsXY { x: 3, y: 4 },
1744 )])
1745 .check();
1746
1747 Tester::default()
1748 .run("GFX_RECT -31000, -32000, 31000, 32000")
1749 .expect_output([CapturedOut::DrawRect(
1750 PixelsXY { x: -31000, y: -32000 },
1751 PixelsXY { x: 31000, y: 32000 },
1752 )])
1753 .check();
1754 }
1755
1756 #[test]
1757 fn test_gfx_rect_errors() {
1758 check_errors_two_xy("GFX_RECT");
1759 }
1760
1761 #[test]
1762 fn test_gfx_rectf_ok() {
1763 Tester::default()
1764 .run("GFX_RECTF 1.1, 2.3, 2.5, 3.9")
1765 .expect_output([CapturedOut::DrawRectFilled(
1766 PixelsXY { x: 1, y: 2 },
1767 PixelsXY { x: 3, y: 4 },
1768 )])
1769 .check();
1770
1771 Tester::default()
1772 .run("GFX_RECTF -31000, -32000, 31000, 32000")
1773 .expect_output([CapturedOut::DrawRectFilled(
1774 PixelsXY { x: -31000, y: -32000 },
1775 PixelsXY { x: 31000, y: 32000 },
1776 )])
1777 .check();
1778 }
1779
1780 #[test]
1781 fn test_gfx_rectf_errors() {
1782 check_errors_two_xy("GFX_RECTF");
1783 }
1784
1785 #[test]
1786 fn test_gfx_sync_ok() {
1787 Tester::default().run("GFX_SYNC").expect_output([CapturedOut::SyncNow]).check();
1788 Tester::default()
1789 .run("GFX_SYNC TRUE")
1790 .expect_output([CapturedOut::ShowCursor, CapturedOut::SetSync(true)])
1791 .check();
1792 Tester::default()
1793 .run("GFX_SYNC FALSE")
1794 .expect_output([CapturedOut::HideCursor, CapturedOut::SetSync(false)])
1795 .check();
1796 }
1797
1798 #[test]
1799 fn test_gfx_sync_errors() {
1800 check_stmt_compilation_err("1:1: GFX_SYNC expected <> | <enabled?>", "GFX_SYNC 2, 3");
1801 check_stmt_compilation_err("1:10: Expected BOOLEAN but found INTEGER", "GFX_SYNC 2");
1802 }
1803
1804 #[test]
1805 fn test_gfx_tri_ok() {
1806 Tester::default()
1807 .run("GFX_TRI 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1808 .expect_output([CapturedOut::DrawTri(
1809 PixelsXY { x: 1, y: 2 },
1810 PixelsXY { x: 3, y: 4 },
1811 PixelsXY { x: 4, y: 6 },
1812 )])
1813 .check();
1814
1815 Tester::default()
1816 .run("GFX_TRI -31000, -32000, 31000, -32000, 0, 32000")
1817 .expect_output([CapturedOut::DrawTri(
1818 PixelsXY { x: -31000, y: -32000 },
1819 PixelsXY { x: 31000, y: -32000 },
1820 PixelsXY { x: 0, y: 32000 },
1821 )])
1822 .check();
1823 }
1824
1825 #[test]
1826 fn test_gfx_tri_errors() {
1827 check_errors_three_xy("GFX_TRI");
1828 }
1829
1830 #[test]
1831 fn test_gfx_trif_ok() {
1832 Tester::default()
1833 .run("GFX_TRIF 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1834 .expect_output([CapturedOut::DrawTriFilled(
1835 PixelsXY { x: 1, y: 2 },
1836 PixelsXY { x: 3, y: 4 },
1837 PixelsXY { x: 4, y: 6 },
1838 )])
1839 .check();
1840
1841 Tester::default()
1842 .run("GFX_TRIF -31000, -32000, 31000, -32000, 0, 32000")
1843 .expect_output([CapturedOut::DrawTriFilled(
1844 PixelsXY { x: -31000, y: -32000 },
1845 PixelsXY { x: 31000, y: -32000 },
1846 PixelsXY { x: 0, y: 32000 },
1847 )])
1848 .check();
1849 }
1850
1851 #[test]
1852 fn test_gfx_trif_errors() {
1853 check_errors_three_xy("GFX_TRIF");
1854 }
1855
1856 #[test]
1857 fn test_gfx_width() {
1858 let mut t = Tester::default();
1859 t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(12345, 1));
1860 t.run("result = GFX_WIDTH").expect_var("result", 12345i32).check();
1861
1862 check_expr_error("1:10: Graphical console size not yet set", "GFX_WIDTH");
1863
1864 check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH()");
1865 check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH(1)");
1866 }
1867}