introduce tags, structs, and reticles
this was meant to be split into smaller changes, but I realised I edited my existing revision too late.
This commit is contained in:
		
							parent
							
								
									8356b6c750
								
							
						
					
					
						commit
						5b7d9586ea
					
				
					 26 changed files with 1113 additions and 351 deletions
				
			
		| 
						 | 
				
			
			@ -7,19 +7,20 @@ use core::{alloc::Layout, slice};
 | 
			
		|||
use alloc::{boxed::Box, vec::Vec};
 | 
			
		||||
use haku::{
 | 
			
		||||
    ast::Ast,
 | 
			
		||||
    bytecode::{Chunk, Defs, DefsImage},
 | 
			
		||||
    bytecode::{Chunk, Defs, DefsImage, DefsLimits},
 | 
			
		||||
    compiler::{compile_expr, ClosureSpec, CompileError, Compiler, Source},
 | 
			
		||||
    diagnostic::Diagnostic,
 | 
			
		||||
    lexer::{lex, Lexer},
 | 
			
		||||
    parser::{self, IntoAstError, Parser},
 | 
			
		||||
    render::{
 | 
			
		||||
        tiny_skia::{Pixmap, PremultipliedColorU8},
 | 
			
		||||
        Renderer, RendererLimits,
 | 
			
		||||
        RendererLimits,
 | 
			
		||||
    },
 | 
			
		||||
    source::SourceCode,
 | 
			
		||||
    system::{ChunkId, System, SystemImage},
 | 
			
		||||
    token::Lexis,
 | 
			
		||||
    value::{Closure, Ref, Value},
 | 
			
		||||
    trampoline::{Cont, Trampoline},
 | 
			
		||||
    value::{Closure, Ref, Vec2},
 | 
			
		||||
    vm::{Exception, Vm, VmImage, VmLimits},
 | 
			
		||||
};
 | 
			
		||||
use log::{debug, info};
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +47,7 @@ struct Limits {
 | 
			
		|||
    max_source_code_len: usize,
 | 
			
		||||
    max_chunks: usize,
 | 
			
		||||
    max_defs: usize,
 | 
			
		||||
    max_tags: usize,
 | 
			
		||||
    max_tokens: usize,
 | 
			
		||||
    max_parser_events: usize,
 | 
			
		||||
    ast_capacity: usize,
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +67,7 @@ impl Default for Limits {
 | 
			
		|||
            max_source_code_len: 65536,
 | 
			
		||||
            max_chunks: 2,
 | 
			
		||||
            max_defs: 256,
 | 
			
		||||
            max_tags: 256,
 | 
			
		||||
            max_tokens: 1024,
 | 
			
		||||
            max_parser_events: 1024,
 | 
			
		||||
            ast_capacity: 1024,
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +113,7 @@ macro_rules! limit_setter {
 | 
			
		|||
limit_setter!(max_source_code_len);
 | 
			
		||||
limit_setter!(max_chunks);
 | 
			
		||||
limit_setter!(max_defs);
 | 
			
		||||
limit_setter!(max_tags);
 | 
			
		||||
limit_setter!(max_tokens);
 | 
			
		||||
limit_setter!(max_parser_events);
 | 
			
		||||
limit_setter!(ast_capacity);
 | 
			
		||||
| 
						 | 
				
			
			@ -133,10 +137,22 @@ struct Instance {
 | 
			
		|||
    vm: Vm,
 | 
			
		||||
    vm_image: VmImage,
 | 
			
		||||
 | 
			
		||||
    value: Value,
 | 
			
		||||
    trampoline: Option<Trampoline>,
 | 
			
		||||
    exception: Option<Exception>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Instance {
 | 
			
		||||
    fn set_exception(&mut self, exn: Exception) {
 | 
			
		||||
        debug!("setting exception = {exn:?}");
 | 
			
		||||
        self.exception = Some(exn);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn reset_exception(&mut self) {
 | 
			
		||||
        debug!("resetting exception");
 | 
			
		||||
        self.exception = None;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance {
 | 
			
		||||
    let limits = *limits;
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +160,10 @@ unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance {
 | 
			
		|||
 | 
			
		||||
    let system = System::new(limits.max_chunks);
 | 
			
		||||
 | 
			
		||||
    let defs = Defs::new(limits.max_defs);
 | 
			
		||||
    let defs = Defs::new(&DefsLimits {
 | 
			
		||||
        max_defs: limits.max_defs,
 | 
			
		||||
        max_tags: limits.max_tags,
 | 
			
		||||
    });
 | 
			
		||||
    let vm = Vm::new(
 | 
			
		||||
        &defs,
 | 
			
		||||
        &VmLimits {
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +187,7 @@ unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance {
 | 
			
		|||
        defs_image,
 | 
			
		||||
        vm,
 | 
			
		||||
        vm_image,
 | 
			
		||||
        value: Value::Nil,
 | 
			
		||||
        trampoline: None,
 | 
			
		||||
        exception: None,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -191,13 +210,6 @@ unsafe extern "C" fn haku_reset(instance: *mut Instance) {
 | 
			
		|||
    instance.defs.restore_image(&instance.defs_image);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_reset_vm(instance: *mut Instance) {
 | 
			
		||||
    debug!("resetting instance VM: {instance:?}");
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    instance.vm.restore_image(&instance.vm_image);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool {
 | 
			
		||||
    (*instance).exception.is_some()
 | 
			
		||||
| 
						 | 
				
			
			@ -426,39 +438,35 @@ unsafe extern "C" fn haku_compile_brush(
 | 
			
		|||
    StatusCode::Ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PixmapLock {
 | 
			
		||||
    pixmap: Pixmap,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapLock {
 | 
			
		||||
    let ptr = Box::leak(Box::new(PixmapLock {
 | 
			
		||||
        pixmap: Pixmap::new(width, height).expect("invalid pixmap size"),
 | 
			
		||||
    })) as *mut _;
 | 
			
		||||
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut Pixmap {
 | 
			
		||||
    let ptr = Box::leak(Box::new(
 | 
			
		||||
        Pixmap::new(width, height).expect("invalid pixmap size"),
 | 
			
		||||
    )) as *mut _;
 | 
			
		||||
    debug!("created pixmap with size {width}x{height}: {ptr:?}");
 | 
			
		||||
    ptr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_destroy(pixmap: *mut PixmapLock) {
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_destroy(pixmap: *mut Pixmap) {
 | 
			
		||||
    debug!("destroying pixmap: {pixmap:?}");
 | 
			
		||||
    drop(Box::from_raw(pixmap))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_data(pixmap: *mut PixmapLock) -> *mut u8 {
 | 
			
		||||
    let pixmap = &mut (*pixmap).pixmap;
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_data(pixmap: *mut Pixmap) -> *mut u8 {
 | 
			
		||||
    let pixmap = &mut *pixmap;
 | 
			
		||||
    pixmap.pixels_mut().as_mut_ptr() as *mut u8
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut PixmapLock) {
 | 
			
		||||
    let pixmap = &mut (*pixmap).pixmap;
 | 
			
		||||
unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut Pixmap) {
 | 
			
		||||
    let pixmap = &mut *pixmap;
 | 
			
		||||
    pixmap.pixels_mut().fill(PremultipliedColorU8::TRANSPARENT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brush) -> StatusCode {
 | 
			
		||||
unsafe extern "C" fn haku_begin_brush(instance: *mut Instance, brush: *const Brush) -> StatusCode {
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    let brush = &*brush;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -466,8 +474,10 @@ unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brus
 | 
			
		|||
        panic!("brush is not compiled and ready to be used");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    debug!("applying defs");
 | 
			
		||||
    instance.vm.restore_image(&instance.vm_image);
 | 
			
		||||
    instance.vm.apply_defs(&instance.defs);
 | 
			
		||||
    instance.reset_exception();
 | 
			
		||||
    instance.trampoline = None;
 | 
			
		||||
 | 
			
		||||
    let Ok(closure_id) = instance
 | 
			
		||||
        .vm
 | 
			
		||||
| 
						 | 
				
			
			@ -476,51 +486,91 @@ unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brus
 | 
			
		|||
        return StatusCode::OutOfRefSlots;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    debug!("resetting exception");
 | 
			
		||||
    instance.exception = None;
 | 
			
		||||
    instance.value = match instance.vm.run(&instance.system, closure_id) {
 | 
			
		||||
    instance.reset_exception();
 | 
			
		||||
    let value = match instance.vm.run(&instance.system, closure_id, &[]) {
 | 
			
		||||
        Ok(value) => value,
 | 
			
		||||
        Err(exn) => {
 | 
			
		||||
            debug!("setting exception {exn:?}");
 | 
			
		||||
            instance.exception = Some(exn);
 | 
			
		||||
            instance.set_exception(exn);
 | 
			
		||||
            return StatusCode::EvalException;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    instance.trampoline = Some(Trampoline::new(value));
 | 
			
		||||
 | 
			
		||||
    StatusCode::Ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_render_value(
 | 
			
		||||
unsafe extern "C" fn haku_cont_kind(instance: *mut Instance) -> Cont {
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    instance.trampoline.as_ref().unwrap().cont(&instance.vm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn wrap_exception(
 | 
			
		||||
    instance: &mut Instance,
 | 
			
		||||
    error_code: StatusCode,
 | 
			
		||||
    f: impl FnOnce(&mut Instance) -> Result<(), Exception>,
 | 
			
		||||
) -> StatusCode {
 | 
			
		||||
    match f(instance) {
 | 
			
		||||
        Ok(_) => StatusCode::Ok,
 | 
			
		||||
        Err(exn) => {
 | 
			
		||||
            instance.set_exception(exn);
 | 
			
		||||
            error_code
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_cont_scribble(
 | 
			
		||||
    instance: *mut Instance,
 | 
			
		||||
    pixmap: *mut PixmapLock,
 | 
			
		||||
    pixmap: *mut Pixmap,
 | 
			
		||||
    translation_x: f32,
 | 
			
		||||
    translation_y: f32,
 | 
			
		||||
) -> StatusCode {
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    debug!("resetting exception");
 | 
			
		||||
    instance.exception = None;
 | 
			
		||||
    instance.reset_exception();
 | 
			
		||||
 | 
			
		||||
    debug!("will render value: {:?}", instance.value);
 | 
			
		||||
    debug!("cont_scribble: pixmap={pixmap:?} translation_x={translation_x:?} translation_y={translation_y:?} trampoline={:?}", instance.trampoline);
 | 
			
		||||
 | 
			
		||||
    let pixmap_locked = &mut (*pixmap).pixmap;
 | 
			
		||||
 | 
			
		||||
    let mut renderer = Renderer::new(
 | 
			
		||||
        pixmap_locked,
 | 
			
		||||
        &RendererLimits {
 | 
			
		||||
            pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
 | 
			
		||||
            transform_stack_capacity: instance.limits.transform_stack_capacity,
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
    renderer.translate(translation_x, translation_y);
 | 
			
		||||
    match renderer.render(&instance.vm, instance.value) {
 | 
			
		||||
        Ok(()) => (),
 | 
			
		||||
        Err(exn) => {
 | 
			
		||||
            instance.exception = Some(exn);
 | 
			
		||||
            instance.vm.restore_image(&instance.vm_image);
 | 
			
		||||
            return StatusCode::RenderException;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    StatusCode::Ok
 | 
			
		||||
    wrap_exception(instance, StatusCode::RenderException, |instance| {
 | 
			
		||||
        instance.trampoline.as_mut().unwrap().scribble(
 | 
			
		||||
            &instance.vm,
 | 
			
		||||
            &mut *pixmap,
 | 
			
		||||
            Vec2 {
 | 
			
		||||
                x: translation_x,
 | 
			
		||||
                y: translation_y,
 | 
			
		||||
            },
 | 
			
		||||
            &RendererLimits {
 | 
			
		||||
                pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
 | 
			
		||||
                transform_stack_capacity: instance.limits.transform_stack_capacity,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
unsafe extern "C" fn haku_cont_dotter(
 | 
			
		||||
    instance: *mut Instance,
 | 
			
		||||
    from_x: f32,
 | 
			
		||||
    from_y: f32,
 | 
			
		||||
    to_x: f32,
 | 
			
		||||
    to_y: f32,
 | 
			
		||||
    num: f32,
 | 
			
		||||
) -> StatusCode {
 | 
			
		||||
    let instance = &mut *instance;
 | 
			
		||||
    instance.reset_exception();
 | 
			
		||||
 | 
			
		||||
    debug!(
 | 
			
		||||
        "cont_dotter: from_x={from_x} from_y={from_y} to_x={to_x} to_y={to_y} trampoline={:?}",
 | 
			
		||||
        instance.trampoline
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    wrap_exception(instance, StatusCode::RenderException, |instance| {
 | 
			
		||||
        instance.trampoline.as_mut().unwrap().dotter(
 | 
			
		||||
            &mut instance.vm,
 | 
			
		||||
            &instance.system,
 | 
			
		||||
            Vec2::new(from_x, from_y),
 | 
			
		||||
            Vec2::new(to_x, to_y),
 | 
			
		||||
            num,
 | 
			
		||||
        )
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ pub enum Opcode {
 | 
			
		|||
    Nil,
 | 
			
		||||
    False,
 | 
			
		||||
    True,
 | 
			
		||||
    Tag,
 | 
			
		||||
    Number, // (float: f32)
 | 
			
		||||
    Rgba,   // (r: u8, g: u8, b: u8, a: u8)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +37,7 @@ pub enum Opcode {
 | 
			
		|||
    // Control flow.
 | 
			
		||||
    Jump,      // (offset: u16)
 | 
			
		||||
    JumpIfNot, // (offset: u16)
 | 
			
		||||
    Field,     // (count: u8, tags: [u16; count])
 | 
			
		||||
 | 
			
		||||
    // Function calls.
 | 
			
		||||
    Call, // (argc: u8)
 | 
			
		||||
| 
						 | 
				
			
			@ -157,6 +159,12 @@ impl Chunk {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Offset {
 | 
			
		||||
    pub fn to_u16(self) -> u16 {
 | 
			
		||||
        self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
pub struct ChunkSizeError;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -193,21 +201,48 @@ impl DefId {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
 | 
			
		||||
pub struct TagId(u16);
 | 
			
		||||
 | 
			
		||||
impl TagId {
 | 
			
		||||
    pub(crate) fn from_u16(x: u16) -> Self {
 | 
			
		||||
        Self(x)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_u16(self) -> u16 {
 | 
			
		||||
        self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct Defs {
 | 
			
		||||
    defs: Vec<String>,
 | 
			
		||||
    tags: Vec<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
pub struct DefsLimits {
 | 
			
		||||
    pub max_defs: usize,
 | 
			
		||||
    pub max_tags: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
pub struct DefsImage {
 | 
			
		||||
    defs: usize,
 | 
			
		||||
    tags: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Defs {
 | 
			
		||||
    pub fn new(capacity: usize) -> Self {
 | 
			
		||||
        assert!(capacity < u16::MAX as usize + 1);
 | 
			
		||||
    pub fn new(limits: &DefsLimits) -> Self {
 | 
			
		||||
        assert!(limits.max_defs < u16::MAX as usize + 1);
 | 
			
		||||
        assert!(limits.max_tags < u16::MAX as usize + 1);
 | 
			
		||||
 | 
			
		||||
        let mut tags = Vec::with_capacity(limits.max_tags);
 | 
			
		||||
        add_well_known_tags(&mut tags);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            defs: Vec::with_capacity(capacity),
 | 
			
		||||
            defs: Vec::with_capacity(limits.max_defs),
 | 
			
		||||
            tags,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -219,14 +254,14 @@ impl Defs {
 | 
			
		|||
        self.len() != 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&mut self, name: &str) -> Option<DefId> {
 | 
			
		||||
    pub fn get_def(&mut self, name: &str) -> Option<DefId> {
 | 
			
		||||
        self.defs
 | 
			
		||||
            .iter()
 | 
			
		||||
            .position(|n| *n == name)
 | 
			
		||||
            .map(|index| DefId(index as u16))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add(&mut self, name: &str) -> Result<DefId, DefError> {
 | 
			
		||||
    pub fn add_def(&mut self, name: &str) -> Result<DefId, DefError> {
 | 
			
		||||
        if self.defs.iter().any(|n| n == name) {
 | 
			
		||||
            Err(DefError::Exists)
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -239,9 +274,27 @@ impl Defs {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_tag(tags: &mut Vec<String>, name: &str) -> Result<TagId, TagError> {
 | 
			
		||||
        if tags.len() >= tags.capacity() {
 | 
			
		||||
            return Err(TagError::OutOfSpace);
 | 
			
		||||
        }
 | 
			
		||||
        let id = TagId(tags.len() as u16);
 | 
			
		||||
        tags.push(name.to_owned());
 | 
			
		||||
        Ok(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_or_add_tag(&mut self, name: &str) -> Result<TagId, TagError> {
 | 
			
		||||
        if let Some(index) = self.tags.iter().position(|n| n == name) {
 | 
			
		||||
            Ok(TagId(index as u16))
 | 
			
		||||
        } else {
 | 
			
		||||
            Self::add_tag(&mut self.tags, name)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn image(&self) -> DefsImage {
 | 
			
		||||
        DefsImage {
 | 
			
		||||
            defs: self.defs.len(),
 | 
			
		||||
            tags: self.tags.len(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -249,6 +302,9 @@ impl Defs {
 | 
			
		|||
        self.defs.resize_with(image.defs, || {
 | 
			
		||||
            panic!("image must be a subset of the current defs")
 | 
			
		||||
        });
 | 
			
		||||
        self.tags.resize_with(image.tags, || {
 | 
			
		||||
            panic!("image must be a subset of the current defs")
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -266,3 +322,45 @@ impl Display for DefError {
 | 
			
		|||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
pub enum TagError {
 | 
			
		||||
    OutOfSpace,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for TagError {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        f.write_str(match self {
 | 
			
		||||
            TagError::OutOfSpace => "too many tags",
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
macro_rules! well_known_tags {
 | 
			
		||||
    ($($ident:tt = $value:tt),* $(,)?) => {
 | 
			
		||||
        impl TagId {
 | 
			
		||||
            $(
 | 
			
		||||
                #[allow(non_upper_case_globals)]
 | 
			
		||||
                pub const $ident: Self = Self($value);
 | 
			
		||||
            )*
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fn add_well_known_tags(tags: &mut Vec<String>) {
 | 
			
		||||
            $(
 | 
			
		||||
                let id = Defs::add_tag(tags, stringify!($ident)).unwrap();
 | 
			
		||||
                assert_eq!(id, TagId::from_u16($value));
 | 
			
		||||
            )*
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
well_known_tags! {
 | 
			
		||||
    // NOTE: The numbers must be sorted from 0 to N, due to limitations of Rust's macro system.
 | 
			
		||||
    // https://github.com/rust-lang/rust/issues/83527
 | 
			
		||||
 | 
			
		||||
    Nil = 0,
 | 
			
		||||
 | 
			
		||||
    From = 1,
 | 
			
		||||
    To = 2,
 | 
			
		||||
    Num = 3,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,9 @@ use alloc::vec::Vec;
 | 
			
		|||
 | 
			
		||||
use crate::{
 | 
			
		||||
    ast::{Ast, NodeId, NodeKind},
 | 
			
		||||
    bytecode::{Chunk, DefError, Defs, EmitError, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
 | 
			
		||||
    bytecode::{
 | 
			
		||||
        Chunk, DefError, Defs, EmitError, Opcode, TagError, TagId, CAPTURE_CAPTURE, CAPTURE_LOCAL,
 | 
			
		||||
    },
 | 
			
		||||
    diagnostic::Diagnostic,
 | 
			
		||||
    source::SourceCode,
 | 
			
		||||
    system::{System, SystemFnArity},
 | 
			
		||||
| 
						 | 
				
			
			@ -123,11 +125,6 @@ pub fn compile_expr<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId)
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn unsupported(c: &mut Compiler, src: &Source, node_id: NodeId, message: &str) -> CompileResult {
 | 
			
		||||
    c.emit(Diagnostic::error(src.ast.span(node_id), message));
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn compile_nil(c: &mut Compiler) -> CompileResult {
 | 
			
		||||
    c.chunk.emit_opcode(Opcode::Nil)?;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +180,7 @@ fn compile_ident<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) ->
 | 
			
		|||
            c.chunk.emit_u8(index)?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(None) => {
 | 
			
		||||
            if let Some(def_id) = c.defs.get(name) {
 | 
			
		||||
            if let Some(def_id) = c.defs.get_def(name) {
 | 
			
		||||
                c.chunk.emit_opcode(Opcode::Def)?;
 | 
			
		||||
                c.chunk.emit_u16(def_id.to_u16())?;
 | 
			
		||||
            } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +199,8 @@ fn compile_ident<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) ->
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
fn compile_tag(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult {
 | 
			
		||||
    let tag = src.ast.span(node_id).slice(src.code);
 | 
			
		||||
    let span = src.ast.span(node_id);
 | 
			
		||||
    let tag = span.slice(src.code);
 | 
			
		||||
 | 
			
		||||
    match tag {
 | 
			
		||||
        "False" => {
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +210,17 @@ fn compile_tag(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult
 | 
			
		|||
            c.chunk.emit_opcode(Opcode::True)?;
 | 
			
		||||
        }
 | 
			
		||||
        _ => {
 | 
			
		||||
            c.emit(Diagnostic::error(src.ast.span(node_id), "uppercased identifiers are reserved for future use; please start your identifiers with a lowercase letter instead"));
 | 
			
		||||
            let tag_id = c.defs.get_or_add_tag(tag).unwrap_or_else(|error| {
 | 
			
		||||
                match error {
 | 
			
		||||
                    TagError::OutOfSpace => {
 | 
			
		||||
                        c.emit(Diagnostic::error(span, "too many unique tags used"));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                TagId::Nil
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            c.chunk.emit_opcode(Opcode::Tag)?;
 | 
			
		||||
            c.chunk.emit_u16(tag_id.to_u16())?;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -598,7 +606,7 @@ fn def_prepass<'a>(c: &mut Compiler<'a>, src: &Source<'a>, toplevel: NodeId) ->
 | 
			
		|||
        if let (Some(ident), Some(op)) = (binary_walk.node(), binary_walk.get(NodeKind::Op)) {
 | 
			
		||||
            if src.ast.span(op).slice(src.code) == "=" {
 | 
			
		||||
                let name = src.ast.span(ident).slice(src.code);
 | 
			
		||||
                match c.defs.add(name) {
 | 
			
		||||
                match c.defs.add_def(name) {
 | 
			
		||||
                    Ok(_) => (),
 | 
			
		||||
                    Err(DefError::Exists) => c.emit(Diagnostic::error(
 | 
			
		||||
                        src.ast.span(ident),
 | 
			
		||||
| 
						 | 
				
			
			@ -651,7 +659,12 @@ fn compile_def<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> C
 | 
			
		|||
        return Ok(());
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if src.ast.kind(left) != NodeKind::Ident {
 | 
			
		||||
    if src.ast.kind(left) == NodeKind::Tag {
 | 
			
		||||
        c.emit(Diagnostic::error(
 | 
			
		||||
            src.ast.span(left),
 | 
			
		||||
            "defs may not be named with uppercase letters, because uppercase letters are used for tags",
 | 
			
		||||
        ));
 | 
			
		||||
    } else if src.ast.kind(left) != NodeKind::Ident {
 | 
			
		||||
        c.emit(Diagnostic::error(
 | 
			
		||||
            src.ast.span(left),
 | 
			
		||||
            "def name (identifier) expected",
 | 
			
		||||
| 
						 | 
				
			
			@ -662,7 +675,7 @@ fn compile_def<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> C
 | 
			
		|||
    // NOTE: def_prepass collects all definitions beforehand.
 | 
			
		||||
    // In case a def ends up not existing, that means we ran out of space for defs - so emit a
 | 
			
		||||
    // zero def instead.
 | 
			
		||||
    let def_id = c.defs.get(name).unwrap_or_default();
 | 
			
		||||
    let def_id = c.defs.get_def(name).unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
    compile_expr(c, src, right)?;
 | 
			
		||||
    c.chunk.emit_opcode(Opcode::SetDef)?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,5 +12,6 @@ pub mod render;
 | 
			
		|||
pub mod source;
 | 
			
		||||
pub mod system;
 | 
			
		||||
pub mod token;
 | 
			
		||||
pub mod trampoline;
 | 
			
		||||
pub mod value;
 | 
			
		||||
pub mod vm;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,7 +127,7 @@ impl<'a> Renderer<'a> {
 | 
			
		|||
            &paint,
 | 
			
		||||
            &SStroke {
 | 
			
		||||
                width: stroke.thickness,
 | 
			
		||||
                line_cap: LineCap::Square,
 | 
			
		||||
                line_cap: LineCap::Round,
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            },
 | 
			
		||||
            transform,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,8 @@ use core::{
 | 
			
		|||
use alloc::vec::Vec;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    bytecode::Chunk,
 | 
			
		||||
    value::Value,
 | 
			
		||||
    bytecode::{Chunk, EmitError, Offset, Opcode, TagId},
 | 
			
		||||
    value::{BytecodeLoc, Closure, FunctionName, Value, Vec4},
 | 
			
		||||
    vm::{Exception, FnArgs, Vm},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +29,7 @@ pub struct System {
 | 
			
		|||
    pub resolve_fn: fn(SystemFnArity, &str) -> Option<u8>,
 | 
			
		||||
    pub fns: [Option<SystemFn>; 256],
 | 
			
		||||
    pub chunks: Vec<Chunk>,
 | 
			
		||||
    structs_chunk_offsets: StructsChunkOffsets,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
| 
						 | 
				
			
			@ -36,33 +37,28 @@ pub struct SystemImage {
 | 
			
		|||
    chunks: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
macro_rules! def_fns {
 | 
			
		||||
    ($($index:tt $arity:tt $name:tt => $fnref:expr),* $(,)?) => {
 | 
			
		||||
        pub(crate) fn init_fns(system: &mut System) {
 | 
			
		||||
            $(
 | 
			
		||||
                debug_assert!(system.fns[$index].is_none());
 | 
			
		||||
                system.fns[$index] = Some($fnref);
 | 
			
		||||
            )*
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        pub(crate) fn resolve(arity: SystemFnArity, name: &str) -> Option<u8> {
 | 
			
		||||
            match (arity, name){
 | 
			
		||||
                $((SystemFnArity::$arity, $name) => Some($index),)*
 | 
			
		||||
                _ => None,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct StructsChunkOffsets {
 | 
			
		||||
    dotter: BytecodeLoc,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl System {
 | 
			
		||||
    pub fn new(max_chunks: usize) -> Self {
 | 
			
		||||
        assert!(
 | 
			
		||||
            max_chunks > 1,
 | 
			
		||||
            "the 0th chunk is allocated for internal purposes; therefore there must be more than one chunk to execute bytecode"
 | 
			
		||||
        );
 | 
			
		||||
        assert!(max_chunks < u32::MAX as usize);
 | 
			
		||||
 | 
			
		||||
        let (structs_chunk, structs_chunk_offsets) = Self::structs_chunk().unwrap();
 | 
			
		||||
 | 
			
		||||
        let mut system = Self {
 | 
			
		||||
            resolve_fn: Self::resolve,
 | 
			
		||||
            fns: [None; 256],
 | 
			
		||||
            chunks: Vec::with_capacity(max_chunks),
 | 
			
		||||
            structs_chunk_offsets,
 | 
			
		||||
        };
 | 
			
		||||
        system.chunks.push(structs_chunk);
 | 
			
		||||
        Self::init_fns(&mut system);
 | 
			
		||||
        system
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +88,64 @@ impl System {
 | 
			
		|||
            panic!("image must be a subset of the current system")
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The structs chunk contains bytecode for _struct functions_.
 | 
			
		||||
    //
 | 
			
		||||
    // Struct functions are a way of encoding structures with arbitrary named data fields.
 | 
			
		||||
    // They are called like regular functions, except they always expect a single tag-type
 | 
			
		||||
    // argument. They're used where performance is not a primary concern---in structures that appear
 | 
			
		||||
    // once or a couple times throughout the lifetime of a program, in which convenient,
 | 
			
		||||
    // backwards-compatible field access is a priority.
 | 
			
		||||
    //
 | 
			
		||||
    // Each struct function has only two opcodes: `Field`, followed by a `Return`.
 | 
			
		||||
    // The `Field` opcode is an efficient way of encoding an if chain made solely out of tag
 | 
			
		||||
    // comparisons, returning a closure capture (or error if there's no field with the given name.)
 | 
			
		||||
    //
 | 
			
		||||
    //    if (tag == A)      capture_0
 | 
			
		||||
    //    else if (tag == B) capture_1
 | 
			
		||||
    //    else if (tag == C) capture_2
 | 
			
		||||
    //    else               error
 | 
			
		||||
    //
 | 
			
		||||
    // Closure captures are used here, because they're a convenient way of attaching indexed data
 | 
			
		||||
    // to any function, even created outside the language itself.
 | 
			
		||||
    //
 | 
			
		||||
    // All of this results in a function that can be called like `d From` to obtain a piece of data
 | 
			
		||||
    // stored inside of the `d` structure.
 | 
			
		||||
    fn structs_chunk() -> Result<(Chunk, StructsChunkOffsets), EmitError> {
 | 
			
		||||
        let mut chunk = Chunk::new(128).unwrap();
 | 
			
		||||
 | 
			
		||||
        let dotter = chunk.offset();
 | 
			
		||||
        chunk.emit_opcode(Opcode::Field)?;
 | 
			
		||||
        chunk.emit_u8(3)?;
 | 
			
		||||
        chunk.emit_u16(TagId::From.to_u16())?;
 | 
			
		||||
        chunk.emit_u16(TagId::To.to_u16())?;
 | 
			
		||||
        chunk.emit_u16(TagId::Num.to_u16())?;
 | 
			
		||||
        chunk.emit_opcode(Opcode::Return)?;
 | 
			
		||||
 | 
			
		||||
        fn loc(offset: Offset) -> BytecodeLoc {
 | 
			
		||||
            BytecodeLoc {
 | 
			
		||||
                chunk_id: ChunkId(0),
 | 
			
		||||
                offset: offset.to_u16(),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok((
 | 
			
		||||
            chunk,
 | 
			
		||||
            StructsChunkOffsets {
 | 
			
		||||
                dotter: loc(dotter),
 | 
			
		||||
            },
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn create_dotter(&self, from: Vec4, to: Vec4, num: f32) -> Closure {
 | 
			
		||||
        Closure {
 | 
			
		||||
            start: self.structs_chunk_offsets.dotter,
 | 
			
		||||
            name: FunctionName::Anonymous,
 | 
			
		||||
            param_count: 1,
 | 
			
		||||
            local_count: 0,
 | 
			
		||||
            captures: Vec::from_iter([Value::Vec4(from), Value::Vec4(to), Value::Number(num)]),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
| 
						 | 
				
			
			@ -105,11 +159,29 @@ impl Display for ChunkError {
 | 
			
		|||
 | 
			
		||||
impl Error for ChunkError {}
 | 
			
		||||
 | 
			
		||||
macro_rules! def_fns {
 | 
			
		||||
    ($($index:tt $arity:tt $name:tt => $fnref:expr),* $(,)?) => {
 | 
			
		||||
        pub(crate) fn init_fns(system: &mut System) {
 | 
			
		||||
            $(
 | 
			
		||||
                debug_assert!(system.fns[$index].is_none());
 | 
			
		||||
                system.fns[$index] = Some($fnref);
 | 
			
		||||
            )*
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        pub(crate) fn resolve(arity: SystemFnArity, name: &str) -> Option<u8> {
 | 
			
		||||
            match (arity, name){
 | 
			
		||||
                $((SystemFnArity::$arity, $name) => Some($index),)*
 | 
			
		||||
                _ => None,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub mod fns {
 | 
			
		||||
    use alloc::{format, vec::Vec};
 | 
			
		||||
 | 
			
		||||
    use crate::{
 | 
			
		||||
        value::{Fill, List, Ref, Rgba, Scribble, Shape, Stroke, Value, Vec2, Vec4},
 | 
			
		||||
        value::{Fill, List, Ref, Reticle, Rgba, Scribble, Shape, Stroke, Value, Vec2, Vec4},
 | 
			
		||||
        vm::{Exception, FnArgs, Vm},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -181,8 +253,11 @@ pub mod fns {
 | 
			
		|||
            0xc1 Nary "line" => line,
 | 
			
		||||
            0xc2 Nary "rect" => rect,
 | 
			
		||||
            0xc3 Nary "circle" => circle,
 | 
			
		||||
 | 
			
		||||
            0xe0 Nary "stroke" => stroke,
 | 
			
		||||
            0xe1 Nary "fill" => fill,
 | 
			
		||||
 | 
			
		||||
            0xf0 Nary "withDotter" => with_dotter,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -469,7 +544,13 @@ pub mod fns {
 | 
			
		|||
 | 
			
		||||
    fn to_shape(value: Value, vm: &Vm) -> Option<Shape> {
 | 
			
		||||
        match value {
 | 
			
		||||
            Value::Nil | Value::False | Value::True | Value::Number(_) | Value::Rgba(_) => None,
 | 
			
		||||
            Value::Nil
 | 
			
		||||
            | Value::False
 | 
			
		||||
            | Value::True
 | 
			
		||||
            | Value::Tag(_)
 | 
			
		||||
            | Value::Number(_)
 | 
			
		||||
            | Value::Rgba(_) => None,
 | 
			
		||||
 | 
			
		||||
            Value::Ref(id) => {
 | 
			
		||||
                if let Ref::Shape(shape) = vm.get_ref(id) {
 | 
			
		||||
                    Some(shape.clone())
 | 
			
		||||
| 
						 | 
				
			
			@ -477,6 +558,7 @@ pub mod fns {
 | 
			
		|||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Value::Vec4(vec) => Some(Shape::Point(vec.into())),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -588,4 +670,22 @@ pub mod fns {
 | 
			
		|||
            Ok(Value::Nil)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_dotter(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
 | 
			
		||||
        if args.num() != 1 {
 | 
			
		||||
            return Err(vm.create_exception(
 | 
			
		||||
                "`withDotter` expects a single argument (withDotter \\d -> [])",
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let draw = args.get_closure(vm, 0, "argument to `withDotter` must be a closure")?;
 | 
			
		||||
        if draw.param_count != 1 {
 | 
			
		||||
            return Err(vm.create_exception("function passed to `withDotter` must take in a single parameter (withDotter \\d -> [])"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let id = vm.create_ref(Ref::Reticle(Reticle::Dotter {
 | 
			
		||||
            draw: args.get(vm, 0),
 | 
			
		||||
        }))?;
 | 
			
		||||
        Ok(Value::Ref(id))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										78
									
								
								crates/haku/src/trampoline.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								crates/haku/src/trampoline.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
use tiny_skia::Pixmap;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    render::{Renderer, RendererLimits},
 | 
			
		||||
    system::System,
 | 
			
		||||
    value::{Ref, Reticle, Value, Vec2},
 | 
			
		||||
    vm::{Exception, Vm},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
pub struct Trampoline {
 | 
			
		||||
    pub value: Value,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE: This must be kept in sync with haku.js.
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
#[repr(u8)]
 | 
			
		||||
pub enum Cont {
 | 
			
		||||
    Scribble,
 | 
			
		||||
    Dotter,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Trampoline {
 | 
			
		||||
    pub fn new(init: Value) -> Self {
 | 
			
		||||
        Self { value: init }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn cont(&self, vm: &Vm) -> Cont {
 | 
			
		||||
        let Some((_, refv)) = vm.get_ref_value(self.value) else {
 | 
			
		||||
            return Cont::Scribble;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        match refv {
 | 
			
		||||
            Ref::Reticle(_) => Cont::Dotter,
 | 
			
		||||
            _ => Cont::Scribble,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn scribble(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        vm: &Vm,
 | 
			
		||||
        pixmap: &mut Pixmap,
 | 
			
		||||
        translation: Vec2,
 | 
			
		||||
        limits: &RendererLimits,
 | 
			
		||||
    ) -> Result<(), Exception> {
 | 
			
		||||
        let mut renderer = Renderer::new(&mut *pixmap, limits);
 | 
			
		||||
        renderer.translate(translation.x, translation.y);
 | 
			
		||||
        renderer.render(vm, self.value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn dotter(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        vm: &mut Vm,
 | 
			
		||||
        system: &System,
 | 
			
		||||
        from: Vec2,
 | 
			
		||||
        to: Vec2,
 | 
			
		||||
        num: f32,
 | 
			
		||||
    ) -> Result<(), Exception> {
 | 
			
		||||
        let (_, vref) = vm.get_ref_value(self.value).expect("value must be a ref");
 | 
			
		||||
        let &Ref::Reticle(Reticle::Dotter {
 | 
			
		||||
            draw: Value::Ref(draw_id),
 | 
			
		||||
        }) = vref
 | 
			
		||||
        else {
 | 
			
		||||
            panic!("value must be a dotter reticle");
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dotter = vm.create_ref(Ref::Closure(system.create_dotter(
 | 
			
		||||
            from.into(),
 | 
			
		||||
            to.into(),
 | 
			
		||||
            num,
 | 
			
		||||
        )))?;
 | 
			
		||||
 | 
			
		||||
        let value = vm.run(system, draw_id, &[Value::Ref(dotter)])?;
 | 
			
		||||
        self.value = value;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
use alloc::vec::Vec;
 | 
			
		||||
 | 
			
		||||
use crate::{compiler::ClosureSpec, system::ChunkId};
 | 
			
		||||
use crate::{bytecode::TagId, compiler::ClosureSpec, system::ChunkId};
 | 
			
		||||
 | 
			
		||||
// TODO: Probably needs some pretty hardcore space optimization.
 | 
			
		||||
// Maybe when we have static typing.
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ pub enum Value {
 | 
			
		|||
    Nil,
 | 
			
		||||
    False,
 | 
			
		||||
    True,
 | 
			
		||||
    Tag(TagId),
 | 
			
		||||
    Number(f32),
 | 
			
		||||
    Vec4(Vec4),
 | 
			
		||||
    Rgba(Rgba),
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +74,12 @@ pub struct Vec2 {
 | 
			
		|||
    pub y: f32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Vec2 {
 | 
			
		||||
    pub fn new(x: f32, y: f32) -> Self {
 | 
			
		||||
        Self { x, y }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Vec4> for Vec2 {
 | 
			
		||||
    fn from(value: Vec4) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +97,17 @@ pub struct Vec4 {
 | 
			
		|||
    pub w: f32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Vec2> for Vec4 {
 | 
			
		||||
    fn from(value: Vec2) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            x: value.x,
 | 
			
		||||
            y: value.y,
 | 
			
		||||
            z: 0.0,
 | 
			
		||||
            w: 0.0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct Rgba {
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +141,7 @@ pub enum Ref {
 | 
			
		|||
    List(List),
 | 
			
		||||
    Shape(Shape),
 | 
			
		||||
    Scribble(Scribble),
 | 
			
		||||
    Reticle(Reticle),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Ref {
 | 
			
		||||
| 
						 | 
				
			
			@ -206,3 +225,8 @@ pub enum Scribble {
 | 
			
		|||
    Stroke(Stroke),
 | 
			
		||||
    Fill(Fill),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub enum Reticle {
 | 
			
		||||
    Dotter { draw: Value },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,10 @@ use core::{
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
use alloc::{string::String, vec::Vec};
 | 
			
		||||
use log::debug;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    bytecode::{self, Defs, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
 | 
			
		||||
    bytecode::{self, Defs, Opcode, TagId, CAPTURE_CAPTURE, CAPTURE_LOCAL},
 | 
			
		||||
    system::{ChunkId, System},
 | 
			
		||||
    value::{BytecodeLoc, Closure, FunctionName, List, Ref, RefId, Rgba, Value, Vec4},
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -178,7 +179,12 @@ impl Vm {
 | 
			
		|||
            .ok_or_else(|| self.create_exception("corrupted bytecode (call stack underflow)"))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn run(&mut self, system: &System, mut closure_id: RefId) -> Result<Value, Exception> {
 | 
			
		||||
    pub fn run(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        system: &System,
 | 
			
		||||
        mut closure_id: RefId,
 | 
			
		||||
        params: &[Value],
 | 
			
		||||
    ) -> Result<Value, Exception> {
 | 
			
		||||
        let closure = self
 | 
			
		||||
            .get_ref(closure_id)
 | 
			
		||||
            .as_closure()
 | 
			
		||||
| 
						 | 
				
			
			@ -191,7 +197,12 @@ impl Vm {
 | 
			
		|||
        let mut fuel = self.fuel;
 | 
			
		||||
 | 
			
		||||
        let init_bottom = bottom;
 | 
			
		||||
        for _ in 0..closure.local_count {
 | 
			
		||||
        let local_count = closure.local_count;
 | 
			
		||||
 | 
			
		||||
        for ¶m in params {
 | 
			
		||||
            self.push(param)?;
 | 
			
		||||
        }
 | 
			
		||||
        for _ in 0..local_count {
 | 
			
		||||
            self.push(Value::Nil)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -219,6 +230,11 @@ impl Vm {
 | 
			
		|||
                Opcode::False => self.push(Value::False)?,
 | 
			
		||||
                Opcode::True => self.push(Value::True)?,
 | 
			
		||||
 | 
			
		||||
                Opcode::Tag => {
 | 
			
		||||
                    let i = chunk.read_u16(&mut pc)?;
 | 
			
		||||
                    self.push(Value::Tag(TagId::from_u16(i)))?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Opcode::Number => {
 | 
			
		||||
                    let x = chunk.read_f32(&mut pc)?;
 | 
			
		||||
                    self.push(Value::Number(x))?;
 | 
			
		||||
| 
						 | 
				
			
			@ -344,6 +360,34 @@ impl Vm {
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Opcode::Field => {
 | 
			
		||||
                    let count = chunk.read_u8(&mut pc)? as usize;
 | 
			
		||||
 | 
			
		||||
                    let field_tag = self.pop()?;
 | 
			
		||||
                    let Value::Tag(field_tag_id) = field_tag else {
 | 
			
		||||
                        return Err(self.create_exception("name of data field to look up must be a tag (starting with an uppercase letter)"));
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    let mut index = None;
 | 
			
		||||
                    for i in 0..count {
 | 
			
		||||
                        let tag_id = TagId::from_u16(chunk.read_u16(&mut pc)?);
 | 
			
		||||
                        if tag_id == field_tag_id {
 | 
			
		||||
                            index = Some(i);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let Some(index) = index else {
 | 
			
		||||
                        return Err(self.create_exception(
 | 
			
		||||
                            "field with this name does not exist in the given data structure",
 | 
			
		||||
                        ));
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    let closure = self.get_ref(closure_id).as_closure().unwrap();
 | 
			
		||||
                    self.push(closure.captures.get(index).copied().ok_or_else(|| {
 | 
			
		||||
                        self.create_exception("corrupted bytecode (field index out of bounds)")
 | 
			
		||||
                    })?)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Opcode::Call => {
 | 
			
		||||
                    let argument_count = chunk.read_u8(&mut pc)? as usize;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -554,6 +598,23 @@ impl FnArgs {
 | 
			
		|||
            .to_rgba()
 | 
			
		||||
            .ok_or_else(|| vm.create_exception(message))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline(never)]
 | 
			
		||||
    pub fn get_closure<'vm>(
 | 
			
		||||
        &self,
 | 
			
		||||
        vm: &'vm Vm,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        message: &'static str,
 | 
			
		||||
    ) -> Result<&'vm Closure, Exception> {
 | 
			
		||||
        let value = self.get(vm, index);
 | 
			
		||||
        let (_, any_ref) = vm
 | 
			
		||||
            .get_ref_value(value)
 | 
			
		||||
            .ok_or_else(|| vm.create_exception(message))?;
 | 
			
		||||
        let Ref::Closure(closure) = any_ref else {
 | 
			
		||||
            return Err(vm.create_exception(message));
 | 
			
		||||
        };
 | 
			
		||||
        Ok(closure)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, PartialEq, Eq)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ use std::error::Error;
 | 
			
		|||
 | 
			
		||||
use haku::{
 | 
			
		||||
    ast::{dump::dump, Ast},
 | 
			
		||||
    bytecode::{Chunk, Defs},
 | 
			
		||||
    bytecode::{Chunk, Defs, DefsLimits},
 | 
			
		||||
    compiler::{compile_expr, Compiler, Source},
 | 
			
		||||
    lexer::{lex, Lexer},
 | 
			
		||||
    parser::{self, Parser, ParserLimits},
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ use haku::{
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
fn eval(code: &str) -> Result<Value, Box<dyn Error>> {
 | 
			
		||||
    let mut system = System::new(1);
 | 
			
		||||
    let mut system = System::new(2);
 | 
			
		||||
 | 
			
		||||
    let code = SourceCode::unlimited_len(code);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +32,10 @@ fn eval(code: &str) -> Result<Value, Box<dyn Error>> {
 | 
			
		|||
        system: &system,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut defs = Defs::new(256);
 | 
			
		||||
    let mut defs = Defs::new(&DefsLimits {
 | 
			
		||||
        max_defs: 256,
 | 
			
		||||
        max_tags: 256,
 | 
			
		||||
    });
 | 
			
		||||
    let mut chunk = Chunk::new(65536).unwrap();
 | 
			
		||||
    let mut compiler = Compiler::new(&mut defs, &mut chunk);
 | 
			
		||||
    compile_expr(&mut compiler, &src, root)?;
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +73,7 @@ fn eval(code: &str) -> Result<Value, Box<dyn Error>> {
 | 
			
		|||
    println!("closure spec: {closure_spec:?}");
 | 
			
		||||
 | 
			
		||||
    let closure = vm.create_ref(Ref::Closure(Closure::chunk(chunk_id, closure_spec)))?;
 | 
			
		||||
    let result = vm.run(&system, closure)?;
 | 
			
		||||
    let result = vm.run(&system, closure, &[])?;
 | 
			
		||||
 | 
			
		||||
    println!("used fuel: {}", limits.fuel - vm.remaining_fuel());
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -281,3 +284,11 @@ fn issue_78() {
 | 
			
		|||
    "#;
 | 
			
		||||
    assert_eq!(eval(code).unwrap(), Value::Ref(RefId::from_u32(2)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn with_dotter_identity() {
 | 
			
		||||
    let code = r#"
 | 
			
		||||
        withDotter \d -> d
 | 
			
		||||
    "#;
 | 
			
		||||
    assert_eq!(eval(code).unwrap(), Value::Ref(RefId::from_u32(0)))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,10 @@ use axum::{
 | 
			
		|||
};
 | 
			
		||||
use base64::Engine;
 | 
			
		||||
use eyre::{bail, Context, OptionExt};
 | 
			
		||||
use haku::value::Value;
 | 
			
		||||
use haku::{
 | 
			
		||||
    trampoline::{Cont, Trampoline},
 | 
			
		||||
    value::Value,
 | 
			
		||||
};
 | 
			
		||||
use schema::{
 | 
			
		||||
    ChunkInfo, Error, LoginRequest, LoginResponse, Notify, Online, Request, Version, WallInfo,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +32,8 @@ use crate::{
 | 
			
		|||
    schema::Vec2,
 | 
			
		||||
    wall::{
 | 
			
		||||
        self, auto_save::AutoSave, chunk_images::ChunkImages, chunk_iterator::ChunkIterator,
 | 
			
		||||
        database::ChunkDataPair, ChunkPosition, JoinError, SessionHandle, UserInit, Wall, WallId,
 | 
			
		||||
        database::ChunkDataPair, ChunkPosition, Interaction, JoinError, SessionHandle, UserInit,
 | 
			
		||||
        Wall, WallId,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -220,12 +224,8 @@ struct SessionLoop {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
enum RenderCommand {
 | 
			
		||||
    SetBrush {
 | 
			
		||||
        brush: String,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    Plot {
 | 
			
		||||
        points: Vec<Vec2>,
 | 
			
		||||
    Interact {
 | 
			
		||||
        interactions: Vec<Interaction>,
 | 
			
		||||
        done: oneshot::Sender<()>,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -245,10 +245,17 @@ impl SessionLoop {
 | 
			
		|||
        // If this ends up dropping commands - it's your fault for trying to DoS my server!
 | 
			
		||||
        let (render_commands_tx, render_commands_rx) = mpsc::channel(1);
 | 
			
		||||
 | 
			
		||||
        render_commands_tx
 | 
			
		||||
            .send(RenderCommand::SetBrush { brush })
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        let thread_ready = {
 | 
			
		||||
            let (done_tx, done_rx) = oneshot::channel();
 | 
			
		||||
            render_commands_tx
 | 
			
		||||
                .send(RenderCommand::Interact {
 | 
			
		||||
                    interactions: vec![Interaction::SetBrush { brush }],
 | 
			
		||||
                    done: done_tx,
 | 
			
		||||
                })
 | 
			
		||||
                .await
 | 
			
		||||
                .unwrap();
 | 
			
		||||
            done_rx
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // We spawn our own thread so as not to clog the tokio blocking thread pool with our
 | 
			
		||||
        // rendering shenanigans.
 | 
			
		||||
| 
						 | 
				
			
			@ -265,6 +272,8 @@ impl SessionLoop {
 | 
			
		|||
            })
 | 
			
		||||
            .context("could not spawn render thread")?;
 | 
			
		||||
 | 
			
		||||
        thread_ready.await?;
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            wall_id,
 | 
			
		||||
            wall,
 | 
			
		||||
| 
						 | 
				
			
			@ -312,47 +321,82 @@ impl SessionLoop {
 | 
			
		|||
                    | wall::EventKind::Leave
 | 
			
		||||
                    | wall::EventKind::Cursor { .. } => (),
 | 
			
		||||
 | 
			
		||||
                    wall::EventKind::SetBrush { brush } => {
 | 
			
		||||
                        // SetBrush is not dropped because it is a very important event.
 | 
			
		||||
                        _ = self
 | 
			
		||||
                            .render_commands_tx
 | 
			
		||||
                            .send(RenderCommand::SetBrush {
 | 
			
		||||
                                brush: brush.clone(),
 | 
			
		||||
                            })
 | 
			
		||||
                            .await;
 | 
			
		||||
                    }
 | 
			
		||||
                    wall::EventKind::Plot { points } => {
 | 
			
		||||
                        let chunks_to_modify: Vec<_> =
 | 
			
		||||
                            chunks_to_modify(&self.wall, points).into_iter().collect();
 | 
			
		||||
                        match self.chunk_images.load(chunks_to_modify.clone()).await {
 | 
			
		||||
                            Ok(_) => {
 | 
			
		||||
                                // We drop commands if we take too long to render instead of lagging
 | 
			
		||||
                                // the WebSocket thread.
 | 
			
		||||
                                // Theoretically this will yield much better responsiveness, but it _will_
 | 
			
		||||
                                // result in some visual glitches if we're getting bottlenecked.
 | 
			
		||||
                                let (done_tx, done_rx) = oneshot::channel();
 | 
			
		||||
                                let send_result =
 | 
			
		||||
                                    self.render_commands_tx.try_send(RenderCommand::Plot {
 | 
			
		||||
                                        points: points.clone(),
 | 
			
		||||
                                        done: done_tx,
 | 
			
		||||
                                    });
 | 
			
		||||
                    wall::EventKind::Interact { interactions } => {
 | 
			
		||||
                        let (done_tx, done_rx) = oneshot::channel();
 | 
			
		||||
 | 
			
		||||
                                if send_result.is_err() {
 | 
			
		||||
                                    info!(
 | 
			
		||||
                                        ?points,
 | 
			
		||||
                                        "render thread is overloaded, dropping request to draw points"
 | 
			
		||||
                                    );
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                let auto_save = Arc::clone(&self.auto_save);
 | 
			
		||||
                                tokio::spawn(async move {
 | 
			
		||||
                                    _ = done_rx.await;
 | 
			
		||||
                                    auto_save.request(chunks_to_modify).await;
 | 
			
		||||
                        if interactions
 | 
			
		||||
                            .iter()
 | 
			
		||||
                            .any(|i| matches!(i, Interaction::SetBrush { .. }))
 | 
			
		||||
                        {
 | 
			
		||||
                            // SetBrush is an important event, so we wait for the render thread
 | 
			
		||||
                            // to unload.
 | 
			
		||||
                            _ = self
 | 
			
		||||
                                .render_commands_tx
 | 
			
		||||
                                .send(RenderCommand::Interact {
 | 
			
		||||
                                    interactions: interactions.clone(),
 | 
			
		||||
                                    done: done_tx,
 | 
			
		||||
                                })
 | 
			
		||||
                                .await;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // If there is no SetBrush, there's no need to wait, so we fire events
 | 
			
		||||
                            // blindly. If the thread's not okay with that... well, whatever.
 | 
			
		||||
                            // That's your issue for making a really slow brush.
 | 
			
		||||
                            let send_result =
 | 
			
		||||
                                self.render_commands_tx.try_send(RenderCommand::Interact {
 | 
			
		||||
                                    interactions: interactions.clone(),
 | 
			
		||||
                                    done: done_tx,
 | 
			
		||||
                                });
 | 
			
		||||
                            if send_result.is_err() {
 | 
			
		||||
                                info!(
 | 
			
		||||
                                    ?interactions,
 | 
			
		||||
                                    "render thread is overloaded, dropping interaction request"
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(err) => error!(?err, "while loading chunks for render command"),
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                        // TODO: Auto save. This'll need us to compute which chunks will be affected
 | 
			
		||||
                        // by the interactions.
 | 
			
		||||
                    } // wall::EventKind::SetBrush { brush } => {
 | 
			
		||||
                      //     // SetBrush is not dropped because it is a very important event.
 | 
			
		||||
                      //     _ = self
 | 
			
		||||
                      //         .render_commands_tx
 | 
			
		||||
                      //         .send(RenderCommand::SetBrush {
 | 
			
		||||
                      //             brush: brush.clone(),
 | 
			
		||||
                      //         })
 | 
			
		||||
                      //         .await;
 | 
			
		||||
                      // }
 | 
			
		||||
                      // wall::EventKind::Plot { points } => {
 | 
			
		||||
                      //     let chunks_to_modify: Vec<_> =
 | 
			
		||||
                      //         chunks_to_modify(&self.wall, points).into_iter().collect();
 | 
			
		||||
                      //     match self.chunk_images.load(chunks_to_modify.clone()).await {
 | 
			
		||||
                      //         Ok(_) => {
 | 
			
		||||
                      //             // We drop commands if we take too long to render instead of lagging
 | 
			
		||||
                      //             // the WebSocket thread.
 | 
			
		||||
                      //             // Theoretically this will yield much better responsiveness, but it _will_
 | 
			
		||||
                      //             // result in some visual glitches if we're getting bottlenecked.
 | 
			
		||||
                      //             let (done_tx, done_rx) = oneshot::channel();
 | 
			
		||||
                      //             let send_result =
 | 
			
		||||
                      //                 self.render_commands_tx.try_send(RenderCommand::Plot {
 | 
			
		||||
                      //                     points: points.clone(),
 | 
			
		||||
                      //                     done: done_tx,
 | 
			
		||||
                      //                 });
 | 
			
		||||
 | 
			
		||||
                      //             if send_result.is_err() {
 | 
			
		||||
                      //                 info!(
 | 
			
		||||
                      //                     ?points,
 | 
			
		||||
                      //                     "render thread is overloaded, dropping request to draw points"
 | 
			
		||||
                      //                 );
 | 
			
		||||
                      //             }
 | 
			
		||||
 | 
			
		||||
                      //             let auto_save = Arc::clone(&self.auto_save);
 | 
			
		||||
                      //             tokio::spawn(async move {
 | 
			
		||||
                      //                 _ = done_rx.await;
 | 
			
		||||
                      //                 auto_save.request(chunks_to_modify).await;
 | 
			
		||||
                      //             });
 | 
			
		||||
                      //         }
 | 
			
		||||
                      //         Err(err) => error!(?err, "while loading chunks for render command"),
 | 
			
		||||
                      //     }
 | 
			
		||||
                      // }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.wall.event(wall::Event {
 | 
			
		||||
| 
						 | 
				
			
			@ -437,32 +481,97 @@ impl SessionLoop {
 | 
			
		|||
 | 
			
		||||
    fn render_thread(wall: Arc<Wall>, limits: Limits, mut commands: mpsc::Receiver<RenderCommand>) {
 | 
			
		||||
        let mut haku = Haku::new(limits);
 | 
			
		||||
        let mut trampoline = None;
 | 
			
		||||
        let mut brush_ok = false;
 | 
			
		||||
        let mut current_render_area = RenderArea::default();
 | 
			
		||||
 | 
			
		||||
        while let Some(command) = commands.blocking_recv() {
 | 
			
		||||
            match command {
 | 
			
		||||
                RenderCommand::SetBrush { brush } => {
 | 
			
		||||
                    brush_ok = haku.set_brush(&brush).is_ok();
 | 
			
		||||
            let RenderCommand::Interact { interactions, done } = command;
 | 
			
		||||
 | 
			
		||||
            let mut queue = VecDeque::from(interactions);
 | 
			
		||||
            while let Some(interaction) = queue.pop_front() {
 | 
			
		||||
                if let Some(render_area) = render_area(wall.settings(), &interaction) {
 | 
			
		||||
                    current_render_area = render_area;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                RenderCommand::Plot { points, done } => {
 | 
			
		||||
                    if brush_ok {
 | 
			
		||||
                        if let Ok(value) = haku.eval_brush() {
 | 
			
		||||
                            for point in points {
 | 
			
		||||
                                // Ignore the result. It's better if we render _something_ rather
 | 
			
		||||
                                // than nothing.
 | 
			
		||||
                                _ = draw_to_chunks(&wall, &haku, value, point);
 | 
			
		||||
                match interaction {
 | 
			
		||||
                    Interaction::SetBrush { brush } => {
 | 
			
		||||
                        brush_ok = haku.set_brush(&brush).is_ok();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Interaction::Dotter { from, to, num } => {
 | 
			
		||||
                        if brush_ok {
 | 
			
		||||
                            if let Some(trampoline) =
 | 
			
		||||
                                jumpstart_trampoline(&mut haku, &mut trampoline)
 | 
			
		||||
                            {
 | 
			
		||||
                                let cont = haku.cont(trampoline);
 | 
			
		||||
                                if cont == Cont::Dotter {
 | 
			
		||||
                                    _ = haku.dotter(trampoline, from, to, num);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    error!("received Dotter interaction when a {cont:?} continuation was next");
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                info!("failed to jumpstart trampoline for Dotter interaction");
 | 
			
		||||
                            }
 | 
			
		||||
                            haku.reset_vm();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    _ = done.send(());
 | 
			
		||||
 | 
			
		||||
                    Interaction::Scribble => {
 | 
			
		||||
                        // Regarding the take(): this is to self-synchronize in case of error.
 | 
			
		||||
                        // Once a scribble is rendered, we reset back to not having a trampoline,
 | 
			
		||||
                        // and further interactions must be sent to kickstart a new one.
 | 
			
		||||
                        //
 | 
			
		||||
                        // Regarding not jumpstarting a trampoline here: it would be useless,
 | 
			
		||||
                        // because a scribble is always the last thing in the chain, and it never
 | 
			
		||||
                        // modifies the render area. Therefore what would end up happening is we'd
 | 
			
		||||
                        // end up rendering to no chunks, therefore not doing anything.
 | 
			
		||||
                        // The server doesn't need to report anything to the user and so can save
 | 
			
		||||
                        // a bit of work by _not_ evaluating the brush initially here.
 | 
			
		||||
                        if let Some(trampoline) = trampoline.take() {
 | 
			
		||||
                            _ = draw_to_chunks(&wall, &haku, current_render_area, trampoline.value);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            info!("tried to scribble without an active trampoline");
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        current_render_area = RenderArea::default();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = done.send(());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, Default)]
 | 
			
		||||
struct RenderArea {
 | 
			
		||||
    top_left: Vec2,
 | 
			
		||||
    bottom_right: Vec2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn render_area(wall_settings: &wall::Settings, interaction: &Interaction) -> Option<RenderArea> {
 | 
			
		||||
    match interaction {
 | 
			
		||||
        Interaction::Dotter { from, to, .. } => {
 | 
			
		||||
            let half_paint_area = wall_settings.paint_area as f32 / 2.0;
 | 
			
		||||
            Some(RenderArea {
 | 
			
		||||
                top_left: Vec2::new(from.x - half_paint_area, from.y - half_paint_area),
 | 
			
		||||
                bottom_right: Vec2::new(to.x + half_paint_area, to.y + half_paint_area),
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        Interaction::SetBrush { .. } | Interaction::Scribble => None,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn jumpstart_trampoline<'a>(
 | 
			
		||||
    haku: &mut Haku,
 | 
			
		||||
    trampoline: &'a mut Option<Trampoline>,
 | 
			
		||||
) -> Option<&'a mut Trampoline> {
 | 
			
		||||
    if trampoline.is_none() {
 | 
			
		||||
        *trampoline = haku.eval_brush().ok().map(Trampoline::new);
 | 
			
		||||
    }
 | 
			
		||||
    trampoline.as_mut()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn chunks_to_modify(wall: &Wall, points: &[Vec2]) -> HashSet<ChunkPosition> {
 | 
			
		||||
    let mut chunks = HashSet::new();
 | 
			
		||||
    for point in points {
 | 
			
		||||
| 
						 | 
				
			
			@ -483,23 +592,23 @@ fn chunks_to_modify(wall: &Wall, points: &[Vec2]) -> HashSet<ChunkPosition> {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
#[instrument(skip(wall, haku, value))]
 | 
			
		||||
fn draw_to_chunks(wall: &Wall, haku: &Haku, value: Value, center: Vec2) -> eyre::Result<()> {
 | 
			
		||||
fn draw_to_chunks(
 | 
			
		||||
    wall: &Wall,
 | 
			
		||||
    haku: &Haku,
 | 
			
		||||
    render_area: RenderArea,
 | 
			
		||||
    value: Value,
 | 
			
		||||
) -> eyre::Result<()> {
 | 
			
		||||
    let settings = wall.settings();
 | 
			
		||||
 | 
			
		||||
    let chunk_size = settings.chunk_size as f32;
 | 
			
		||||
    let paint_area = settings.paint_area as f32;
 | 
			
		||||
 | 
			
		||||
    let left = center.x - paint_area / 2.0;
 | 
			
		||||
    let top = center.y - paint_area / 2.0;
 | 
			
		||||
    let top_left_chunk = settings.chunk_at(render_area.top_left);
 | 
			
		||||
    let bottom_right_chunk = settings.chunk_at_ceil(render_area.bottom_right);
 | 
			
		||||
 | 
			
		||||
    let left_chunk = settings.chunk_at_1d(left);
 | 
			
		||||
    let top_chunk = settings.chunk_at_1d(top);
 | 
			
		||||
    let right_chunk = settings.chunk_at_1d_ceil(left + paint_area);
 | 
			
		||||
    let bottom_chunk = settings.chunk_at_1d_ceil(top + paint_area);
 | 
			
		||||
    for chunk_y in top_chunk..bottom_chunk {
 | 
			
		||||
        for chunk_x in left_chunk..right_chunk {
 | 
			
		||||
            let x = f32::floor(-chunk_x as f32 * chunk_size + center.x);
 | 
			
		||||
            let y = f32::floor(-chunk_y as f32 * chunk_size + center.y);
 | 
			
		||||
    for chunk_y in top_left_chunk.y..bottom_right_chunk.y {
 | 
			
		||||
        for chunk_x in top_left_chunk.x..bottom_right_chunk.x {
 | 
			
		||||
            let x = f32::floor(-chunk_x as f32 * chunk_size);
 | 
			
		||||
            let y = f32::floor(-chunk_y as f32 * chunk_size);
 | 
			
		||||
            let chunk_ref = wall.get_or_create_chunk(ChunkPosition::new(chunk_x, chunk_y));
 | 
			
		||||
            let mut chunk = chunk_ref.blocking_lock();
 | 
			
		||||
            haku.render_value(&mut chunk.pixmap, value, Vec2 { x, y })?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
use eyre::{bail, Context, OptionExt};
 | 
			
		||||
use haku::{
 | 
			
		||||
    ast::Ast,
 | 
			
		||||
    bytecode::{Chunk, Defs, DefsImage},
 | 
			
		||||
    bytecode::{Chunk, Defs, DefsImage, DefsLimits},
 | 
			
		||||
    compiler::{ClosureSpec, Compiler, Source},
 | 
			
		||||
    lexer::{lex, Lexer},
 | 
			
		||||
    parser::{self, Parser, ParserLimits},
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ use haku::{
 | 
			
		|||
    source::SourceCode,
 | 
			
		||||
    system::{ChunkId, System, SystemImage},
 | 
			
		||||
    token::Lexis,
 | 
			
		||||
    trampoline::{Cont, Trampoline},
 | 
			
		||||
    value::{Closure, Ref, Value},
 | 
			
		||||
    vm::{Vm, VmImage, VmLimits},
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +31,7 @@ pub struct Limits {
 | 
			
		|||
    pub max_source_code_len: u32,
 | 
			
		||||
    pub max_chunks: usize,
 | 
			
		||||
    pub max_defs: usize,
 | 
			
		||||
    pub max_tags: usize,
 | 
			
		||||
    pub max_tokens: usize,
 | 
			
		||||
    pub max_parser_events: usize,
 | 
			
		||||
    pub ast_capacity: usize,
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +61,10 @@ pub struct Haku {
 | 
			
		|||
impl Haku {
 | 
			
		||||
    pub fn new(limits: Limits) -> Self {
 | 
			
		||||
        let system = System::new(limits.max_chunks);
 | 
			
		||||
        let defs = Defs::new(limits.max_defs);
 | 
			
		||||
        let defs = Defs::new(&DefsLimits {
 | 
			
		||||
            max_defs: limits.max_defs,
 | 
			
		||||
            max_tags: limits.max_tags,
 | 
			
		||||
        });
 | 
			
		||||
        let vm = Vm::new(
 | 
			
		||||
            &defs,
 | 
			
		||||
            &VmLimits {
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +163,7 @@ impl Haku {
 | 
			
		|||
 | 
			
		||||
        let scribble = self
 | 
			
		||||
            .vm
 | 
			
		||||
            .run(&self.system, closure_id)
 | 
			
		||||
            .run(&self.system, closure_id, &[])
 | 
			
		||||
            .context("an exception occurred while evaluating the scribble")?;
 | 
			
		||||
 | 
			
		||||
        Ok(scribble)
 | 
			
		||||
| 
						 | 
				
			
			@ -187,4 +192,18 @@ impl Haku {
 | 
			
		|||
    pub fn reset_vm(&mut self) {
 | 
			
		||||
        self.vm.restore_image(&self.vm_image);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn cont(&self, trampoline: &Trampoline) -> Cont {
 | 
			
		||||
        trampoline.cont(&self.vm)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn dotter(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        trampoline: &mut Trampoline,
 | 
			
		||||
        from: Vec2,
 | 
			
		||||
        to: Vec2,
 | 
			
		||||
        num: f32,
 | 
			
		||||
    ) -> eyre::Result<()> {
 | 
			
		||||
        Ok(trampoline.dotter(&mut self.vm, &self.system, from.into(), to.into(), num)?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct Vec2 {
 | 
			
		||||
    pub x: f32,
 | 
			
		||||
    pub y: f32,
 | 
			
		||||
| 
						 | 
				
			
			@ -11,3 +11,9 @@ impl Vec2 {
 | 
			
		|||
        Self { x, y }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Vec2> for haku::value::Vec2 {
 | 
			
		||||
    fn from(value: Vec2) -> Self {
 | 
			
		||||
        Self::new(value.x, value.y)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -192,9 +192,19 @@ pub enum EventKind {
 | 
			
		|||
    Leave,
 | 
			
		||||
 | 
			
		||||
    Cursor { position: Vec2 },
 | 
			
		||||
    Interact { interactions: Vec<Interaction> },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize)]
 | 
			
		||||
#[serde(
 | 
			
		||||
    tag = "kind",
 | 
			
		||||
    rename_all = "camelCase",
 | 
			
		||||
    rename_all_fields = "camelCase"
 | 
			
		||||
)]
 | 
			
		||||
pub enum Interaction {
 | 
			
		||||
    SetBrush { brush: String },
 | 
			
		||||
    Plot { points: Vec<Vec2> },
 | 
			
		||||
    Dotter { from: Vec2, to: Vec2, num: f32 },
 | 
			
		||||
    Scribble,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
| 
						 | 
				
			
			@ -276,7 +286,7 @@ impl Wall {
 | 
			
		|||
 | 
			
		||||
                // Drawing events are handled by the owner session's thread to make drawing as
 | 
			
		||||
                // parallel as possible.
 | 
			
		||||
                EventKind::SetBrush { .. } | EventKind::Plot { .. } => (),
 | 
			
		||||
                EventKind::Interact { .. } => {}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										70
									
								
								docs/rkgk.dj
									
										
									
									
									
								
							
							
						
						
									
										70
									
								
								docs/rkgk.dj
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -27,7 +27,8 @@ In case you edited anything in the input box on the right, paste the following t
 | 
			
		|||
-- Try playing around with the numbers,
 | 
			
		||||
-- and see what happens!
 | 
			
		||||
 | 
			
		||||
stroke 8 #000 (vec 0 0)
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  stroke 8 #000 (d To)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
rakugaki is a drawing program for digital scribbles and other pieces of art.
 | 
			
		||||
| 
						 | 
				
			
			@ -83,10 +84,11 @@ If you want to draw multiple scribbles, you can wrap them into a list, which we
 | 
			
		|||
 | 
			
		||||
```haku
 | 
			
		||||
-- Draw two colorful dots instead of one!
 | 
			
		||||
[
 | 
			
		||||
  stroke 8 #F00 (vec 4 0)
 | 
			
		||||
  stroke 8 #00F (vec (-4) 0))
 | 
			
		||||
]
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  [
 | 
			
		||||
    stroke 8 #F00 (vec ((vecX (d To)) + 4) (vecY (d To)))
 | 
			
		||||
    stroke 8 #00F (vec ((vecX (d To)) - 4) (vecY (d To)))
 | 
			
		||||
  ]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
::: aside
 | 
			
		||||
| 
						 | 
				
			
			@ -103,25 +105,29 @@ And what's even crazier is that you can compose lists _further_---you can make a
 | 
			
		|||
It'll draw the first inner list, which contains two scribbles, and then it'll draw the second inner list, which contains two scribbles.
 | 
			
		||||
 | 
			
		||||
```haku
 | 
			
		||||
[
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  [
 | 
			
		||||
    stroke 8 #F00 (vec 4 (-4))
 | 
			
		||||
    stroke 8 #00F (vec (-4) (-4))
 | 
			
		||||
    [
 | 
			
		||||
      stroke 8 #F00 (vec ((vecX (d To)) + 4) (vecY (d To)))
 | 
			
		||||
      stroke 8 #00F (vec ((vecX (d To)) - 4) (vecY (d To)))
 | 
			
		||||
    ]
 | 
			
		||||
    [
 | 
			
		||||
      stroke 8 #FF0 (vec (vecX (d To)) ((vecY (d To)) + 4))
 | 
			
		||||
      stroke 8 #0FF (vec (vecX (d To)) ((vecY (d To)) - 4))
 | 
			
		||||
    ]
 | 
			
		||||
  ]
 | 
			
		||||
  [
 | 
			
		||||
    stroke 8 #FF0 (vec 4 4)
 | 
			
		||||
    stroke 8 #0FF (vec (-4) 4)
 | 
			
		||||
  ]
 | 
			
		||||
]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
::: aside
 | 
			
		||||
 | 
			
		||||
Another weird thing: when negating a number, you have to put it in parentheses.
 | 
			
		||||
I know this example is kind of horrendous to read right now.
 | 
			
		||||
 | 
			
		||||
This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing!
 | 
			
		||||
In this case, it will always choose the 2nd interpretation---vec minus four.
 | 
			
		||||
So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses.
 | 
			
		||||
This is because haku currently does not have vector math, which means we have to disassemble and reassemble vectors manually.
 | 
			
		||||
 | 
			
		||||
{% Another weird thing: when negating a number, you have to put it in parentheses. %}
 | 
			
		||||
{% This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing! %}
 | 
			
		||||
{% In this case, it will always choose the 2nd interpretation---vec minus four. %}
 | 
			
		||||
{% So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses. %}
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -142,15 +148,24 @@ Anyways!
 | 
			
		|||
Recall that super simple brush from before...
 | 
			
		||||
 | 
			
		||||
```haku
 | 
			
		||||
stroke 8 #000 (vec 0 0)
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  stroke 8 #000 (d To)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This reads as "a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the point `(0, 0)` relative to the mouse cursor."
 | 
			
		||||
This reads as "given a dotter, output a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the dotter's `To` coordinates."
 | 
			
		||||
 | 
			
		||||
All these symbols are very meaningful to haku.
 | 
			
		||||
If you reorder or remove any one of them, your brush isn't going to work!
 | 
			
		||||
 | 
			
		||||
- Reading from left to right, we start with `stroke`.\
 | 
			
		||||
- Reading from left to right, we start with `withDotter`.
 | 
			
		||||
  We can't draw without knowing _where_ to draw, and the `withDotter` incantation lets us ask the UI for that.
 | 
			
		||||
 | 
			
		||||
  - Then, `\d ->` lets us name the data we get back from the UI.
 | 
			
		||||
    `d` ends up containing a few useful properties, but the most useful one for us is `To`, which contains the current mouse position (where *`To`* draw).
 | 
			
		||||
 | 
			
		||||
  - We'll get to what all these sigils mean to haku later!
 | 
			
		||||
 | 
			
		||||
- On the next line we have a `stroke`.
 | 
			
		||||
  `stroke` is a _function_---a recipe for producing data!\
 | 
			
		||||
  haku has [many such built-in recipes](/docs/system.html).
 | 
			
		||||
  `stroke` is one of them.
 | 
			
		||||
| 
						 | 
				
			
			@ -192,7 +207,8 @@ Note how it's parenthesized though---recall that function arguments are separate
 | 
			
		|||
And with all that, we let haku mix all the ingredients together, and get a black dot under the cursor.
 | 
			
		||||
 | 
			
		||||
```haku
 | 
			
		||||
stroke 8 #000 (vec 0 0)
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  stroke 8 #000 (d To)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Nice!
 | 
			
		||||
| 
						 | 
				
			
			@ -206,8 +222,16 @@ So to spice things up, haku has a few shapes you can choose from!
 | 
			
		|||
Recall that 3rd argument to `stroke`.
 | 
			
		||||
We can actually pass any arbitrary shape to it, and haku will outline it for us.
 | 
			
		||||
 | 
			
		||||
Right now haku supports two additional shapes: rectangles and circles.
 | 
			
		||||
You can try them out by playing with this brush!
 | 
			
		||||
The experience of drawing with that example brush is pretty crap, because it can draw dots that don't connect with each other at all.
 | 
			
		||||
Let's fix that by drawing a `line` instead!
 | 
			
		||||
 | 
			
		||||
```haku
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  stroke 8 #000 (line (d From) (d To))
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We replace the singular position `d To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points.
 | 
			
		||||
For the starting position we use a _different_ property of `d`, which is `From`---this is the _previous_ value of `To`, which allows us to draw a continuous line.
 | 
			
		||||
 | 
			
		||||
```haku
 | 
			
		||||
[
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,6 +69,11 @@ max_chunks = 2
 | 
			
		|||
# Maximum amount of defs across all source code chunks.
 | 
			
		||||
max_defs = 256
 | 
			
		||||
 | 
			
		||||
# Maximum amount of unique tags across all source code chunks.
 | 
			
		||||
# Note that tag uniqueness is determined by the tag's source code string, so two instances of `A`
 | 
			
		||||
# only occupy one slot.
 | 
			
		||||
max_tags = 256
 | 
			
		||||
 | 
			
		||||
# Maximum amount of tokens a single chunk can have.
 | 
			
		||||
max_tokens = 65536
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
import { CodeEditor, getLineStart } from "rkgk/code-editor.js";
 | 
			
		||||
import { BrushPreview } from "rkgk/brush-preview.js";
 | 
			
		||||
import { CodeEditor } from "rkgk/code-editor.js";
 | 
			
		||||
 | 
			
		||||
const defaultBrush = `
 | 
			
		||||
-- This is your brush.
 | 
			
		||||
-- Try playing around with the numbers,
 | 
			
		||||
-- Try playing around with the numbers, 
 | 
			
		||||
-- and see what happens!
 | 
			
		||||
 | 
			
		||||
stroke 8 #000 (vec 0 0)
 | 
			
		||||
withDotter \d ->
 | 
			
		||||
  stroke 8 #000 (d To)
 | 
			
		||||
`.trim();
 | 
			
		||||
 | 
			
		||||
export class BrushEditor extends HTMLElement {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,32 +21,35 @@ export class BrushPreview extends HTMLElement {
 | 
			
		|||
        this.pixmap = new Pixmap(this.canvas.width, this.canvas.height);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #renderBrushInner(haku) {
 | 
			
		||||
        haku.resetVm();
 | 
			
		||||
    async #renderBrushInner(haku) {
 | 
			
		||||
        this.pixmap.clear();
 | 
			
		||||
        let evalResult = await haku.evalBrush({
 | 
			
		||||
            runDotter: async () => {
 | 
			
		||||
                return {
 | 
			
		||||
                    fromX: this.canvas.width / 2,
 | 
			
		||||
                    fromY: this.canvas.width / 2,
 | 
			
		||||
                    toX: this.canvas.width / 2,
 | 
			
		||||
                    toY: this.canvas.width / 2,
 | 
			
		||||
                    num: 0,
 | 
			
		||||
                };
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
        let evalResult = haku.evalBrush();
 | 
			
		||||
            runScribble: async (renderToPixmap) => {
 | 
			
		||||
                return renderToPixmap(this.pixmap, 0, 0);
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
        if (evalResult.status != "ok") {
 | 
			
		||||
            return { status: "error", phase: "eval", result: evalResult };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.pixmap.clear();
 | 
			
		||||
        let renderResult = haku.renderValue(
 | 
			
		||||
            this.pixmap,
 | 
			
		||||
            this.canvas.width / 2,
 | 
			
		||||
            this.canvas.height / 2,
 | 
			
		||||
        );
 | 
			
		||||
        if (renderResult.status != "ok") {
 | 
			
		||||
            return { status: "error", phase: "render", result: renderResult };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ctx.putImageData(this.pixmap.getImageData(), 0, 0);
 | 
			
		||||
 | 
			
		||||
        return { status: "ok" };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderBrush(haku) {
 | 
			
		||||
    async renderBrush(haku) {
 | 
			
		||||
        this.unsetErrorFlag();
 | 
			
		||||
        let result = this.#renderBrushInner(haku);
 | 
			
		||||
        let result = await this.#renderBrushInner(haku);
 | 
			
		||||
        if (result.status == "error") {
 | 
			
		||||
            this.setErrorFlag();
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
        this.#cursorReportingBehaviour();
 | 
			
		||||
        this.#panningBehaviour();
 | 
			
		||||
        this.#zoomingBehaviour();
 | 
			
		||||
        this.#paintingBehaviour();
 | 
			
		||||
        this.#interactionBehaviour();
 | 
			
		||||
 | 
			
		||||
        this.addEventListener("contextmenu", (event) => event.preventDefault());
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -388,18 +388,6 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
 | 
			
		||||
    // Behaviours
 | 
			
		||||
 | 
			
		||||
    async #cursorReportingBehaviour() {
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let event = await listen([this, "mousemove"]);
 | 
			
		||||
            let [x, y] = this.viewport.toViewportSpace(
 | 
			
		||||
                event.clientX - this.clientLeft,
 | 
			
		||||
                event.offsetY - this.clientTop,
 | 
			
		||||
                this.getWindowSize(),
 | 
			
		||||
            );
 | 
			
		||||
            this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sendViewportUpdate() {
 | 
			
		||||
        this.dispatchEvent(new Event(".viewportUpdate"));
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -443,24 +431,28 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #paintingBehaviour() {
 | 
			
		||||
        const paint = (x, y) => {
 | 
			
		||||
            let [wallX, wallY] = this.viewport.toViewportSpace(x, y, this.getWindowSize());
 | 
			
		||||
            this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
 | 
			
		||||
        };
 | 
			
		||||
    async #cursorReportingBehaviour() {
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let event = await listen([this, "mousemove"]);
 | 
			
		||||
            let [x, y] = this.viewport.toViewportSpace(
 | 
			
		||||
                event.clientX - this.clientLeft,
 | 
			
		||||
                event.offsetY - this.clientTop,
 | 
			
		||||
                this.getWindowSize(),
 | 
			
		||||
            );
 | 
			
		||||
            this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #interactionBehaviour() {
 | 
			
		||||
        while (true) {
 | 
			
		||||
            let mouseDown = await listen([this, "mousedown"]);
 | 
			
		||||
            if (mouseDown.button == 0) {
 | 
			
		||||
                paint(mouseDown.offsetX, mouseDown.offsetY);
 | 
			
		||||
                while (true) {
 | 
			
		||||
                    let event = await listen([window, "mousemove"], [window, "mouseup"]);
 | 
			
		||||
                    if (event.type == "mousemove") {
 | 
			
		||||
                        paint(event.clientX - this.clientLeft, event.offsetY - this.clientTop);
 | 
			
		||||
                    } else if (event.type == "mouseup") {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                let [mouseX, mouseY] = this.viewport.toViewportSpace(
 | 
			
		||||
                    mouseDown.clientX - this.clientLeft,
 | 
			
		||||
                    mouseDown.clientY - this.clientTop,
 | 
			
		||||
                    this.getWindowSize(),
 | 
			
		||||
                );
 | 
			
		||||
                notifyInteraction(this, "start", { mouseX, mouseY, num: 0 });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -468,6 +460,68 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
 | 
			
		||||
customElements.define("rkgk-canvas-renderer", CanvasRenderer);
 | 
			
		||||
 | 
			
		||||
function notifyInteraction(canvasRenderer, kind, fields) {
 | 
			
		||||
    canvasRenderer.dispatchEvent(
 | 
			
		||||
        Object.assign(new InteractEvent(canvasRenderer), { interactionKind: kind, ...fields }),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class InteractEvent extends Event {
 | 
			
		||||
    constructor(canvasRenderer) {
 | 
			
		||||
        super(".interact");
 | 
			
		||||
 | 
			
		||||
        this.canvasRenderer = canvasRenderer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    continueAsDotter() {
 | 
			
		||||
        (async () => {
 | 
			
		||||
            let event = await listen(
 | 
			
		||||
                [this.canvasRenderer, "mousemove"],
 | 
			
		||||
                [this.canvasRenderer, "mouseup"],
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (event.type == "mousemove") {
 | 
			
		||||
                let [mouseX, mouseY] = this.canvasRenderer.viewport.toViewportSpace(
 | 
			
		||||
                    event.clientX - this.canvasRenderer.clientLeft,
 | 
			
		||||
                    event.clientY - this.canvasRenderer.clientTop,
 | 
			
		||||
                    this.canvasRenderer.getWindowSize(),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                notifyInteraction(this.canvasRenderer, "dotter", {
 | 
			
		||||
                    previousX: this.mouseX,
 | 
			
		||||
                    previousY: this.mouseY,
 | 
			
		||||
                    mouseX,
 | 
			
		||||
                    mouseY,
 | 
			
		||||
                    num: this.num + 1,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (event.type == "mouseup" && event.button == 0) {
 | 
			
		||||
                // Break the loop.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        })();
 | 
			
		||||
 | 
			
		||||
        if (this.previousX != null && this.previousY != null) {
 | 
			
		||||
            return {
 | 
			
		||||
                fromX: this.previousX,
 | 
			
		||||
                fromY: this.previousY,
 | 
			
		||||
                toX: this.mouseX,
 | 
			
		||||
                toY: this.mouseY,
 | 
			
		||||
                num: this.num,
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            return {
 | 
			
		||||
                fromX: this.mouseX,
 | 
			
		||||
                fromY: this.mouseY,
 | 
			
		||||
                toX: this.mouseX,
 | 
			
		||||
                toY: this.mouseY,
 | 
			
		||||
                num: this.num,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Atlas {
 | 
			
		||||
    static getInitBuffer(chunkSize, nChunks) {
 | 
			
		||||
        let imageSize = chunkSize * nChunks;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -113,6 +113,12 @@ export class Pixmap {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE: This must be kept in sync with ContKind on the haku-wasm side.
 | 
			
		||||
export const ContKind = {
 | 
			
		||||
    Scribble: 0,
 | 
			
		||||
    Dotter: 1,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class Haku {
 | 
			
		||||
    #pInstance = 0;
 | 
			
		||||
    #pBrush = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -206,17 +212,49 @@ export class Haku {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    evalBrush() {
 | 
			
		||||
        return this.#statusCodeToResultObject(w.haku_eval_brush(this.#pInstance, this.#pBrush));
 | 
			
		||||
    beginBrush() {
 | 
			
		||||
        return this.#statusCodeToResultObject(w.haku_begin_brush(this.#pInstance, this.#pBrush));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderValue(pixmap, translationX, translationY) {
 | 
			
		||||
    expectedContKind() {
 | 
			
		||||
        return w.haku_cont_kind(this.#pInstance);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    contScribble(pixmap, translationX, translationY) {
 | 
			
		||||
        return this.#statusCodeToResultObject(
 | 
			
		||||
            w.haku_render_value(this.#pInstance, pixmap.ptr, translationX, translationY),
 | 
			
		||||
            w.haku_cont_scribble(this.#pInstance, pixmap.ptr, translationX, translationY),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resetVm() {
 | 
			
		||||
        w.haku_reset_vm(this.#pInstance);
 | 
			
		||||
    contDotter({ fromX, fromY, toX, toY, num }) {
 | 
			
		||||
        return this.#statusCodeToResultObject(
 | 
			
		||||
            w.haku_cont_dotter(this.#pInstance, fromX, fromY, toX, toY, num),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async evalBrush(options) {
 | 
			
		||||
        let { runDotter, runScribble } = options;
 | 
			
		||||
 | 
			
		||||
        let result;
 | 
			
		||||
        result = this.beginBrush();
 | 
			
		||||
        if (result.status != "ok") return result;
 | 
			
		||||
 | 
			
		||||
        while (this.expectedContKind() != ContKind.Invalid) {
 | 
			
		||||
            switch (this.expectedContKind()) {
 | 
			
		||||
                case ContKind.Scribble:
 | 
			
		||||
                    result = await runScribble((pixmap, translationX, translationY) => {
 | 
			
		||||
                        return this.contScribble(pixmap, translationX, translationY);
 | 
			
		||||
                    });
 | 
			
		||||
                    return result;
 | 
			
		||||
 | 
			
		||||
                case ContKind.Dotter:
 | 
			
		||||
                    let dotter = await runDotter();
 | 
			
		||||
                    result = this.contDotter(dotter);
 | 
			
		||||
                    if (result.status != "ok") return result;
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { status: "ok" };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import {
 | 
			
		|||
} from "rkgk/session.js";
 | 
			
		||||
import { debounce } from "rkgk/framework.js";
 | 
			
		||||
import { ReticleCursor } from "rkgk/reticle-renderer.js";
 | 
			
		||||
import { selfController } from "rkgk/painter.js";
 | 
			
		||||
 | 
			
		||||
const updateInterval = 1000 / 60;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -175,14 +176,8 @@ function readUrl(urlString) {
 | 
			
		|||
                user.reticle.setCursor(x, y);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (wallEvent.kind.event == "setBrush") {
 | 
			
		||||
                user.setBrush(wallEvent.kind.brush);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (wallEvent.kind.event == "plot") {
 | 
			
		||||
                for (let { x, y } of wallEvent.kind.points) {
 | 
			
		||||
                    user.renderBrushToChunks(wall, x, y);
 | 
			
		||||
                }
 | 
			
		||||
            if (wallEvent.kind.event == "interact") {
 | 
			
		||||
                user.simulate(wall, wallEvent.kind.interactions);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -230,30 +225,21 @@ function readUrl(urlString) {
 | 
			
		|||
        reportCursor(event.x, event.y);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let plotQueue = [];
 | 
			
		||||
    async function flushPlotQueue() {
 | 
			
		||||
        let points = plotQueue.splice(0, plotQueue.length);
 | 
			
		||||
        if (points.length != 0) {
 | 
			
		||||
            session.sendPlot(points);
 | 
			
		||||
    let interactionQueue = [];
 | 
			
		||||
    function flushInteractionQueue() {
 | 
			
		||||
        if (interactionQueue.length != 0) {
 | 
			
		||||
            session.sendInteraction(interactionQueue);
 | 
			
		||||
            interactionQueue.splice(0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setInterval(flushPlotQueue, updateInterval);
 | 
			
		||||
    setInterval(flushInteractionQueue, updateInterval);
 | 
			
		||||
 | 
			
		||||
    canvasRenderer.addEventListener(".paint", async (event) => {
 | 
			
		||||
        plotQueue.push({ x: event.x, y: event.y });
 | 
			
		||||
 | 
			
		||||
        if (currentUser.isBrushOk) {
 | 
			
		||||
            brushEditor.resetErrors();
 | 
			
		||||
 | 
			
		||||
            let result = currentUser.renderBrushToChunks(wall, event.x, event.y);
 | 
			
		||||
            if (result.status == "error") {
 | 
			
		||||
                brushEditor.renderHakuResult(
 | 
			
		||||
                    result.phase == "eval" ? "Evaluation" : "Rendering",
 | 
			
		||||
                    result.result,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    canvasRenderer.addEventListener(".interact", async (event) => {
 | 
			
		||||
        let result = await currentUser.haku.evalBrush(
 | 
			
		||||
            selfController(interactionQueue, wall, event),
 | 
			
		||||
        );
 | 
			
		||||
        brushEditor.renderHakuResult(result.phase == "eval" ? "Evaluation" : "Rendering", result);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
 | 
			
		||||
| 
						 | 
				
			
			@ -270,20 +256,23 @@ function readUrl(urlString) {
 | 
			
		|||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let previewResult = brushPreview.renderBrush(currentUser.haku);
 | 
			
		||||
        if (previewResult.status == "error") {
 | 
			
		||||
            brushEditor.renderHakuResult(
 | 
			
		||||
                previewResult.phase == "eval" ? "Evaluation" : "Rendering",
 | 
			
		||||
                previewResult.result,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        brushPreview.renderBrush(currentUser.haku).then((previewResult) => {
 | 
			
		||||
            if (previewResult.status == "error") {
 | 
			
		||||
                brushEditor.renderHakuResult(
 | 
			
		||||
                    previewResult.phase == "eval" ? "Evaluation" : "Rendering",
 | 
			
		||||
                    previewResult.result,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    compileBrush();
 | 
			
		||||
    brushEditor.addEventListener(".codeChanged", async () => {
 | 
			
		||||
        flushPlotQueue();
 | 
			
		||||
        compileBrush();
 | 
			
		||||
        session.sendSetBrush(brushEditor.code);
 | 
			
		||||
        interactionQueue.push({
 | 
			
		||||
            kind: "setBrush",
 | 
			
		||||
            brush: brushEditor.code,
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    session.eventLoop();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { Haku } from "rkgk/haku.js";
 | 
			
		||||
import { Painter } from "rkgk/painter.js";
 | 
			
		||||
import { ContKind, Haku } from "rkgk/haku.js";
 | 
			
		||||
import { renderToChunksInArea, dotterRenderArea } from "rkgk/painter.js";
 | 
			
		||||
 | 
			
		||||
export class User {
 | 
			
		||||
    nickname = "";
 | 
			
		||||
| 
						 | 
				
			
			@ -7,12 +7,11 @@ export class User {
 | 
			
		|||
    reticle = null;
 | 
			
		||||
 | 
			
		||||
    isBrushOk = false;
 | 
			
		||||
    simulation = null;
 | 
			
		||||
 | 
			
		||||
    constructor(wallInfo, nickname) {
 | 
			
		||||
        this.nickname = nickname;
 | 
			
		||||
 | 
			
		||||
        this.haku = new Haku(wallInfo.hakuLimits);
 | 
			
		||||
        this.painter = new Painter(wallInfo.paintArea);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +37,58 @@ export class User {
 | 
			
		|||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    simulate(wall, interactions) {
 | 
			
		||||
        console.group("simulate");
 | 
			
		||||
        for (let interaction of interactions) {
 | 
			
		||||
            if (interaction.kind == "setBrush") {
 | 
			
		||||
                this.simulation = null;
 | 
			
		||||
                this.setBrush(interaction.brush);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.isBrushOk) {
 | 
			
		||||
                if (this.simulation == null) {
 | 
			
		||||
                    console.log("no simulation -- beginning brush");
 | 
			
		||||
                    this.simulation = { renderArea: { left: 0, top: 0, right: 0, bottom: 0 } };
 | 
			
		||||
                    this.haku.beginBrush();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (interaction.kind == "dotter" && this.#expectContKind(ContKind.Dotter)) {
 | 
			
		||||
                    let dotter = {
 | 
			
		||||
                        fromX: interaction.from.x,
 | 
			
		||||
                        fromY: interaction.from.y,
 | 
			
		||||
                        toX: interaction.to.x,
 | 
			
		||||
                        toY: interaction.to.y,
 | 
			
		||||
                        num: interaction.num,
 | 
			
		||||
                    };
 | 
			
		||||
                    this.haku.contDotter(dotter);
 | 
			
		||||
                    this.simulation.renderArea = dotterRenderArea(wall, dotter);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) {
 | 
			
		||||
                    renderToChunksInArea(
 | 
			
		||||
                        wall,
 | 
			
		||||
                        this.simulation.renderArea,
 | 
			
		||||
                        (pixmap, translationX, translationY) => {
 | 
			
		||||
                            return this.haku.contScribble(pixmap, translationX, translationY);
 | 
			
		||||
                        },
 | 
			
		||||
                    );
 | 
			
		||||
                    console.log("ended simulation");
 | 
			
		||||
                    this.simulation = null;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        console.groupEnd();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #expectContKind(kind) {
 | 
			
		||||
        if (this.haku.expectedContKind() == kind) {
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error(`expected cont kind: ${kind}`);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class OnlineUsers extends EventTarget {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,43 +1,69 @@
 | 
			
		|||
export class Painter {
 | 
			
		||||
    constructor(paintArea) {
 | 
			
		||||
        this.paintArea = paintArea;
 | 
			
		||||
    }
 | 
			
		||||
import { listen } from "rkgk/framework.js";
 | 
			
		||||
 | 
			
		||||
    renderBrushToWall(haku, centerX, centerY, wall) {
 | 
			
		||||
        haku.resetVm();
 | 
			
		||||
 | 
			
		||||
        let evalResult = haku.evalBrush();
 | 
			
		||||
        if (evalResult.status != "ok")
 | 
			
		||||
            return { status: "error", phase: "eval", result: evalResult };
 | 
			
		||||
 | 
			
		||||
        let left = centerX - this.paintArea / 2;
 | 
			
		||||
        let top = centerY - this.paintArea / 2;
 | 
			
		||||
 | 
			
		||||
        let leftChunk = Math.floor(left / wall.chunkSize);
 | 
			
		||||
        let topChunk = Math.floor(top / wall.chunkSize);
 | 
			
		||||
        let rightChunk = Math.ceil((left + this.paintArea) / wall.chunkSize);
 | 
			
		||||
        let bottomChunk = Math.ceil((top + this.paintArea) / wall.chunkSize);
 | 
			
		||||
        for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
 | 
			
		||||
            for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
 | 
			
		||||
                let x = Math.floor(-chunkX * wall.chunkSize + centerX);
 | 
			
		||||
                let y = Math.floor(-chunkY * wall.chunkSize + centerY);
 | 
			
		||||
                let chunk = wall.getOrCreateChunk(chunkX, chunkY);
 | 
			
		||||
                chunk.markModified();
 | 
			
		||||
 | 
			
		||||
                let renderResult = haku.renderValue(chunk.pixmap, x, y);
 | 
			
		||||
                if (renderResult.status != "ok") {
 | 
			
		||||
                    return { status: "error", phase: "render", result: renderResult };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
function* chunksInRectangle(left, top, right, bottom, chunkSize) {
 | 
			
		||||
    let leftChunk = Math.floor(left / chunkSize);
 | 
			
		||||
    let topChunk = Math.floor(top / chunkSize);
 | 
			
		||||
    let rightChunk = Math.ceil(right / chunkSize);
 | 
			
		||||
    let bottomChunk = Math.ceil(bottom / chunkSize);
 | 
			
		||||
    for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
 | 
			
		||||
        for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
 | 
			
		||||
            yield [chunkX, chunkY];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let y = topChunk; y < bottomChunk; ++y) {
 | 
			
		||||
            for (let x = leftChunk; x < rightChunk; ++x) {
 | 
			
		||||
                let chunk = wall.getChunk(x, y);
 | 
			
		||||
                chunk.syncFromPixmap();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { status: "ok" };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderToChunksInArea(wall, renderArea, renderToPixmap) {
 | 
			
		||||
    for (let [chunkX, chunkY] of chunksInRectangle(
 | 
			
		||||
        renderArea.left,
 | 
			
		||||
        renderArea.top,
 | 
			
		||||
        renderArea.right,
 | 
			
		||||
        renderArea.bottom,
 | 
			
		||||
        wall.chunkSize,
 | 
			
		||||
    )) {
 | 
			
		||||
        let chunk = wall.getOrCreateChunk(chunkX, chunkY);
 | 
			
		||||
        let translationX = -chunkX * wall.chunkSize;
 | 
			
		||||
        let translationY = -chunkY * wall.chunkSize;
 | 
			
		||||
        let result = renderToPixmap(chunk.pixmap, translationX, translationY);
 | 
			
		||||
        chunk.markModified();
 | 
			
		||||
        if (result.status != "ok") return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { status: "ok" };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function dotterRenderArea(wall, dotter) {
 | 
			
		||||
    let halfPaintArea = wall.paintArea / 2;
 | 
			
		||||
    return {
 | 
			
		||||
        left: dotter.toX - halfPaintArea,
 | 
			
		||||
        top: dotter.toY - halfPaintArea,
 | 
			
		||||
        right: dotter.toX + halfPaintArea,
 | 
			
		||||
        bottom: dotter.toY + halfPaintArea,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function selfController(interactionQueue, wall, event) {
 | 
			
		||||
    let renderArea = null;
 | 
			
		||||
    return {
 | 
			
		||||
        async runScribble(renderToPixmap) {
 | 
			
		||||
            interactionQueue.push({ kind: "scribble" });
 | 
			
		||||
            if (renderArea != null) {
 | 
			
		||||
                return renderToChunksInArea(wall, renderArea, renderToPixmap);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.debug("render area is empty, nothing will be rendered");
 | 
			
		||||
            }
 | 
			
		||||
            return { status: "ok" };
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async runDotter() {
 | 
			
		||||
            let dotter = await event.continueAsDotter();
 | 
			
		||||
            interactionQueue.push({
 | 
			
		||||
                kind: "dotter",
 | 
			
		||||
                from: { x: dotter.fromX, y: dotter.fromY },
 | 
			
		||||
                to: { x: dotter.toX, y: dotter.toY },
 | 
			
		||||
                num: dotter.num,
 | 
			
		||||
            });
 | 
			
		||||
            renderArea = dotterRenderArea(wall, dotter);
 | 
			
		||||
            return dotter;
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -267,22 +267,13 @@ class Session extends EventTarget {
 | 
			
		|||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sendPlot(points) {
 | 
			
		||||
    sendInteraction(interactions) {
 | 
			
		||||
        console.log(interactions);
 | 
			
		||||
        this.#sendJson({
 | 
			
		||||
            request: "wall",
 | 
			
		||||
            wallEvent: {
 | 
			
		||||
                event: "plot",
 | 
			
		||||
                points,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sendSetBrush(brush) {
 | 
			
		||||
        this.#sendJson({
 | 
			
		||||
            request: "wall",
 | 
			
		||||
            wallEvent: {
 | 
			
		||||
                event: "setBrush",
 | 
			
		||||
                brush,
 | 
			
		||||
                event: "interact",
 | 
			
		||||
                interactions,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								static/signal.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								static/signal.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -28,6 +28,7 @@ export class Wall {
 | 
			
		|||
 | 
			
		||||
    constructor(wallInfo) {
 | 
			
		||||
        this.chunkSize = wallInfo.chunkSize;
 | 
			
		||||
        this.paintArea = wallInfo.paintArea;
 | 
			
		||||
        this.onlineUsers = new OnlineUsers(wallInfo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue