denko 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/build_atmega_avr.yml +2 -1
- data/.github/workflows/build_atmega_megaavr.yml +2 -1
- data/.github/workflows/build_atsam3x.yml +1 -0
- data/.github/workflows/build_atsamd21.yml +2 -1
- data/.github/workflows/build_esp32.yml +4 -2
- data/.github/workflows/build_esp32c3.yml +4 -3
- data/.github/workflows/build_esp32c6.yml +4 -2
- data/.github/workflows/build_esp32h2.yml +4 -2
- data/.github/workflows/build_esp32s2.yml +4 -2
- data/.github/workflows/build_esp32s3.yml +4 -2
- data/.github/workflows/build_esp8266.yml +2 -1
- data/.github/workflows/build_ra4m1.yml +1 -0
- data/.github/workflows/build_rp2040.yml +4 -3
- data/.github/workflows/ruby.yml +1 -1
- data/CHANGELOG.md +203 -0
- data/DEPS_CLI.md +16 -16
- data/DEPS_IDE.md +31 -30
- data/MICROCONTROLLERS.md +103 -0
- data/PERIPHERALS.md +178 -0
- data/README.md +28 -21
- data/denko.gemspec +6 -1
- data/lib/denko/analog_io/ads1118.rb +5 -5
- data/lib/denko/analog_io/ads111x.rb +23 -19
- data/lib/denko/analog_io/joystick.rb +87 -0
- data/lib/denko/analog_io/potentiometer.rb +1 -5
- data/lib/denko/analog_io.rb +22 -8
- data/lib/denko/behaviors/bus_controller.rb +2 -1
- data/lib/denko/behaviors/bus_peripheral.rb +1 -1
- data/lib/denko/behaviors/callbacks.rb +18 -16
- data/lib/denko/behaviors/component.rb +0 -4
- data/lib/denko/behaviors/lifecycle.rb +1 -1
- data/lib/denko/behaviors/listener.rb +9 -3
- data/lib/denko/behaviors/multi_pin.rb +4 -6
- data/lib/denko/behaviors/poller.rb +11 -2
- data/lib/denko/behaviors/reader.rb +109 -21
- data/lib/denko/behaviors/single_pin.rb +2 -4
- data/lib/denko/behaviors/state.rb +18 -13
- data/lib/denko/behaviors/threaded.rb +19 -8
- data/lib/denko/behaviors.rb +36 -23
- data/lib/denko/board/eeprom.rb +1 -1
- data/lib/denko/board/i2c.rb +1 -1
- data/lib/denko/board/i2c_bit_bang.rb +9 -5
- data/lib/denko/board/map.rb +6 -2
- data/lib/denko/board/one_wire.rb +3 -3
- data/lib/denko/board/spi.rb +30 -30
- data/lib/denko/board/spi_bit_bang.rb +8 -11
- data/lib/denko/board.rb +6 -3
- data/lib/denko/connection/flow_control.rb +1 -1
- data/lib/denko/connection/serial.rb +5 -5
- data/lib/denko/digital_io/output.rb +12 -4
- data/lib/denko/digital_io/pcf8574.rb +114 -0
- data/lib/denko/digital_io/rotary_encoder.rb +10 -6
- data/lib/denko/digital_io.rb +24 -6
- data/lib/denko/display/canvas.rb +350 -157
- data/lib/denko/display/font/bmp_5x7.rb +142 -0
- data/lib/denko/display/font/bmp_6x8.rb +142 -0
- data/lib/denko/display/font/bmp_8x16.rb +141 -0
- data/lib/denko/display/font.rb +22 -0
- data/lib/denko/display/hd44780.rb +24 -20
- data/lib/denko/display/il0373.rb +186 -0
- data/lib/denko/display/mono_oled.rb +193 -0
- data/lib/denko/display/pcd8544.rb +154 -0
- data/lib/denko/display/pixel_common.rb +83 -0
- data/lib/denko/display/sh1106.rb +17 -21
- data/lib/denko/display/sh1107.rb +10 -0
- data/lib/denko/display/spi_common.rb +35 -0
- data/lib/denko/display/spi_epaper_common.rb +30 -0
- data/lib/denko/display/ssd1306.rb +6 -228
- data/lib/denko/display/ssd1680.rb +14 -0
- data/lib/denko/display/ssd1681.rb +8 -0
- data/lib/denko/display/ssd168x.rb +227 -0
- data/lib/denko/display/st7302.rb +207 -0
- data/lib/denko/display/st7565.rb +166 -0
- data/lib/denko/display.rb +40 -4
- data/lib/denko/eeprom/at24c.rb +67 -0
- data/lib/denko/eeprom/board.rb +69 -0
- data/lib/denko/eeprom.rb +15 -1
- data/lib/denko/helpers/engine_check.rb +13 -0
- data/lib/denko/{mutex_stub.rb → helpers/mutex_stub.rb} +6 -0
- data/lib/denko/helpers.rb +6 -0
- data/lib/denko/i2c/bit_bang.rb +1 -0
- data/lib/denko/i2c/bus_common.rb +9 -4
- data/lib/denko/i2c/peripheral.rb +5 -1
- data/lib/denko/i2c.rb +17 -4
- data/lib/denko/led/apa102.rb +1 -3
- data/lib/denko/led/base.rb +5 -0
- data/lib/denko/led/rgb.rb +16 -10
- data/lib/denko/led/seven_segment.rb +1 -1
- data/lib/denko/led.rb +17 -8
- data/lib/denko/motor/{stepper.rb → a3967.rb} +1 -1
- data/lib/denko/motor/servo.rb +16 -6
- data/lib/denko/motor.rb +16 -3
- data/lib/denko/one_wire/bus.rb +20 -16
- data/lib/denko/one_wire/bus_enumerator.rb +25 -14
- data/lib/denko/one_wire/helper.rb +4 -2
- data/lib/denko/one_wire.rb +18 -5
- data/lib/denko/pulse_io/buzzer.rb +2 -6
- data/lib/denko/pulse_io/ir_output.rb +1 -5
- data/lib/denko/pulse_io/pwm_output.rb +56 -31
- data/lib/denko/pulse_io.rb +17 -3
- data/lib/denko/rtc/ds3231.rb +4 -3
- data/lib/denko/rtc.rb +14 -1
- data/lib/denko/sensor/aht.rb +16 -20
- data/lib/denko/sensor/bme280.rb +23 -36
- data/lib/denko/sensor/bmp180.rb +8 -13
- data/lib/denko/sensor/dht.rb +17 -7
- data/lib/denko/sensor/ds18b20.rb +5 -4
- data/lib/denko/sensor/hdc1080.rb +174 -0
- data/lib/denko/sensor/htu21d.rb +17 -6
- data/lib/denko/sensor/htu31d.rb +6 -5
- data/lib/denko/sensor/jsnsr04t.rb +49 -0
- data/lib/denko/sensor/qmp6988.rb +14 -30
- data/lib/denko/sensor/rcwl9620.rb +1 -0
- data/lib/denko/sensor/sht3x.rb +6 -5
- data/lib/denko/sensor/sht4x.rb +125 -0
- data/lib/denko/sensor/vl53l0x.rb +58 -0
- data/lib/denko/sensor.rb +33 -15
- data/lib/denko/spi/base_register.rb +11 -7
- data/lib/denko/spi/bus_common.rb +12 -15
- data/lib/denko/spi/input_register.rb +6 -6
- data/lib/denko/spi/output_register.rb +13 -4
- data/lib/denko/spi/peripheral.rb +82 -84
- data/lib/denko/spi.rb +20 -10
- data/lib/denko/uart/bit_bang.rb +2 -27
- data/lib/denko/uart/common.rb +33 -0
- data/lib/denko/uart/hardware.rb +1 -26
- data/lib/denko/uart.rb +16 -2
- data/lib/denko/version.rb +1 -1
- data/lib/denko.rb +22 -25
- data/lib/denko_cli/targets.rb +7 -7
- data/lib/denko_cli/targets.txt +19 -20
- data/src/lib/Denko.cpp +26 -13
- data/src/lib/Denko.h +4 -4
- data/src/lib/DenkoDefines.h +7 -26
- data/src/lib/DenkoLEDArray.cpp +1 -8
- data/src/lib/DenkoSPI.cpp +31 -29
- data/src/lib/DenkoSPIBB.cpp +12 -14
- data/test/analog_io/input_test.rb +1 -1
- data/test/analog_io/potentiometer_test.rb +2 -2
- data/test/behaviors/bus_peripheral_test.rb +4 -4
- data/test/behaviors/callbacks_test.rb +20 -10
- data/test/behaviors/component_test.rb +18 -8
- data/test/board/board_test.rb +9 -9
- data/test/board/one_wire_test.rb +25 -14
- data/test/board/spi_test.rb +31 -15
- data/test/digital_io/input_test.rb +2 -2
- data/test/display/canvas_test.rb +306 -0
- data/test/display/hd44780_test.rb +34 -7
- data/test/eeprom/board_test.rb +45 -0
- data/test/helpers/mruby_minitest.rb +95 -0
- data/test/helpers/mruby_runner.rb +13 -0
- data/test/i2c/bus_test.rb +93 -30
- data/test/i2c/peripheral_test.rb +2 -2
- data/test/led/apa102_test.rb +24 -0
- data/test/led/rgb_test.rb +4 -4
- data/test/motor/{stepper_test.rb → a3967_test.rb} +2 -2
- data/test/one_wire/bus_enumerator_test.rb +1 -1
- data/test/one_wire/bus_test.rb +42 -35
- data/test/one_wire/peripheral_test.rb +5 -17
- data/test/pulse_io/ir_output_test.rb +5 -0
- data/test/pulse_io/pwm_output_test.rb +10 -10
- data/test/rtc/ds3231_test.rb +3 -2
- data/test/sensor/dht_test.rb +11 -11
- data/test/spi/bitbang_test.rb +27 -0
- data/test/spi/bus_test.rb +19 -29
- data/test/spi/input_register_test.rb +2 -2
- data/test/spi/{peripheral_multi_pin_test.rb → peripheral_test.rb} +25 -5
- data/test/test_helper.rb +44 -124
- data/vendor/board-maps/BoardMap.h +264 -0
- data/vendor/board-maps/yaml/ALFREDO_NOU3.yml +2 -0
- data/vendor/board-maps/yaml/ATD143_S3.yml +1 -0
- data/vendor/board-maps/yaml/BHARATPI_A7672S_4G.yml +14 -0
- data/vendor/board-maps/yaml/BHARATPI_LORA.yml +14 -0
- data/vendor/board-maps/yaml/BHARATPI_NODE_WIFI.yml +14 -0
- data/vendor/board-maps/yaml/BPI_LEAF_S3.yml +0 -1
- data/vendor/board-maps/yaml/CEZERIO_DEV_ESP32C6.yml +14 -0
- data/vendor/board-maps/yaml/CEZERIO_MINI_DEV_ESP32C6.yml +12 -0
- data/vendor/board-maps/yaml/CIRCUITART_ZERO_S3.yml +71 -0
- data/vendor/board-maps/yaml/CODECELLC3.yml +13 -0
- data/vendor/board-maps/yaml/CYOBOT_V2_ESP32S3.yml +7 -0
- data/vendor/board-maps/yaml/EDGES3D.yml +25 -0
- data/vendor/board-maps/yaml/ESP32C6_DEV.yml +3 -4
- data/vendor/board-maps/yaml/ESP32C6_THING_PLUS.yml +0 -1
- data/vendor/board-maps/yaml/ESP32H2_DEV.yml +0 -1
- data/vendor/board-maps/yaml/ESP32H2_DEVKIT_LIPO.yml +0 -1
- data/vendor/board-maps/yaml/ESP32P4_DEV.yml +35 -0
- data/vendor/board-maps/yaml/ESP32S2_DEV.yml +0 -1
- data/vendor/board-maps/yaml/ESP32S2_DEVKIT_LIPO.yml +0 -1
- data/vendor/board-maps/yaml/ESP32S2_DEVKIT_LIPO_USB.yml +0 -1
- data/vendor/board-maps/yaml/ESP32_2432S028R.yml +14 -0
- data/vendor/board-maps/yaml/FEATHERS3.yml +1 -1
- data/vendor/board-maps/yaml/FRI3D_2024_ESP32S3.yml +43 -0
- data/vendor/board-maps/yaml/GEEKBLE_ESP32C3.yml +0 -1
- data/vendor/board-maps/yaml/GEEKBLE_NANO_ESP32S3.yml +25 -0
- data/vendor/board-maps/yaml/HELTEC_VISION_MASTER_E290.yml +41 -0
- data/vendor/board-maps/yaml/HELTEC_VISION_MASTER_E_213.yml +41 -0
- data/vendor/board-maps/yaml/HELTEC_VISION_MASTER_T190.yml +41 -0
- data/vendor/board-maps/yaml/HUIDU_HD_WF2.yml +5 -0
- data/vendor/board-maps/yaml/HUIDU_HD_WF4.yml +1 -0
- data/vendor/board-maps/yaml/LILYGO_LORA_CC1101.yml +6 -0
- data/vendor/board-maps/yaml/LILYGO_LORA_LR1121.yml +6 -0
- data/vendor/board-maps/yaml/LILYGO_LORA_SI4432.yml +6 -0
- data/vendor/board-maps/yaml/LILYGO_LORA_SX1262.yml +6 -0
- data/vendor/board-maps/yaml/LILYGO_LORA_SX1280.yml +6 -0
- data/vendor/board-maps/yaml/LOLIN_C3_MINI.yml +0 -1
- data/vendor/board-maps/yaml/LOLIN_C3_PICO.yml +1 -2
- data/vendor/board-maps/yaml/LoPy.yml +0 -1
- data/vendor/board-maps/yaml/LoPy4.yml +0 -1
- data/vendor/board-maps/yaml/M5STACK_DINMETER.yml +8 -0
- data/vendor/board-maps/yaml/M5STACK_FIRE.yml +1 -1
- data/vendor/board-maps/yaml/OMGS3.yml +25 -0
- data/vendor/board-maps/yaml/PCBCUPID_GLYPHC3.yml +23 -0
- data/vendor/board-maps/yaml/PCBCUPID_GLYPHC6.yml +32 -0
- data/vendor/board-maps/yaml/PCBCUPID_GLYPHH2.yml +24 -0
- data/vendor/board-maps/yaml/PYCOM_GPY.yml +0 -1
- data/vendor/board-maps/yaml/SENSEBOX_MCU_ESP32S2.yml +1 -1
- data/vendor/board-maps/yaml/SPARKFUN_ESP32S3_THING_PLUS.yml +13 -0
- data/vendor/board-maps/yaml/SPARKLEMOTIONMINI_ESP32.yml +12 -0
- data/vendor/board-maps/yaml/SPARKLEMOTIONSTICK_ESP32.yml +11 -0
- data/vendor/board-maps/yaml/SPARKLEMOTION_ESP32.yml +12 -0
- data/vendor/board-maps/yaml/SQUIXL.yml +7 -0
- data/vendor/board-maps/yaml/THINGPULSE_EPULSE_FEATHER_C6.yml +0 -1
- data/vendor/board-maps/yaml/T_LORA_PAGER.yml +6 -0
- data/vendor/board-maps/yaml/T_WATCH_S3.yml +7 -0
- data/vendor/board-maps/yaml/T_WATCH_S3_ULTRA.yml +6 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_146.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_147.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_169.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_LCD_185.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_RELAY_6CH.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_143.yml +7 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_164.yml +7 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_18.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_191.yml +7 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_AMOLED_241.yml +7 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_146.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_169.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_185.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_185_BOX.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_21.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_28.yml +41 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_4.yml +36 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_43.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_43B.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_5.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_5B.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_TOUCH_LCD_7.yml +38 -0
- data/vendor/board-maps/yaml/WAVESHARE_ESP32_S3_ZERO.yml +36 -0
- data/vendor/board-maps/yaml/WIPY3.yml +0 -1
- data/vendor/board-maps/yaml/WS_ESP32_S3_MATRIX.yml +38 -0
- data/vendor/board-maps/yaml/XIAO_ESP32S3_PLUS.yml +46 -0
- data/vendor/board-maps/yaml/YB_ESP32S3_AMP_V2.yml +28 -0
- data/vendor/board-maps/yaml/YB_ESP32S3_AMP_V3.yml +28 -0
- data/vendor/board-maps/yaml/YB_ESP32S3_ETH.yml +40 -0
- data/vendor/board-maps/yaml/mercury.yml +20 -0
- metadata +116 -101
- data/.vscode/settings.json +0 -5
- data/.vscode/tasks.json +0 -20
- data/HARDWARE.md +0 -263
- data/benchmarks/analog_listen.rb +0 -49
- data/benchmarks/digital_write.rb +0 -28
- data/benchmarks/i2c_ssd1306_refresh.rb +0 -91
- data/examples/advanced/m5_env3.rb +0 -46
- data/examples/advanced/rotary_encoder_mac_volume.rb +0 -53
- data/examples/advanced/ssd1306_time_temp_rh.rb +0 -43
- data/examples/analog_io/ads1100.rb +0 -48
- data/examples/analog_io/ads1115.rb +0 -57
- data/examples/analog_io/ads1118.rb +0 -65
- data/examples/analog_io/dac_loopback.rb +0 -34
- data/examples/analog_io/input.rb +0 -56
- data/examples/analog_io/input_smoothing.rb +0 -27
- data/examples/analog_io/potentiometer.rb +0 -31
- data/examples/connection/binary_echo.rb +0 -34
- data/examples/connection/tcp.rb +0 -19
- data/examples/digital_io/button.rb +0 -17
- data/examples/digital_io/relay.rb +0 -17
- data/examples/digital_io/rotary_encoder.rb +0 -36
- data/examples/display/hd44780.png +0 -0
- data/examples/display/hd44780.rb +0 -47
- data/examples/display/ssd1306.rb +0 -43
- data/examples/display/ssd1306_s2_pico.rb +0 -29
- data/examples/eeprom/built_in.rb +0 -32
- data/examples/i2c/search.rb +0 -39
- data/examples/led/apa102_bounce.rb +0 -32
- data/examples/led/apa102_fade.rb +0 -44
- data/examples/led/builtin_blink.rb +0 -14
- data/examples/led/builtin_fade.rb +0 -19
- data/examples/led/rgb_led.rb +0 -31
- data/examples/led/seven_segment_char_echo.rb +0 -17
- data/examples/led/ws2812_bounce.rb +0 -30
- data/examples/led/ws2812_builtin_blink.rb +0 -22
- data/examples/led/ws2812_fade.rb +0 -43
- data/examples/motor/l298.rb +0 -45
- data/examples/motor/servo.rb +0 -17
- data/examples/motor/stepper.png +0 -0
- data/examples/motor/stepper.rb +0 -43
- data/examples/one_wire/search.rb +0 -32
- data/examples/pulse_io/buzzer.rb +0 -35
- data/examples/pulse_io/ir_output.rb +0 -51
- data/examples/pulse_io/pwm_output.rb +0 -30
- data/examples/rtc/ds3231.rb +0 -31
- data/examples/sensor/aht10.rb +0 -17
- data/examples/sensor/aht20.rb +0 -17
- data/examples/sensor/bme280.rb +0 -38
- data/examples/sensor/bmp180.rb +0 -26
- data/examples/sensor/dht.rb +0 -29
- data/examples/sensor/ds18b20.rb +0 -57
- data/examples/sensor/generic_pir.rb +0 -27
- data/examples/sensor/hcsr04.rb +0 -17
- data/examples/sensor/htu21d.rb +0 -43
- data/examples/sensor/htu31d.rb +0 -33
- data/examples/sensor/neat_tph_readings.rb +0 -32
- data/examples/sensor/qmp6988.rb +0 -51
- data/examples/sensor/rcwl9620.rb +0 -15
- data/examples/sensor/sht3x.rb +0 -32
- data/examples/spi/bitbang_loopback.rb +0 -46
- data/examples/spi/input_register.rb +0 -40
- data/examples/spi/output_register.rb +0 -41
- data/examples/spi/ssd_through_registers.rb +0 -28
- data/examples/spi/two_registers.rb +0 -40
- data/examples/uart/bit_bang_demo.rb +0 -25
- data/examples/uart/board_passthrough.rb +0 -40
- data/examples/uart/hardware_loopback.rb +0 -16
- data/lib/denko/eeprom/built_in.rb +0 -72
- data/lib/denko/fonts.rb +0 -106
- data/test/eeprom/built_in_test.rb +0 -61
- data/test/spi/peripheral_single_pin_test.rb +0 -48
- data/tutorial/01-led/led.fzz +0 -0
- data/tutorial/01-led/led.pdf +0 -0
- data/tutorial/01-led/led.rb +0 -73
- data/tutorial/02-button/button.fzz +0 -0
- data/tutorial/02-button/button.pdf +0 -0
- data/tutorial/02-button/button.rb +0 -65
- data/tutorial/03-potentiometer/potentiometer.fzz +0 -0
- data/tutorial/03-potentiometer/potentiometer.pdf +0 -0
- data/tutorial/03-potentiometer/potentiometer.rb +0 -66
- data/tutorial/04-pwm_led/pwm_led.fzz +0 -0
- data/tutorial/04-pwm_led/pwm_led.pdf +0 -0
- data/tutorial/04-pwm_led/pwm_led.rb +0 -64
- data/tutorial/05-rgb_led/rgb_led.fzz +0 -0
- data/tutorial/05-rgb_led/rgb_led.pdf +0 -0
- data/tutorial/05-rgb_led/rgb_led.rb +0 -58
- data/tutorial/05-rgb_led/rgb_mapping.rb +0 -76
data/lib/denko/display/canvas.rb
CHANGED
@@ -1,181 +1,283 @@
|
|
1
1
|
module Denko
|
2
2
|
module Display
|
3
3
|
class Canvas
|
4
|
-
|
5
|
-
|
6
|
-
attr_reader :columns, :rows, :framebuffer
|
7
|
-
|
8
|
-
def initialize(columns, rows)
|
9
|
-
raise ArgumentError, "bitmap height must be divisible by 8" unless (rows % 8 == 0)
|
10
|
-
|
11
|
-
@columns = columns
|
12
|
-
@rows = rows
|
4
|
+
attr_reader :columns, :rows, :framebuffer, :framebuffers, :colors
|
13
5
|
|
6
|
+
def initialize(columns, rows, colors: 1)
|
7
|
+
@columns = columns
|
8
|
+
@rows = rows
|
9
|
+
@rows = ((rows / 8.0).ceil * 8) if (rows % 8 != 0)
|
14
10
|
# Use a byte array for the framebuffer. Each byte is 8 pixels arranged vertically.
|
15
11
|
# Each slice @columns long represents an area @columns wide * 8 pixels tall.
|
16
|
-
@bytes
|
17
|
-
|
12
|
+
@bytes = @columns * (@rows / 8)
|
13
|
+
|
14
|
+
# Framebuffer setup. 1-bit framebuffer for each color.
|
15
|
+
# Works for mono LCDs and OLEDs, or mono/multi-color e-paper.
|
16
|
+
@colors = colors
|
17
|
+
@framebuffers = []
|
18
|
+
@colors.times { @framebuffers << Array.new(@bytes) { 0x00 } }
|
19
|
+
# Only first framebuffer is used for mono displays.
|
20
|
+
@framebuffer = @framebuffers.first
|
21
|
+
|
22
|
+
# Default drawing state
|
23
|
+
self.font = Denko::Display::Font::BMP_6X8
|
24
|
+
@font_scale = 1
|
25
|
+
@current_color = 1
|
26
|
+
|
27
|
+
# Transformation state
|
28
|
+
@swap_xy = false
|
29
|
+
@invert_x = false
|
30
|
+
@invert_y = false
|
31
|
+
@rotation = 0
|
32
|
+
calculate_bounds
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear
|
36
|
+
@framebuffers.each { |fb| fb.fill 0x00 }
|
18
37
|
end
|
19
38
|
|
20
39
|
def fill
|
21
|
-
|
40
|
+
# Clear all buffers, then fill the first one, which is the only
|
41
|
+
# one for mono displays, black for multi-color e-paper.
|
42
|
+
clear
|
43
|
+
@framebuffers.first.fill 0xFF
|
22
44
|
end
|
23
45
|
|
24
|
-
|
25
|
-
|
46
|
+
#
|
47
|
+
# PIXEL OPERATIONS
|
48
|
+
#
|
49
|
+
def _get_pixel(x, y)
|
50
|
+
byte = ((y / 8) * @columns) + x
|
51
|
+
bit = y % 8
|
52
|
+
|
53
|
+
# Go through framebuffers and return that color when bit is set.
|
54
|
+
@framebuffers.each_with_index do |fb, index|
|
55
|
+
return index+1 if ((fb[byte] >> bit) & 0b1 == 1)
|
56
|
+
end
|
57
|
+
|
58
|
+
# If bit wasn't set in any framebuffer, color is 0.
|
59
|
+
return 0
|
26
60
|
end
|
27
61
|
|
28
|
-
def get_pixel(x
|
29
|
-
|
30
|
-
bit = y % 8
|
31
|
-
(@framebuffer[byte] >> bit) & 0b00000001
|
62
|
+
def get_pixel(x:, y:)
|
63
|
+
_get_pixel(x, y)
|
32
64
|
end
|
33
65
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
66
|
+
def _set_pixel(x, y, color=current_color)
|
67
|
+
xt = (@invert_x) ? @x_max - x : x
|
68
|
+
yt = (@invert_y) ? @y_max - y : y
|
69
|
+
if (@swap_xy)
|
70
|
+
tt = xt
|
71
|
+
xt = yt
|
72
|
+
yt = tt
|
73
|
+
end
|
74
|
+
|
75
|
+
# Bounds check
|
76
|
+
return nil if (xt < 0 || yt < 0 || xt > @columns-1 || yt > @rows -1)
|
77
|
+
return nil if (color < 0) || (color > colors)
|
78
|
+
|
79
|
+
byte = ((yt / 8) * @columns) + xt
|
80
|
+
bit = yt % 8
|
81
|
+
|
82
|
+
# Set pixel bit in that color's buffer. Clear in others.
|
83
|
+
# When color == 0, clears in all buffers and sets in none.
|
84
|
+
for i in 1..colors
|
85
|
+
if (color == i)
|
86
|
+
@framebuffers[i-1][byte] |= (0b1 << bit)
|
87
|
+
else
|
88
|
+
@framebuffers[i-1][byte] &= ~(0b1 << bit)
|
89
|
+
end
|
90
|
+
end
|
38
91
|
end
|
39
92
|
|
40
|
-
def
|
41
|
-
|
42
|
-
bit = y % 8
|
43
|
-
@framebuffer[byte] &= ~(0b1 << bit)
|
93
|
+
def set_pixel(x:, y:, color:current_color)
|
94
|
+
_set_pixel(x, y, color)
|
44
95
|
end
|
45
96
|
|
46
|
-
def
|
47
|
-
(
|
97
|
+
def clear_pixel(x:, y:)
|
98
|
+
_set_pixel(x, y, 0)
|
48
99
|
end
|
49
100
|
|
50
|
-
#
|
51
|
-
|
101
|
+
#
|
102
|
+
# LINE
|
103
|
+
#
|
104
|
+
def _line(x1, y1, x2, y2, color=current_color)
|
52
105
|
# Deltas in each axis.
|
53
106
|
dy = y2 - y1
|
54
107
|
dx = x2 - x1
|
55
108
|
|
56
|
-
#
|
109
|
+
# Optimize vertical lines, and avoid division by 0.
|
57
110
|
if (dx == 0)
|
58
111
|
# Ensure y1 < y2.
|
59
112
|
y1, y2 = y2, y1 if (y2 < y1)
|
60
|
-
(y1..y2).each
|
61
|
-
pixel(x1, y, color)
|
62
|
-
end
|
113
|
+
(y1..y2).each { |y| _set_pixel(x1, y, color) }
|
63
114
|
return
|
64
115
|
end
|
65
116
|
|
66
|
-
|
67
|
-
|
68
|
-
# Gradient magnitude <= 45 degrees: find y for each x.
|
69
|
-
if (gradient >= -1) && (gradient <= 1)
|
117
|
+
# Optimize horizontal lines.
|
118
|
+
if (dy == 0)
|
70
119
|
# Ensure x1 < x2.
|
71
|
-
x1,
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
y_step = gradient
|
76
|
-
(x1..x2).each do |x|
|
77
|
-
pixel(x, y.round, color)
|
78
|
-
y = y + y_step
|
79
|
-
end
|
120
|
+
x1, x2 = x2, x1 if (x2 < x1)
|
121
|
+
(x1..x2).each { |x| _set_pixel(x, y1, color) }
|
122
|
+
return
|
123
|
+
end
|
80
124
|
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
125
|
+
# Based on Bresenham's line algorithm
|
126
|
+
# Slope calculations
|
127
|
+
step_axis = (dx.abs > dy.abs) ? :x : :y
|
128
|
+
step_count = (step_axis == :x) ? dx.abs : dy.abs
|
129
|
+
x_step = (dx > 0) ? 1 : -1
|
130
|
+
y_step = (dy > 0) ? 1 : -1
|
131
|
+
|
132
|
+
# Error calculations
|
133
|
+
error_step = (step_axis == :x) ? dy.abs : dx.abs
|
134
|
+
error_threshold = (step_axis == :x) ? dx.abs : dy.abs
|
135
|
+
|
136
|
+
x = x1
|
137
|
+
y = y1
|
138
|
+
error = 0
|
139
|
+
(0..step_count).each do |i|
|
140
|
+
_set_pixel(x, y, color)
|
141
|
+
|
142
|
+
if (step_axis == :x)
|
143
|
+
x += x_step
|
144
|
+
error += error_step
|
145
|
+
if (error >= error_threshold)
|
146
|
+
y += y_step
|
147
|
+
error -= error_threshold
|
148
|
+
end
|
149
|
+
else
|
150
|
+
y += y_step
|
151
|
+
error += error_step
|
152
|
+
if (error >= error_threshold)
|
153
|
+
x += x_step
|
154
|
+
error -= error_threshold
|
155
|
+
end
|
92
156
|
end
|
93
157
|
end
|
94
158
|
end
|
95
159
|
|
96
|
-
|
97
|
-
|
98
|
-
line(x, y, x+width, y, color)
|
99
|
-
line(x+width, y, x+width, y+height, color)
|
100
|
-
line(x+width, y+height, x, y+height, color)
|
101
|
-
line(x, y+height, x, y, color)
|
160
|
+
def line(x1:, y1:, x2:, y2:, color:current_color)
|
161
|
+
_line(x1, y1, x2, y2, color)
|
102
162
|
end
|
103
163
|
|
104
|
-
#
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
164
|
+
#
|
165
|
+
# RECTANGLE & SQUARE
|
166
|
+
#
|
167
|
+
def _rectangle(x1, y1, x2, y2, filled=false, color=current_color)
|
168
|
+
if filled
|
169
|
+
y1, y2 = y2, y1 if (y2 < y1)
|
170
|
+
(y1..y2).each { |y| _line(x1, y, x2, y, color) }
|
171
|
+
else
|
172
|
+
_line(x1, y1, x2, y1, color)
|
173
|
+
_line(x2, y1, x2, y2, color)
|
174
|
+
_line(x2, y2, x1, y2, color)
|
175
|
+
_line(x1, y2, x1, y1, color)
|
110
176
|
end
|
111
177
|
end
|
112
178
|
|
113
|
-
|
114
|
-
|
115
|
-
|
179
|
+
def rectangle(x1:nil, y1:nil, x2:nil, y2:nil, x:nil, y:nil, w:nil, h:nil, filled:false, color:current_color)
|
180
|
+
if (x || y || w || h) && (x1 || x2 || y1 || y2)
|
181
|
+
raise ArgumentError, "#rectangle accepts x:, y:, w:, h: OR x1:, y1:, x2:, y2: not a combination of both"
|
182
|
+
end
|
183
|
+
|
184
|
+
if (w && h)
|
185
|
+
x1 = x
|
186
|
+
x2 = x1 + w
|
187
|
+
x2 += 1 if (x2 < x1)
|
188
|
+
x2 -= 1 if (x1 < x2)
|
189
|
+
|
190
|
+
y1 = y
|
191
|
+
y2 = y1 + h
|
192
|
+
y2 += 1 if (y2 < y1)
|
193
|
+
y2 -= 1 if (y1 < y2)
|
194
|
+
end
|
195
|
+
|
196
|
+
_rectangle(x1, y1, x2, y2, filled, color)
|
197
|
+
end
|
198
|
+
|
199
|
+
def square(x:, y:, size:, filled:false, color:current_color)
|
200
|
+
rectangle(x: x, y: y, w: size, h: size, filled: filled, color: color)
|
201
|
+
end
|
202
|
+
|
203
|
+
#
|
204
|
+
# PATH, POLYGON & TRIANGLE
|
205
|
+
#
|
206
|
+
def _path(points, color=current_color)
|
116
207
|
start = points[0]
|
117
208
|
(1..points.length-1).each do |i|
|
118
209
|
finish = points[i]
|
119
|
-
|
210
|
+
_line(start[0], start[1], finish[0], finish[1], color)
|
120
211
|
start = finish
|
121
212
|
end
|
122
213
|
end
|
123
214
|
|
124
|
-
|
125
|
-
|
126
|
-
points << points[0]
|
127
|
-
path(points)
|
215
|
+
def path(points, color:current_color)
|
216
|
+
_path(points, color)
|
128
217
|
end
|
129
218
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
219
|
+
def _polygon(points, filled=false, color=current_color)
|
220
|
+
if filled
|
221
|
+
# Get all the X and Y coordinates from the points as floats.
|
222
|
+
coords_x = points.map { |point| point.first.to_f }
|
223
|
+
coords_y = points.map { |point| point.last.to_f }
|
224
|
+
|
225
|
+
# Get Y bounds of the polygon to limit rows.
|
226
|
+
y_min = coords_y.min.to_i
|
227
|
+
y_max = coords_y.max.to_i
|
228
|
+
|
229
|
+
# Cast horizontal ray on each row, storing nodes where it intersects polygon edges.
|
230
|
+
(y_min..y_max).each do |y|
|
231
|
+
nodes = []
|
232
|
+
i = 0
|
233
|
+
j = points.count - 1
|
234
|
+
|
235
|
+
while (i < points.count) do
|
236
|
+
# First condition excludes horizontal edges.
|
237
|
+
# Second and third check for +ve and -ve intersection respectively.
|
238
|
+
if (coords_y[i] != coords_y[j]) && ((coords_y[i] < y && coords_y[j] >= y) || (coords_y[j] < y && coords_y[i] >= y))
|
239
|
+
# Interoplate to find the intersection point (node).
|
240
|
+
nodes << (coords_x[i] + (y - coords_y[i]) / (coords_y[j] - coords_y[i]) * (coords_x[j] - coords_x[i])).round
|
241
|
+
end
|
242
|
+
j = i
|
243
|
+
i += 1
|
149
244
|
end
|
150
|
-
|
151
|
-
i += 1
|
152
|
-
end
|
153
|
-
nodes = nodes.sort
|
245
|
+
nodes = nodes.sort
|
154
246
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
247
|
+
# Take pairs of nodes and fill between them.
|
248
|
+
# This ignores the spaces between odd then even nodes (eg. 1->2), which are outside the polygon.
|
249
|
+
nodes.each_slice(2) do |pair|
|
250
|
+
_line(pair.first, y, pair.last, y, color) if pair.length == 2
|
251
|
+
end
|
160
252
|
end
|
161
253
|
end
|
162
254
|
|
163
|
-
# Stroke
|
164
|
-
|
255
|
+
# Stroke regardless, since floating point math misses thin areas of fill.
|
256
|
+
_path(points, color)
|
257
|
+
# Close open path by adding a line between the first and last points.
|
258
|
+
_line(points[-1][0], points[-1][1], points[0][0], points[0][1], color)
|
259
|
+
end
|
260
|
+
|
261
|
+
def polygon(points, filled:false, color:current_color)
|
262
|
+
_polygon(points, filled, color)
|
165
263
|
end
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
264
|
+
|
265
|
+
def _triangle(x1, y1, x2, y2, x3, y3, filled=false, color=current_color)
|
266
|
+
if filled
|
267
|
+
_polygon([[x1,y1], [x2,y2], [x3,y3]], filled, color)
|
268
|
+
else
|
269
|
+
_line(x1, y1, x2, y2, color)
|
270
|
+
_line(x2, y2, x3, y3, color)
|
271
|
+
_line(x3, y3, x1, y1, color)
|
272
|
+
end
|
170
273
|
end
|
171
274
|
|
172
|
-
|
173
|
-
|
174
|
-
filled_polygon([[x1,y1], [x2,y2], [x3,y3]], color)
|
275
|
+
def triangle(x1:, y1:, x2:, y2:, x3:, y3:, filled:false, color:current_color)
|
276
|
+
_triangle(x1, y1, x2, y2, x3, y3, filled, color)
|
175
277
|
end
|
176
278
|
|
177
|
-
|
178
|
-
|
279
|
+
def _ellipse(x_center, y_center, a, b, filled=false, color=current_color)
|
280
|
+
# Midpoint ellipse / circle based on Bresenham's circle algorithm.
|
179
281
|
# Start position
|
180
282
|
x = -a
|
181
283
|
y = 0
|
@@ -193,9 +295,15 @@ module Denko
|
|
193
295
|
# Since starting at max negative X, continue until x is 0.
|
194
296
|
while (x <= 0)
|
195
297
|
if filled
|
196
|
-
|
298
|
+
# Fill left and right quadrants in single line, alternating +ve and -ve y.
|
299
|
+
_line(x_center - x, y_center + y, x_center + x, y_center + y, color)
|
300
|
+
_line(x_center - x, y_center - y, x_center + x, y_center - y, color)
|
197
301
|
else
|
198
|
-
|
302
|
+
# Stroke quadrants in order as if y-axis is reversed and going counter-clockwise from +ve X.
|
303
|
+
_set_pixel(x_center - x, y_center - y, color)
|
304
|
+
_set_pixel(x_center + x, y_center - y, color)
|
305
|
+
_set_pixel(x_center + x, y_center + y, color)
|
306
|
+
_set_pixel(x_center - x, y_center + y, color)
|
199
307
|
end
|
200
308
|
|
201
309
|
e2 = 2 * e1
|
@@ -214,69 +322,154 @@ module Denko
|
|
214
322
|
# Continue if y hasn't reached the vertical size.
|
215
323
|
while (y < b)
|
216
324
|
y += 1
|
217
|
-
|
218
|
-
|
325
|
+
_set_pixel(x_center, y_center + y, color)
|
326
|
+
_set_pixel(x_center, y_center - y, color)
|
219
327
|
end
|
220
328
|
end
|
221
329
|
|
222
|
-
def
|
223
|
-
|
224
|
-
pixel(x_center - x, y_center - y, color)
|
225
|
-
pixel(x_center + x, y_center - y, color)
|
226
|
-
pixel(x_center + x, y_center + y, color)
|
227
|
-
pixel(x_center - x, y_center + y, color)
|
330
|
+
def ellipse(x:, y:, a:, b:, filled:false, color:current_color)
|
331
|
+
_ellipse(x, y, a, b, filled, color)
|
228
332
|
end
|
229
333
|
|
230
|
-
def
|
231
|
-
|
232
|
-
line(x_center - x, y_center - y, x_center + x, y_center - y, color)
|
334
|
+
def circle(x:, y:, r:, filled:false, color:current_color)
|
335
|
+
_ellipse(x, y, r, r, filled, color)
|
233
336
|
end
|
234
337
|
|
235
|
-
|
236
|
-
|
338
|
+
#
|
339
|
+
# BITMAP TEXT
|
340
|
+
#
|
341
|
+
def text(str, color:current_color)
|
342
|
+
str.to_s.split("").each { |char| draw_char(char, color: color) }
|
237
343
|
end
|
238
344
|
|
239
|
-
def
|
240
|
-
|
345
|
+
def draw_char(char, color:current_color)
|
346
|
+
# 0th character in font is SPACE. Offset ASCII code and show ? if character doesn't exist in font.
|
347
|
+
index = char.ord - 32
|
348
|
+
index = 31 if (index < 0 || index > @font_last_character)
|
349
|
+
char_map = @font_characters[index]
|
350
|
+
|
351
|
+
# Offset by scaled height, since bottom left of char starts at text cursor.
|
352
|
+
x = text_cursor[0]
|
353
|
+
y = text_cursor[1] + 1 - (@font_height * @font_scale)
|
354
|
+
|
355
|
+
# Draw it
|
356
|
+
_draw_char(char_map, x, y, @font_width, @font_scale, color)
|
357
|
+
|
358
|
+
# Increment the text cursor, scaling width.
|
359
|
+
self.text_cursor[0] += @font_width * @font_scale
|
241
360
|
end
|
242
361
|
|
243
|
-
def
|
244
|
-
|
362
|
+
def _draw_char(byte_array, x, y, width, scale, color=current_color)
|
363
|
+
byte_array.each_slice(width) do |slice|
|
364
|
+
slice.each_with_index do |byte, col_offset|
|
365
|
+
8.times do |bit|
|
366
|
+
# Don't do anything if this bit isn't set in the font.
|
367
|
+
if (((byte >> bit) & 0b1) == 1)
|
368
|
+
scale.times do |x_offset|
|
369
|
+
scale.times do |y_offset|
|
370
|
+
_set_pixel(x + (col_offset * scale) + x_offset, y + (bit * scale) + y_offset, color)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
y = y + (8 * scale)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
#
|
381
|
+
# DRAWING STATE
|
382
|
+
#
|
383
|
+
def current_color=(color)
|
384
|
+
raise Argument error, "color must be within (0..#{colors})" if (color < 0) || (color > colors)
|
385
|
+
@current_color = color
|
245
386
|
end
|
246
387
|
|
388
|
+
attr_reader :current_color
|
389
|
+
|
390
|
+
DEFAULT_TEXT_CURSOR = [0, 7]
|
391
|
+
|
247
392
|
def text_cursor
|
248
|
-
@text_cursor ||=
|
393
|
+
@text_cursor ||= DEFAULT_TEXT_CURSOR
|
249
394
|
end
|
250
395
|
|
251
|
-
def
|
252
|
-
|
253
|
-
print_char(char)
|
254
|
-
end
|
396
|
+
def text_cursor=(array=DEFAULT_TEXT_CURSOR)
|
397
|
+
@text_cursor = array
|
255
398
|
end
|
256
399
|
|
257
|
-
def
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
400
|
+
def font=(font)
|
401
|
+
if font.class == Symbol
|
402
|
+
@font = Display::Font.const_get(font.to_s.upcase)
|
403
|
+
else
|
404
|
+
@font = font
|
405
|
+
end
|
262
406
|
|
263
|
-
|
407
|
+
@font_height = @font[:height]
|
408
|
+
@font_width = @font[:width]
|
409
|
+
@font_characters = @font[:characters]
|
410
|
+
@font_last_character = @font_characters.length - 1
|
264
411
|
end
|
265
412
|
|
266
|
-
def
|
267
|
-
#
|
268
|
-
|
269
|
-
|
413
|
+
def font_scale=(scale)
|
414
|
+
raise ArgumentError, "#font_scale must be a positive integer" unless (scale.class == Integer) && scale > 0
|
415
|
+
@font_scale = scale
|
416
|
+
end
|
270
417
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
418
|
+
attr_reader :font, :font_scale
|
419
|
+
|
420
|
+
#
|
421
|
+
# TRANSFORMATION
|
422
|
+
#
|
423
|
+
def rotate(degrees=180)
|
424
|
+
raise ArgumentError, "Canvas can only be rotated in multiples of 90 degrees" unless (degrees % 90 == 0)
|
425
|
+
old_rotation = @rotation
|
426
|
+
@rotation = (old_rotation + degrees) % 360
|
427
|
+
change = (@rotation - old_rotation) % 360
|
428
|
+
|
429
|
+
(change / 90).times do
|
430
|
+
@swap_xy = !@swap_xy
|
431
|
+
if (!@invert_x && !@invert_y)
|
432
|
+
@invert_x = true
|
433
|
+
@invert_y = false
|
434
|
+
elsif (@invert_x && !@invert_y)
|
435
|
+
@invert_x = true
|
436
|
+
@invert_y = true
|
437
|
+
elsif (@invert_x && @invert_y)
|
438
|
+
@invert_x = false
|
439
|
+
@invert_y = true
|
440
|
+
elsif (!@invert_x && @invert_y)
|
441
|
+
@invert_x = false
|
442
|
+
@invert_y = false
|
443
|
+
end
|
275
444
|
end
|
445
|
+
calculate_bounds
|
446
|
+
end
|
276
447
|
|
277
|
-
|
278
|
-
|
448
|
+
def reflect(axis)
|
449
|
+
raise ArgumentError "Canvas can only be reflected in :x or :y axis" unless [:x, :y].include? (axis)
|
450
|
+
(axis == :x) ? @invert_x = !@invert_x : @invert_y = !@invert_y
|
451
|
+
calculate_bounds
|
279
452
|
end
|
453
|
+
|
454
|
+
def reflect_x
|
455
|
+
reflect(:x)
|
456
|
+
end
|
457
|
+
|
458
|
+
def reflect_y
|
459
|
+
reflect(:y)
|
460
|
+
end
|
461
|
+
|
462
|
+
def calculate_bounds
|
463
|
+
if @swap_xy
|
464
|
+
@x_max = @rows - 1
|
465
|
+
@y_max = @columns - 1
|
466
|
+
else
|
467
|
+
@x_max = @columns - 1
|
468
|
+
@y_max = @rows - 1
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
attr_reader :x_max, :y_max
|
280
473
|
end
|
281
474
|
end
|
282
475
|
end
|