Has the state a limit of 4 entries per array?

Hi all,

i have a question regarding the state.
Some context: I try creating an exam containing some figures the students should draw stuff in. Since some of them will mess up some sketches unreadable i want to have some figure sheet containing all “messupable” graphics. For that i have a function, that takes the graphic and its heading counter at the current context and appends it to an state array and prints the graphic itself:

#let _all_graphics = state("all_graphics", ())

#let messupable_graphic(body) = context{
  let all_graphics = _all_graphics.get()
  let section_number = counter(heading).get()
  let state_element = (section_number, body)

  all_graphics.push(state_element)
  _all_graphics.update(all_graphics)
  
  [Section number: #section_number \ \ ]
  [State element: #state_element \ \ ]
  [State input: #all_graphics \ \ ]

  body
}

Called it should look like this:

#import "@preview/cetz:0.3.1"

#let question1(color) = context {
  let graphic = context {
    cetz.canvas(length: 1cm, {
      import cetz.draw: *
      rect((0, 0), (1, 1), fill: color)
    })
  }
  [
    === foo. \
    #messupable_graphic(graphic)
  ]
}

This state will then be used in a figure template (which is called at the end of the exam template) like this: (reducing the heading count by 1 setting a heading and printing the graphic.)

  #let all_graphics = _all_graphics.final()
  #for graphic in all_graphics {
    let heading_counter = graphic.at(0)
    if(heading_counter.len() > 1){
      heading_counter.at(-1) -= 1
    }
    counter(heading).update(heading_counter)
    [=== ]
    graphic.at(1)
  }

When i call it up to 3 times everything is fine and as expected. But with the fourth call the first heading counter gets wrong and with the fifth and more the first value is lost.
Is there a limit to the state that it can only hold 4 element stuff (same problem occurs when i try using a dict or make the state a list containing the actual all_graphics array as second entry, this second entry also can only hold 4 elements) or am i missing some context somewhere?

question

Full code for a minimal running example:

#import "@preview/cetz:0.3.1"

#let _all_graphics = state("all_graphics", ())

#let messupable_graphic(body) = context{
  let all_graphics = _all_graphics.get()
  let section_number = counter(heading).get()
  let state_element = (section_number, body)

  all_graphics.push(state_element)
  _all_graphics.update(all_graphics)
  
  [Section number: #section_number \ \ ]
  [State element: #state_element \ \ ]
  [State input: #all_graphics \ \ ]

  body

}

#let question1(color) = context {

  let graphic = context {
    cetz.canvas(length: 1cm, {
      import cetz.draw: *

      rect((0, 0), (1, 1), fill: color)
    })
  }

  [
    === foo. \
    #messupable_graphic(graphic)
  ]

}

#let figure_template(
  body,
) = context {

  set heading(numbering: (..nums) => {
    let nums = nums.pos()
    // the zero-based level of the current heading
    let level = nums.len() - 1

    if level == 2 {
      numbering("I.1", nums.at(nums.len() - 2), nums.last())
    }
  })

  let all_graphics = _all_graphics.final()
  for graphic in all_graphics {
    let heading_counter = graphic.at(0)
    if(heading_counter.len() > 1){
      heading_counter.at(-1) -= 1
    }
    counter(heading).update(heading_counter)
    [=== ]
    graphic.at(1)
  }

}

#let exam_template(body) = context {

  show heading.where(level: 3): it => {
      set text(luma(50), weight: 700)
      [Question ] + counter(heading).display(it.numbering)
      if (measure(it.body).width > 0.1cm) {
        set text(luma(50), weight: 400) 
        [ -- ] + it.body
      }
    }
  set heading(numbering: (..nums) => {
      let nums = nums.pos()
      // the zero-based level of the current heading
      let level = nums.len() - 1
      
      if level == 2 {
        numbering("I.1", nums.at(nums.len() - 2), nums.last())
      } 

    })

  body

  context figure_template()[]
}

#show: exam_template.with()

#context [
  = 
  == 
  #context{
    let current_state = _all_graphics.at(here())
    [State before green: #current_state \ \ ]
  }
  #question1(green)
  #context{
    let current_state = _all_graphics.at(here())
    [State after green: #current_state \ \ ]
  }
  #question1(red)
  #context{
    let current_state = _all_graphics.at(here())
    [State after red: #current_state \ \ ]
  }
  #question1(blue)
  
  #context{
    let current_state = _all_graphics.at(here())
    [State after blue: #current_state \ \ ]
  }

  #question1(gray)
  
  #context{
    let current_state = _all_graphics.at(here())
    [State after gray: #current_state \ \ ]
  }
  == 
  #question1(yellow)
  
  #context{
    let current_state = _all_graphics.at(here())
    [State after yellow: #current_state \ \ ]
  }

  #question1(orange)

  #context{
    let current_state = _all_graphics.at(here())
    [State after orange: #current_state \ \ ]
  }
  #line(length: 100%)
]

I admit I haven’t read your whole post, but this sounds suspiciously like the problem also discussed here:

Maybe this helps you already :slight_smile:

1 Like

Hi @Sebastian_Vogt,

i haven’t had the time to fully read through your code. But no the state is not limited.

Doing state based content creation in a with-based template leads to a recursive situation which usually leads to the Warning:

Layout did not converge within 5 attempts

After which the typst compiler stops to render the document.

1 Like

Sounds like the same problem. But I struggle fixing it:
I tried
_all_graphics.update(x => x.push(state_element)) and remove all other get/and update stuff. Then i get “type none has no push method”. (It somehow seems to get called (), none, (), none … ) Even adding a guard like that does not help: _all_graphics.update(x => if(type(x) == none){x} else {x.push(state_element)}). Trying to cast x as an array gives me “Cannot mutate a temporary value”

#let messupable_graphic(body) = context{
  let section_number = counter(heading).get()
  let state_element = (section_number, body)

  _all_graphics.update(x => x.push(state_element))
  
  [Section number: #section_number \ \ ]
  [State element: #state_element \ \ ]

  body
}```

the problem here is that push() modifies the array, but doesn’t return it. You can rewrite this in two ways:

x => {
  x.push(new-element)
  x
}

whis is your approach but with the missing result, or

x => (..x, new-element)
// or
x => x + (new-element,)

which doesn’t “modify” the original array but constructs a new one with the additional element

2 Likes

Thank you so much. What a dumb newbie mistake -.-

1 Like

For what it’s worth: I don’t think this is a dumb mistake. This is easily one of the most tricky parts of Typst.

2 Likes

For the non coverage stuff I agree. But here i meant the push does not give a return value and thus cannot be used alone as update function :sweat_smile: