Make a bundle reopenable, and number slices
A bundle was a one-way trip. The slice images are output and the cuts that produced them lived only in the producer's own project file, so a bundle someone handed you meant cutting the score again from scratch. The manifest now carries the geometry, in a `source` block: per page the cut polylines, skew, levels and content rectangle, all in normalised coordinates so they survive any render resolution, and per slice the page and slot it came from. Discards are stated by omission — a slot no slice claims was discarded — since shipping a discarded slice's image would defeat discarding it. `noteman-slicer open song.zip` unpacks the archived PDF, rebuilds the project from that geometry, restores markers, engravings and the title block, and opens the editor. Jump destinations go back from an array index to the (page, slot) the editor works in. The images in the zip are discarded: the PDF is what the pipeline renders from. Re-exporting a reopened bundle reproduces its manifest exactly. It refuses to overwrite a PDF or project file already sitting there, because the obvious place to unpack is where someone's unfinished cuts live. Separately, every slice can now carry the measure it starts at, not just a re-engraved one — a scanned system is numbered in the score the same way, and noteman wants to answer "take it from bar 33" about either. It moves off the replacement onto the page, alongside markers and discards, and out of the bundle's engraving object onto the slice.
This commit is contained in:
@@ -147,10 +147,6 @@ class Replacement:
|
||||
voices: list[Voice] = field(default_factory=list)
|
||||
key: str | None = None
|
||||
time: str | None = None
|
||||
# The measure this system starts at, printed above its first bar the way a
|
||||
# score numbers its systems. Per slice and nothing else: it is the one thing
|
||||
# about a replacement that cannot be inherited or guessed.
|
||||
bar: int | None = None
|
||||
# The printed score repeats the key signature at every system but not the
|
||||
# time signature, so a re-engraved middle slice must not show one.
|
||||
print_time: bool = False
|
||||
@@ -168,6 +164,11 @@ class Page:
|
||||
# A re-engraved system per slice, when the scan is past saving. None for
|
||||
# the ordinary case, which is nearly all of them.
|
||||
replacements: list[Replacement | None] = field(default_factory=lambda: [None])
|
||||
# The measure each slice starts at, when it is known. A property of the
|
||||
# slice rather than of a replacement: a scanned system has a bar number
|
||||
# printed on it just as an engraved one does, and noteman wants to say
|
||||
# "from bar 33" about either.
|
||||
bars: list[int | None] = field(default_factory=lambda: [None])
|
||||
content_rect: tuple[float, float, float, float] | None = None
|
||||
levels: tuple[int, int] | None = None
|
||||
|
||||
@@ -193,6 +194,10 @@ class Page:
|
||||
self.discards.insert(index, self.discards[index])
|
||||
self.markers.insert(index + 1, [])
|
||||
self.replacements.insert(index + 1, None)
|
||||
# The upper half keeps the number: it still starts where the slice did.
|
||||
# What bar the new lower half starts at needs counting, which is the
|
||||
# user's job.
|
||||
self.bars.insert(index + 1, None)
|
||||
return index
|
||||
|
||||
def remove_cut(self, index: int) -> None:
|
||||
@@ -205,6 +210,8 @@ class Page:
|
||||
# Two engraved halves cannot be merged, so the upper one wins.
|
||||
below = self.replacements.pop(index + 1)
|
||||
self.replacements[index] = self.replacements[index] or below
|
||||
# The merged slice starts where the upper half did.
|
||||
self.bars.pop(index + 1)
|
||||
|
||||
def remember_clefs(self, project: Project, slot: int) -> None:
|
||||
"""Carry this slice's clefs forward as the song's defaults."""
|
||||
@@ -296,6 +303,7 @@ class Project:
|
||||
discards=discards,
|
||||
markers=[[] for _ in discards],
|
||||
replacements=[None] * len(discards),
|
||||
bars=[None] * len(discards),
|
||||
# Per page, not per song: scans drift, so the margin junk
|
||||
# sits in a different place on each one.
|
||||
content_rect=detection.content,
|
||||
@@ -360,10 +368,10 @@ class Project:
|
||||
**({"key": r.key} if r.key else {}),
|
||||
**({"time": r.time} if r.time else {}),
|
||||
**({"print_time": True} if r.print_time else {}),
|
||||
**({"bar": r.bar} if r.bar else {}),
|
||||
}
|
||||
for r in page.replacements
|
||||
],
|
||||
"bars": page.bars,
|
||||
"content_rect": list(page.content_rect) if page.content_rect else None,
|
||||
"levels": list(page.levels) if page.levels else None,
|
||||
}
|
||||
@@ -417,10 +425,19 @@ class Project:
|
||||
key=r.get("key"),
|
||||
time=r.get("time"),
|
||||
print_time=r.get("print_time", False),
|
||||
bar=r.get("bar"),
|
||||
)
|
||||
for r in page.get("replacements", [None] * len(page["discards"]))
|
||||
],
|
||||
bars=page.get(
|
||||
"bars",
|
||||
# Before bar numbers were a property of the slice they lived
|
||||
# on the replacement, so an engraved slice is where an older
|
||||
# project keeps one.
|
||||
[
|
||||
r.get("bar") if isinstance(r, dict) else None
|
||||
for r in page.get("replacements", [None] * len(page["discards"]))
|
||||
],
|
||||
),
|
||||
content_rect=tuple(page["content_rect"]) if page["content_rect"] else None,
|
||||
levels=tuple(page["levels"]) if page["levels"] else None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user