04 Note and Panel Count
In this chapter, we will add PreviewNote and size the canvas from the latest note instead of a fixed count.
Note Archetype
First, create guide/preview/note.py and set up PreviewNote:
python
from sonolus.script.archetype import PreviewArchetype
from guide.lib import archetype_names
class PreviewNote(PreviewArchetype):
name = archetype_names.NOTEThe shared name lets PreviewNote handle the same Note entities as play and watch modes.
Then register it in the archetypes list in guide/preview/mode.py:
python
from guide.preview.note import PreviewNote
preview_mode = PreviewMode(
archetypes=[
PreviewInitialization,
PreviewStage,
PreviewNote,
],
skin=Skin,
)Duration
If we know the duration, we can use it to calculate the panel count.
One simple way to obtain the duration is to store it in Level Data and update it as each Note is preprocessed. The level_data decorator creates this shared data. Add this to guide/preview/chart.py:
python
from sonolus.script.globals import level_data
@level_data
class Chart:
duration: floatUpdating Duration
To calculate a Note's time, we need its beat just as we did in play mode. The StandardImport values declare imported Entity Data fields with standard Sonolus names. In guide/preview/note.py, add the StandardImport import and the beat field to PreviewNote:
python
from sonolus.script.archetype import PreviewArchetype, StandardImport
class PreviewNote(PreviewArchetype):
beat: StandardImport.BEATThe numeric fields in Chart start at 0. In the same file, add a preprocess callback that uses max to keep the latest note time in Chart.duration:
python
from sonolus.script.timing import beat_to_time
from guide.preview.chart import Chart
class PreviewNote(PreviewArchetype):
# ...
def preprocess(self):
Chart.duration = max(Chart.duration, beat_to_time(self.beat))Panel Count
In guide/preview/chart.py, delete PANEL_COUNT = 10, import floor, and add panel_count. Dividing the duration by the 2-second panel height locates the last event's panel. Adding 1 includes that panel when the event is exactly on its bottom boundary, while max keeps an empty or zero-duration chart at one panel:
python
from math import floor
def panel_count():
return max(1, floor(Chart.duration / PANEL_HEIGHT) + 1)In guide/preview/stage.py, replace PANEL_COUNT with panel_count in the import from guide.preview.chart. In render_panels, change for i in range(PANEL_COUNT): to for i in range(panel_count()):. Keep the existing panel drawing code inside the loop.
Canvas
Finally, calculate the canvas size after the notes have updated the duration. Preprocess callbacks run from lowest to highest order, and entity preprocess callbacks use order 0 by default. Assigning order 1 to PreviewStage.preprocess makes the stage run after all note callbacks. The BPM and time scale callbacks added later will also use order 0, so they will contribute to the chart bounds before the stage reads them.
In guide/preview/stage.py, add the following imports and callback to PreviewStage:
python
from sonolus.script.archetype import PreviewArchetype, callback
from sonolus.script.runtime import ScrollDirection, canvas, screen
from guide.preview.chart import PANEL_HEIGHT, PANEL_WIDTH, panel_count
class PreviewStage(PreviewArchetype):
@callback(order=1)
def preprocess(self):
canvas().scroll_direction = ScrollDirection.LEFT_TO_RIGHT
canvas().size = panel_count() * PANEL_WIDTH * screen().h / 20
# ...Clean up guide/preview/initialization.py: delete the canvas().update(...) block, remove PANEL_COUNT from the chart import, then remove ScrollDirection and canvas from the runtime import. Keep PANEL_HEIGHT, PANEL_WIDTH, screen, and set_skin_transform. The coordinate system still needs them.
Checkpoint
Refresh Preview. The canvas should now end after the panel containing the sample level's last note, rather than after 10 panels. A level whose last note is exactly on a panel boundary should still include the new panel that starts there.