Skip to content

Commit 78297c8

Browse files
Merge branch 'main' into vulkna-10bit-windows
2 parents 0be0c50 + 3dad8d0 commit 78297c8

6 files changed

Lines changed: 356 additions & 175 deletions

File tree

src/plugins/rv-packages/otio_reader/annotation_hook.py

Lines changed: 220 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,166 @@ def _transform_otio_to_world_coordinate(point):
8282
return (world_coordinate_x, world_coordinate_y, world_coordinate_width)
8383

8484

85+
def _get_source_image_height() -> float:
86+
"""Return the pixel height of the first media source. Falls back to 1080."""
87+
try:
88+
media_switch = commands.nodeConnections("MediaTrack")[0][0]
89+
media_source_group = commands.nodeConnections(media_switch)[0][0]
90+
first_source_node = extra_commands.nodesInGroupOfType(media_source_group, "RVFileSource")[0]
91+
media_info = commands.sourceMediaInfo(first_source_node)
92+
h = float(media_info.get("height", 0))
93+
if h > 1:
94+
return h
95+
except Exception:
96+
pass
97+
return 1080.0
98+
99+
100+
def _transform_pos(x, y):
101+
"""Transform a 2D OTIO position to RV WCS. Returns (wcs_x, wcs_y) or None."""
102+
result = _transform_otio_to_world_coordinate(otio.schemadef.Point.Point(x=x, y=y, width=0.0))
103+
if result is None:
104+
return None
105+
return result[0], result[1]
106+
107+
108+
def _transform_scalar(w):
109+
"""Scale a scalar OTIO distance to RV WCS via the x-axis scale, or return the raw value."""
110+
result = _transform_otio_to_world_coordinate(otio.schemadef.Point.Point(x=0.0, y=0.0, width=float(w)))
111+
return result[2] if result is not None else float(w)
112+
113+
114+
def _add_shape_to_frame_order(paint_node, stroke_id, frame, prefix):
115+
"""Insert a shape into the frame draw-order list."""
116+
frame_component = f"{paint_node}.frame:{frame}"
117+
if not commands.propertyExists(f"{frame_component}.order"):
118+
commands.newProperty(f"{frame_component}.order", commands.StringType, 1)
119+
commands.insertStringProperty(f"{frame_component}.order", [f"{prefix}:{stroke_id}:{frame}:annotation"])
120+
121+
122+
def _create_bbox_shape(layer, paint_node, stroke_id, frame, shape_type):
123+
"""Create an RVPaint rect: or ellipse: node from a shape layer."""
124+
prefix = "rect" if shape_type.startswith("Rectangle.") else "ellipse"
125+
shape_component = f"{paint_node}.{prefix}:{stroke_id}:{frame}:annotation"
126+
127+
min_pos = _transform_pos(layer.min.x, layer.min.y)
128+
max_pos = _transform_pos(layer.max.x, layer.max.y)
129+
if min_pos is None or max_pos is None:
130+
logging.warning(f"annotation_hook: could not transform bbox coords for {shape_component} — skipping")
131+
return
132+
133+
border_width = float(layer.border_width) if layer.border_width is not None else 0.0
134+
wcs_border_width = _transform_scalar(border_width)
135+
136+
corner_radius = getattr(layer, "corner_radius", None)
137+
wcs_corner_radius = float(corner_radius) if corner_radius is not None else 0.0
138+
139+
effectHook.add_rv_effect_props(
140+
shape_component,
141+
{
142+
"min": list(min_pos),
143+
"max": list(max_pos),
144+
"innerColor": [float(c) for c in layer.inner_color],
145+
"borderColor": [float(c) for c in layer.border_color],
146+
"borderWidth": [wcs_border_width],
147+
"cornerRadius": [wcs_corner_radius],
148+
"startFrame": frame,
149+
"duration": 1,
150+
"eye": 2,
151+
"softDeleted": layer.visible is False,
152+
"uuid": [layer.id],
153+
},
154+
)
155+
156+
if layer.visible is not False:
157+
_add_shape_to_frame_order(paint_node, stroke_id, frame, prefix)
158+
159+
160+
def _create_point_pair_shape(layer, paint_node, stroke_id, frame, shape_type):
161+
"""Create an RVPaint arrow: or line: node from a shape layer."""
162+
prefix = "arrow" if shape_type.startswith("Arrow.") else "line"
163+
shape_component = f"{paint_node}.{prefix}:{stroke_id}:{frame}:annotation"
164+
165+
start_pos = _transform_pos(layer.start_position.x, layer.start_position.y)
166+
end_pos = _transform_pos(layer.end_position.x, layer.end_position.y)
167+
if start_pos is None or end_pos is None:
168+
logging.warning(f"annotation_hook: could not transform point-pair coords for {shape_component} — skipping")
169+
return
170+
171+
raw_bw = float(layer.width if shape_type.startswith("Line.") else layer.border_width)
172+
wcs_border_width = _transform_scalar(raw_bw)
173+
174+
props = {
175+
"startPos": list(start_pos),
176+
"endPos": list(end_pos),
177+
"borderColor": [float(c) for c in layer.border_color],
178+
"borderWidth": [wcs_border_width],
179+
"startFrame": frame,
180+
"duration": 1,
181+
"eye": 2,
182+
"softDeleted": layer.visible is False,
183+
"uuid": [layer.id],
184+
}
185+
186+
if shape_type.startswith("Arrow."):
187+
wcs_thickness = _transform_scalar(float(layer.width))
188+
props["innerColor"] = [float(c) for c in layer.inner_color]
189+
props["thickness"] = [wcs_thickness]
190+
191+
effectHook.add_rv_effect_props(shape_component, props)
192+
193+
if layer.visible is not False:
194+
_add_shape_to_frame_order(paint_node, stroke_id, frame, prefix)
195+
196+
197+
def _create_text_shape(layer, paint_node, stroke_id, frame, source_node=None):
198+
"""Create an RVPaint text: node from a shape layer."""
199+
shape_component = f"{paint_node}.text:{stroke_id}:{frame}:annotation"
200+
201+
anchor = layer.anchor
202+
anchor_pos = _transform_pos(anchor.x, anchor.y)
203+
if anchor_pos is None:
204+
logging.warning(f"annotation_hook: could not transform text anchor for {shape_component} — skipping")
205+
return
206+
207+
# font_size in OTIO is in OTIO-space units (same coordinate space as
208+
# border_width). Convert to RVPaint logical pixels: OTIO → WCS via
209+
# _transform_scalar, then WCS → pixels by multiplying by image height.
210+
# Use the media track's source for height — the annotation source group
211+
# is a blank node whose sourceMediaInfo returns height=1, not the real
212+
# image dimensions.
213+
image_h = _get_source_image_height()
214+
font_size_wcs = _transform_scalar(float(layer.font_size))
215+
font_size_rv = font_size_wcs * image_h
216+
217+
# RVPaint .position is the BOTTOM of the text quad (y-up). The OTIO anchor
218+
# is the typographic baseline. Descent ≈ 0.2 × em, so the bottom of the
219+
# quad is ~0.2 × em below the baseline → shift position DOWN by 0.2 × em.
220+
anchor_pos = (anchor_pos[0], anchor_pos[1] - font_size_wcs * 0.2)
221+
222+
effectHook.add_rv_effect_props(
223+
shape_component,
224+
{
225+
"text": [layer.text],
226+
"fontFamily": [layer.font_family],
227+
"fontWeight": [layer.font_weight],
228+
"fontStyle": [layer.font_style],
229+
"textDecoration": [layer.text_decoration],
230+
"textAlign": [layer.text_align],
231+
"position": list(anchor_pos),
232+
"fontSize": [font_size_rv],
233+
"color": [float(c) for c in layer.inner_color],
234+
"startFrame": frame,
235+
"duration": 1,
236+
"softDeleted": layer.visible is False,
237+
"uuid": [layer.id],
238+
},
239+
)
240+
241+
if layer.visible is not False:
242+
_add_shape_to_frame_order(paint_node, stroke_id, frame, "text")
243+
244+
85245
def hook_function(in_timeline: otio.schemadef.Annotation.Annotation, argument_map: dict | None = None) -> None:
86246
"""A hook for the annotation schema"""
87247
try:
@@ -106,54 +266,71 @@ def hook_function(in_timeline: otio.schemadef.Annotation.Annotation, argument_ma
106266
paint_node = extra_commands.nodesInGroupOfType(source_node, "RVPaint")[0]
107267
paint_component = f"{paint_node}.paint"
108268
stroke_id = commands.getIntProperty(f"{paint_component}.nextId")[0] + 1
109-
pen_component = f"{paint_node}.pen:{stroke_id}:{frame}:annotation"
110269
frame_component = f"{paint_node}.frame:{frame}"
111270

112271
# Set properties on the paint component of the RVPaint node
113272
effectHook.set_rv_effect_props(paint_component, {"nextId": stroke_id})
114273

115-
start_time = int(time_range.start_time.value)
116-
end_time = int(start_time + time_range.duration.value)
117-
118-
duration = end_time - start_time
119-
120-
# Add and set properties on the pen component of the RVPaint node
121-
effectHook.add_rv_effect_props(
122-
pen_component,
123-
{
124-
"color": list(map(float, layer.rgba)),
125-
"brush": [layer.brush],
126-
"debug": 1,
127-
"join": 3,
128-
"cap": 1,
129-
"splat": 0,
130-
"mode": 0 if layer.type == "COLOR" else 1,
131-
"startFrame": start_time,
132-
"duration": duration,
133-
"softDeleted": layer.soft_deleted,
134-
"uuid": [layer.id],
135-
},
136-
)
137-
138-
points_property = f"{pen_component}.points"
139-
width_property = f"{pen_component}.width"
140-
141-
if not commands.propertyExists(points_property):
142-
commands.newProperty(points_property, commands.FloatType, 2)
143-
if not commands.propertyExists(width_property):
144-
commands.newProperty(width_property, commands.FloatType, 1)
145-
146-
for point in layer.points:
147-
world_coordinate_x, world_coordinate_y, world_coordinate_width = _transform_otio_to_world_coordinate(point)
148-
149-
commands.insertFloatProperty(
150-
points_property,
151-
[world_coordinate_x, world_coordinate_y],
274+
shape_label = getattr(layer, "_serializable_label", "")
275+
276+
if isinstance(layer, otio.schemadef.Paint.Paint):
277+
pen_component = f"{paint_node}.pen:{stroke_id}:{frame}:annotation"
278+
279+
start_time = int(time_range.start_time.value)
280+
end_time = int(start_time + time_range.duration.value)
281+
duration = end_time - start_time
282+
283+
# Add and set properties on the pen component of the RVPaint node
284+
effectHook.add_rv_effect_props(
285+
pen_component,
286+
{
287+
"color": list(map(float, layer.rgba)),
288+
"brush": [layer.brush],
289+
"debug": 1,
290+
"join": 3,
291+
"cap": 1,
292+
"splat": 0,
293+
"mode": 0 if layer.type == "COLOR" else 1,
294+
"startFrame": start_time,
295+
"duration": duration,
296+
"softDeleted": layer.soft_deleted,
297+
"uuid": [layer.id],
298+
},
152299
)
153-
commands.insertFloatProperty(width_property, [world_coordinate_width])
154300

155-
if not layer.soft_deleted:
156-
if not commands.propertyExists(f"{frame_component}.order"):
157-
commands.newProperty(f"{frame_component}.order", commands.StringType, 1)
301+
points_property = f"{pen_component}.points"
302+
width_property = f"{pen_component}.width"
303+
304+
if not commands.propertyExists(points_property):
305+
commands.newProperty(points_property, commands.FloatType, 2)
306+
if not commands.propertyExists(width_property):
307+
commands.newProperty(width_property, commands.FloatType, 1)
158308

159-
commands.insertStringProperty(f"{frame_component}.order", [f"pen:{stroke_id}:{frame}:annotation"])
309+
for point in layer.points:
310+
world_coordinate_x, world_coordinate_y, world_coordinate_width = _transform_otio_to_world_coordinate(
311+
point
312+
)
313+
314+
commands.insertFloatProperty(
315+
points_property,
316+
[world_coordinate_x, world_coordinate_y],
317+
)
318+
commands.insertFloatProperty(width_property, [world_coordinate_width])
319+
320+
if not layer.soft_deleted:
321+
if not commands.propertyExists(f"{frame_component}.order"):
322+
commands.newProperty(f"{frame_component}.order", commands.StringType, 1)
323+
324+
commands.insertStringProperty(f"{frame_component}.order", [f"pen:{stroke_id}:{frame}:annotation"])
325+
326+
elif shape_label.startswith(("Rectangle.", "Ellipse.")):
327+
_create_bbox_shape(layer, paint_node, stroke_id, frame, shape_label)
328+
329+
elif shape_label.startswith(("Arrow.", "Line.")):
330+
_create_point_pair_shape(layer, paint_node, stroke_id, frame, shape_label)
331+
332+
elif shape_label.startswith("Text."):
333+
_create_text_shape(layer, paint_node, stroke_id, frame, source_node)
334+
335+
else:
336+
logging.warning(f"annotation_hook: unrecognised layer type {shape_label!r} — skipping")

src/plugins/rv-packages/otio_reader/arrow_schema.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
arrow = otio.schemadef.Arrow.Arrow(
1616
start_position=otio.schemadef.Position.Position(x=-4.0, y=0.0),
1717
end_position=otio.schemadef.Position.Position(x=4.0, y=0.0),
18-
innerColor=[1.0, 1.0, 0.0, 1.0],
19-
borderColor=[0.8, 0.8, 0.0, 1.0],
18+
inner_color=[1.0, 1.0, 0.0, 1.0],
19+
border_color=[0.8, 0.8, 0.0, 1.0],
2020
width=0.15,
21-
borderWidth=0.02,
21+
border_width=0.02,
2222
id="c3d4e5f6-a7b8-9012-cdef-123456789012",
2323
visible=True,
2424
layer_range=otio.opentime.TimeRange(...),
@@ -39,21 +39,21 @@ def __init__(
3939
self,
4040
start_position=None,
4141
end_position=None,
42-
innerColor: list | None = None,
43-
borderColor: list | None = None,
42+
inner_color: list | None = None,
43+
border_color: list | None = None,
4444
width: float | None = None,
45-
borderWidth: float | None = None,
45+
border_width: float | None = None,
4646
id: str | None = None,
4747
visible: bool | None = None,
4848
layer_range: otio.opentime.TimeRange | None = None,
4949
) -> None:
5050
super().__init__()
5151
self.start_position = start_position
5252
self.end_position = end_position
53-
self.innerColor = innerColor
54-
self.borderColor = borderColor
53+
self.inner_color = inner_color
54+
self.border_color = border_color
5555
self.width = width
56-
self.borderWidth = borderWidth
56+
self.border_width = border_width
5757
self.id = id
5858
self.visible = visible
5959
self.layer_range = layer_range
@@ -63,31 +63,31 @@ def __init__(
6363
"end_position", doc="Tip of the arrow — arrowhead points here (Position.1)"
6464
)
6565

66-
_innerColor = otio.core.serializable_field("innerColor", required_type=list, doc="Shaft fill colour [r, g, b, a]")
66+
_inner_color = otio.core.serializable_field("inner_color", required_type=list, doc="Shaft fill colour [r, g, b, a]")
6767

6868
@property
69-
def innerColor(self) -> list:
70-
return self._innerColor
69+
def inner_color(self) -> list:
70+
return self._inner_color
7171

72-
@innerColor.setter
73-
def innerColor(self, val: list) -> None:
74-
self._innerColor = val
72+
@inner_color.setter
73+
def inner_color(self, val: list) -> None:
74+
self._inner_color = val
7575

76-
_borderColor = otio.core.serializable_field("borderColor", required_type=list, doc="Outline colour [r, g, b, a]")
76+
_border_color = otio.core.serializable_field("border_color", required_type=list, doc="Outline colour [r, g, b, a]")
7777

7878
@property
79-
def borderColor(self) -> list:
80-
return self._borderColor
79+
def border_color(self) -> list:
80+
return self._border_color
8181

82-
@borderColor.setter
83-
def borderColor(self, val: list) -> None:
84-
self._borderColor = val
82+
@border_color.setter
83+
def border_color(self, val: list) -> None:
84+
self._border_color = val
8585

8686
width = otio.core.serializable_field(
8787
"width", required_type=float, doc="Shaft width in normalised units; arrowhead scales proportionally"
8888
)
89-
borderWidth = otio.core.serializable_field(
90-
"borderWidth", required_type=float, doc="Outline width in normalised units"
89+
border_width = otio.core.serializable_field(
90+
"border_width", required_type=float, doc="Outline width in normalised units"
9191
)
9292
id = otio.core.serializable_field("id", required_type=str, doc="UUID for undo/redo tracking")
9393
visible = otio.core.serializable_field("visible", required_type=bool, doc="Show/hide")

0 commit comments

Comments
 (0)