Skip to content

Commit

Permalink
ADD misc fixes and updates for beta-6 (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlahmidi authored Apr 22, 2024
1 parent 0b63e2f commit c0b4ad9
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 110 deletions.
7 changes: 6 additions & 1 deletion docs/usage/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ clip.render("./out.#.png", start=10, end=22)
!!! warning

For more details on how we handle frame ranges in the projects and clips, please check the sections below, which go
into detail about how TVPaint handles ranges and how we changed taht to fit our needs
into detail about how TVPaint handles ranges and how we changed it to fit our needs.

## Sequence parsing with Fileseq

Expand Down Expand Up @@ -396,3 +396,8 @@ print(c2.timeline_end) # => 63
invalid range anyways, then consider using these wrapped functions directly (`george.tv_project_save_sequence`
, `george.tv_save_sequence`). This also means that you will have to do the range conversions yourself, as shown
in the examples above.

!!! Warning

Even tough pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still
encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytvpaint"
version = "1.0.0b5"
version = "1.0.0b6"
description = "Python scripting for TVPaint"
authors = [
"Brunch Studio Developers <[email protected]>",
Expand Down
119 changes: 77 additions & 42 deletions pytvpaint/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ def project(self) -> Project:

@property
def scene(self) -> Scene:
"""The clip's scene."""
"""The clip's scene.
Raises:
ValueError: if clip cannot be found in the project
"""
for scene in self.project.scenes:
for other_clip in scene.clips:
if other_clip == self:
Expand All @@ -133,7 +137,11 @@ def camera(self) -> Camera:

@property
def position(self) -> int:
"""The position of the clip in the scene."""
"""The position of the clip in the scene.
Raises:
ValueError: if clip cannot be found in the project
"""
for pos, clip_id in enumerate(self.scene.clip_ids):
if clip_id == self.id:
return pos
Expand All @@ -142,6 +150,7 @@ def position(self) -> int:
@position.setter
def position(self, value: int) -> None:
"""Set the position of the clip in the scene."""
value = max(0, value)
george.tv_clip_move(self.id, self.scene.id, value)

@property
Expand Down Expand Up @@ -334,7 +343,11 @@ def layer_names(self) -> Iterator[str]:

@property
def current_layer(self) -> Layer:
"""Get the current layer in the clip."""
"""Get the current layer in the clip.
Raises:
ValueError: if clip cannot be found in the project
"""
for layer in self.layers:
if layer.is_current:
return layer
Expand Down Expand Up @@ -371,7 +384,7 @@ def load_media(
start_count: tuple[int, int] | None = None,
stretch: bool = False,
time_stretch: bool = False,
pre_load: bool = False,
preload: bool = False,
with_name: str = "",
field_order: george.FieldOrder = george.FieldOrder.LOWER,
) -> Layer:
Expand All @@ -382,7 +395,7 @@ def load_media(
start_count: the start and number of image of sequence to load. Defaults to None.
stretch: Stretch each image to the size of the layer. Defaults to None.
time_stretch: Once loaded, the layer will have a new number of image corresponding to the project framerate. Defaults to None.
pre_load: Load all the images in memory, no more reference on the files. Defaults to None.
preload: Load all the images in memory, no more reference on the files. Defaults to None.
with_name: the name of the new layer
field_order: the field order. Defaults to None.
Expand All @@ -397,7 +410,7 @@ def load_media(
field_order,
stretch,
time_stretch,
pre_load,
preload,
)

new_layer = Layer.current_layer()
Expand Down Expand Up @@ -445,7 +458,7 @@ def render(
use_camera: bool = False,
layer_selection: list[Layer] | None = None,
alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY,
background_mode: george.BackgroundMode = george.BackgroundMode.NONE,
background_mode: george.BackgroundMode | None = None,
format_opts: list[str] | None = None,
) -> None:
"""Render the clip to a single frame or frame sequence or movie.
Expand All @@ -457,7 +470,7 @@ def render(
use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False.
layer_selection: list of layers to render, if None render all of them. Defaults to None.
alpha_mode: the alpha mode for rendering. Defaults to george.AlphaSaveMode.PREMULTIPLY.
background_mode: the background mode for rendering. Defaults to george.BackgroundMode.NONE.
background_mode: the background mode for rendering. Defaults to None.
format_opts: custom format options. Defaults to None.
Raises:
Expand All @@ -467,8 +480,12 @@ def render(
Note:
This functions uses the clip's range as a basis (start-end). This is different from a project range, which
uses the project timeline. For more details on the differences in frame range and the timeline in TVPaint,
please check the `Limitations` section of the documentation.
uses the project timeline. For more details on the differences in frame ranges and the timeline in TVPaint,
please check the `Usage/Rendering` section of the documentation.
Warning:
Even tough pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still
encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason.
"""
default_start = self.mark_in or self.start
default_end = self.mark_out or self.end
Expand All @@ -491,6 +508,7 @@ def export_tvp(self, export_path: Path | str) -> None:
"""Exports the clip in .tvp format which can be imported as a project in TVPaint.
Raises:
ValueError: if output extension is not (.tvp)
FileNotFoundError: if the render failed and no files were found on disk
"""
export_path = Path(export_path)
Expand All @@ -515,7 +533,7 @@ def export_json(
file_pattern: str = r"[%3ii] %ln",
layer_selection: list[Layer] | None = None,
alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY,
background_mode: george.BackgroundMode = george.BackgroundMode.NONE,
background_mode: george.BackgroundMode | None = None,
format_opts: list[str] | None = None,
all_images: bool = False,
ignore_duplicates: bool = False,
Expand All @@ -529,13 +547,13 @@ def export_json(
file_pattern: the file name pattern (%li: layer index, %ln: layer name, %ii: image index, %in: image name, %fi: file index (added in 11.0.8)). Defaults to None.
layer_selection: list of layers to render or all if None. Defaults to None.
alpha_mode: the export alpha mode. Defaults to george.AlphaSaveMode.PREMULTIPLY.
background_mode: the export background mode. Defaults to george.BackgroundMode.NONE.
background_mode: the export background mode. Defaults to None.
format_opts: custom format options. Defaults to None.
all_images: export all images (not only the instances). Defaults to False.
ignore_duplicates: Ignore duplicates images. Defaults to None.
Raises:
FileNotFoundError: if the render failed and no files were found on disk
FileNotFoundError: if the export failed and no files were found on disk
"""
export_path = Path(export_path)
export_path.parent.mkdir(exist_ok=True, parents=True)
Expand Down Expand Up @@ -567,7 +585,7 @@ def export_psd(
end: int | None = None,
layer_selection: list[Layer] | None = None,
alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY,
background_mode: george.BackgroundMode = george.BackgroundMode.NONE,
background_mode: george.BackgroundMode | None = None,
format_opts: list[str] | None = None,
) -> None:
"""Save the current clip as a PSD.
Expand All @@ -579,8 +597,11 @@ def export_psd(
end: the end frame. Defaults to None.
layer_selection: layers to render. Defaults to None (render all the layers).
alpha_mode: the alpha save mode. Defaults to george.AlphaSaveMode.PREMULTIPLY.
background_mode: the export background mode. Defaults to george.BackgroundMode.NONE.
background_mode: the export background mode. Defaults to None.
format_opts: custom format options. Defaults to None.
Raises:
FileNotFoundError: if the export failed and no files were found on disk
"""
start = start or self.mark_in or self.start
end = end or self.mark_out or self.end
Expand Down Expand Up @@ -622,7 +643,7 @@ def export_csv(
exposure_label: str = "",
layer_selection: list[Layer] | None = None,
alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY,
background_mode: george.BackgroundMode = george.BackgroundMode.NONE,
background_mode: george.BackgroundMode | None = None,
format_opts: list[str] | None = None,
) -> None:
"""Save the current clip as a CSV.
Expand All @@ -634,7 +655,7 @@ def export_csv(
exposure_label: give a label when the image is an exposure. Defaults to None.
layer_selection: layers to render. Defaults to None (render all the layers).
alpha_mode: the alpha save mode. Defaults to george.AlphaSaveMode.PREMULTIPLY.
background_mode: the export background mode. Defaults to george.BackgroundMode.NONE.
background_mode: the export background mode. Defaults to None.
format_opts: custom format options. Defaults to None.
Raises:
Expand Down Expand Up @@ -664,7 +685,7 @@ def export_sprites(
space: int = 0,
layer_selection: list[Layer] | None = None,
alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY,
background_mode: george.BackgroundMode = george.BackgroundMode.NONE,
background_mode: george.BackgroundMode | None = None,
format_opts: list[str] | None = None,
) -> None:
"""Save the current clip as sprites in one image.
Expand All @@ -675,8 +696,11 @@ def export_sprites(
space: the space between each sprite in the image. Defaults to None.
layer_selection: layers to render. Defaults to None (render all the layers).
alpha_mode: the alpha save mode. Defaults to george.AlphaSaveMode.PREMULTIPLY.
background_mode: the export background mode. Defaults to george.BackgroundMode.NONE.
background_mode: the export background mode. Defaults to None.
format_opts: custom format options. Defaults to None.
Raises:
FileNotFoundError: if the export failed and no files were found on disk
"""
export_path = Path(export_path)
save_format = george.SaveFormat.from_extension(export_path.suffix)
Expand All @@ -702,7 +726,7 @@ def export_flix(
send: bool = False,
layer_selection: list[Layer] | None = None,
alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY,
background_mode: george.BackgroundMode = george.BackgroundMode.NONE,
background_mode: george.BackgroundMode | None = None,
format_opts: list[str] | None = None,
) -> None:
"""Save the current clip for Flix.
Expand All @@ -716,11 +740,12 @@ def export_flix(
send: open a browser with the prefilled url. Defaults to None.
layer_selection: layers to render. Defaults to None (render all the layers).
alpha_mode: the alpha save mode. Defaults to george.AlphaSaveMode.PREMULTIPLY.
background_mode: the export background mode. Defaults to george.BackgroundMode.NONE.
background_mode: the export background mode. Defaults to None.
format_opts: custom format options. Defaults to None.
Raises:
ValueError: if the extension is not .xml
FileNotFoundError: if the export failed and no files were found on disk
"""
export_path = Path(export_path)

Expand Down Expand Up @@ -768,8 +793,12 @@ def mark_in(self) -> int | None:
@set_as_current
def mark_in(self, value: int | None) -> None:
"""Set the mark int value of the clip or None to clear it."""
action = george.MarkAction.CLEAR if value is None else george.MarkAction.SET
value = value or self.mark_in or 0
if value is None:
action = george.MarkAction.CLEAR
value = self.mark_in or 0
else:
action = george.MarkAction.SET
value = value

frame = value - self.project.start_frame
george.tv_mark_in_set(
Expand All @@ -789,8 +818,12 @@ def mark_out(self) -> int | None:
@set_as_current
def mark_out(self, value: int | None) -> None:
"""Set the mark in of the clip or None to clear it."""
action = george.MarkAction.CLEAR if value is None else george.MarkAction.SET
value = value or self.mark_out or 0
if value is None:
action = george.MarkAction.CLEAR
value = self.mark_out or 0
else:
action = george.MarkAction.SET
value = value

frame = value - self.project.start_frame
george.tv_mark_out_set(
Expand All @@ -805,27 +838,26 @@ def layer_colors(self) -> Iterator[LayerColor]:
for color_index in range(26):
yield LayerColor(color_index=color_index, clip=self)

def set_layer_color(
self,
index: int,
color: george.RGBColor,
name: str | None = None,
) -> None:
def set_layer_color(self, layer_color: LayerColor) -> None:
"""Set the layer color at the provided index.
Args:
index: the layer color index
color: the new color
name: the name to change. Defaults to None.
layer_color: the layer color instance.
"""
george.tv_layer_color_set_color(self.id, index, color, name)
george.tv_layer_color_set_color(
self.id, layer_color.index, layer_color.color, layer_color.name
)

def get_layer_color(
self,
by_index: int | None = None,
by_name: str | None = None,
) -> LayerColor:
"""Get a layer color by index or name."""
) -> LayerColor | None:
"""Get a layer color by index or name.
Raises:
ValueError: if none of the arguments `by_index` and `by_name` where provided
"""
if not by_index and by_name:
raise ValueError(
"At least one value (by_index or by_name) must be provided"
Expand All @@ -837,9 +869,7 @@ def get_layer_color(
try:
return next(c for c in self.layer_colors if c.name == by_name)
except StopIteration:
raise ValueError(
f"No LayerColor found with name ({by_name}) in Clip ({self.name})"
)
return None

@property
def bookmarks(self) -> Iterator[int]:
Expand Down Expand Up @@ -889,11 +919,16 @@ def get_sound(
by_id: int | None = None,
by_path: Path | str | None = None,
) -> ClipSound | None:
"""Get a clip sound by id or by path."""
"""Get a clip sound by id or by path.
Raises:
ValueError: if sound object could not be found in clip
"""
for sound in self.sounds:
if (by_id and sound.id == by_id) or (by_path and sound.path == by_path):
return sound
raise ValueError("Can't find sound")

return None

def add_sound(self, sound_path: Path | str) -> ClipSound:
"""Adds a new clip soundtrack."""
Expand Down
4 changes: 2 additions & 2 deletions pytvpaint/george/grg_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,8 @@ class SaveFormat(Enum):
@classmethod
def from_extension(cls, extension: str) -> SaveFormat:
"""Returns the correct tvpaint format value from a string extension."""
extension = extension.replace(".", "").lower()
if not hasattr(SaveFormat, extension.upper()):
extension = extension.replace(".", "").upper()
if not hasattr(SaveFormat, extension):
raise ValueError(
f"Could not find format ({extension}) in accepted formats ({SaveFormat})"
)
Expand Down
Loading

0 comments on commit c0b4ad9

Please sign in to comment.