typst icon indicating copy to clipboard operation
typst copied to clipboard

References to floating figures point to wrong location

Open EpicEricEE opened this issue 1 year ago • 7 comments

Description

The location of a floating figure is set to where it is created, but not where it is actually laid out. This is visible in the location function, and also noticeable when clicking on a reference only to not end up at the figure.

#set page(height: 2.2cm, width: 8cm)

@figure

#figure(
  [#rect()<actual>],
  placement: top,
  caption: [A rectangle]
) <figure>

`figure:` #context query(<figure>).first().location().position() \
`rect:  ` #context query(<actual>).first().location().position()

image

Tried with 0.11.1 on the webapp, and on main with f91cad7d

Reproduction URL

No response

Operating system

Web app, Windows

Typst version

  • [X] I am using the latest version of Typst

EpicEricEE avatar Jun 09 '24 17:06 EpicEricEE

Closely related to https://github.com/typst/typst/issues/4322#issuecomment-2147712634

laurmaedje avatar Jun 10 '24 08:06 laurmaedje

This is pretty hard to fix because the location must be in-flow for counters to resolve correctly.

laurmaedje avatar Dec 04 '24 07:12 laurmaedje

I encountered the some problem on 0.13.0

cgahr avatar Mar 07 '25 13:03 cgahr

I encountered the some problem on 0.13.1 😢

MrAMS avatar Mar 21 '25 13:03 MrAMS

I've found a workaround, by using place(top: float: true, figure(...)), the location would be ok for the case in the issue(not sure for other cases). Not working with a show-set and I have no idea why.

sjfhsjfh avatar Mar 21 '25 16:03 sjfhsjfh

I've found a workaround, by using place(top: float: true, figure(...)), the location would be ok for the case in the issue(not sure for other cases). Not working with a show-set and I have no idea why.

This method works well for me. 👍 Here is a complete example:

#place(bottom, scope: "parent", float: true, [
#figure(
// ...
)<fig:name>
])

MrAMS avatar Mar 22 '25 00:03 MrAMS

A possible workaround is to create a show rule on ref so that it links to the figure's body instead of the figure itself. It's a bit convoluted, as it then also requires a show rule on floating figures to make getting the body's location even possible, but it seems to work.

(Note that this creates a nested figure, so any show rules using relative sizes such as show figure: set text(size: 0.8em) are applied twice, which in this case leads to a text size of 0.64em)

The show rules
#show figure: it => {
  if it.placement == none { return it }

  // Re-wrap placed figures to include metadata containing
  // the body's location.
  place(it.placement, float: true, scope: it.scope, block(width: 100%, {
    let fields = it.fields()
    let body = fields.remove("body")
    let label = fields.remove("label")
    let counter = fields.remove("counter")

    // Need to step back to keep the same number in the new figure.
    counter.update(n => n - 1)

    let meta = context metadata((
      figure-location: it.location(),
      body-location: here()
    ))
    
    figure(meta + body, ..fields, placement: none)
  })
}

#show ref: it => {
  let fig = it.element
  if fig == none { return it }
  if fig.func() != figure { return it }
  if fig.numbering == none { return it }
  if fig.placement == none { return it }

  // Rebuild reference from scratch.
  let num = numbering(fig.numbering, ..fig.counter.at(fig.location()))
  let supplement = (if it.supplement == auto { fig } else { it }).supplement
  if supplement not in (text(""), [], none) { supplement += [~] }

  // Use location of figure's body for linking.
  let location = query(metadata).find(data => (
    type(data.value) == dictionary
    and data.value.at("figure-location", default: none) == fig.location()
  )).value.body-location
  
  link(location, [#supplement#num])
}

EpicEricEE avatar Apr 23 '25 20:04 EpicEricEE

Still getting this issue using the following code, where the aqua figure is placed on page 1 and the orange figure on page 2 in all cases:

#set page(numbering: "1")

#outline(title: "List of figures", target: figure.where(kind: image))

#figure(
  placement: auto,
  box(fill: aqua, width: 90%, height: 50%),
  caption: [This is an aqua figure.]
)
#figure(
  placement: auto,
  box(fill: orange, width: 90%, height: 50%),
  caption: [This is an orange figure.]
)
Typst version Figure 1 claimed page number Figure 2 claimed page number
0.10.0 1 1
0.11.1 1 1
0.12.0 2 2
0.13.0 2 2
0.13.1 2 2

I used the following fix inspired by sjfhsjfh's comment above to successfully correct the outlined page numbers of my 102 (😢) figures in my thesis:

#place(auto, scope: "parent", float: true, [
// figure goes here
])

I can confirm that subpar environments have the same issue as figures and are solved in the same way. Also, I get better placement by using only the place command above and removing the "placement: auto" from the figure or subpar environment.

uzgit avatar May 05 '25 03:05 uzgit

Faced the same issue in v0.13.1, the place workaround worked for me!

AntoniosBarotsis avatar May 15 '25 11:05 AntoniosBarotsis

Facing the same issue in v0.14.0

dunzhichen avatar Oct 29 '25 03:10 dunzhichen

The show rules

The workaround does the job, but the code is broken without ).

Fixed and formatted
#show figure: it => {
  if it.placement == none { return it }

  // Re-wrap placed figures to include metadata containing
  // the body's location.
  place(it.placement, float: true, scope: it.scope, block(width: 100%, {
    let fields = it.fields()
    let body = fields.remove("body")
    let label = fields.remove("label")
    let counter = fields.remove("counter")

    // Need to step back to keep the same number in the new figure.
    counter.update(n => n - 1)

    let meta = context metadata((
      figure-location: it.location(),
      body-location: here(),
    ))

    figure(meta + body, ..fields, placement: none)
  }))
}

#show ref: it => {
  let fig = it.element
  if fig == none { return it }
  if fig.func() != figure { return it }
  if fig.numbering == none { return it }
  if fig.placement == none { return it }

  // Rebuild reference from scratch.
  let num = numbering(fig.numbering, ..fig.counter.at(fig.location()))
  let supplement = (if it.supplement == auto { fig } else { it }).supplement
  if supplement not in (text(""), [], none) { supplement += [~] }

  // Use location of figure's body for linking.
  let location = query(metadata)
    .find(data => (
      type(data.value) == dictionary
        and data.value.at("figure-location", default: none) == fig.location()
    ))
    .value
    .body-location

  link(location, [#supplement#num])
}

Andrew15-5 avatar Nov 21 '25 23:11 Andrew15-5